summaryrefslogtreecommitdiff
path: root/modules/home-manager/personal/gui/x/i3
diff options
context:
space:
mode:
Diffstat (limited to 'modules/home-manager/personal/gui/x/i3')
-rw-r--r--modules/home-manager/personal/gui/x/i3/bar/default.nix27
-rw-r--r--modules/home-manager/personal/gui/x/i3/bar/i3status.go289
-rw-r--r--modules/home-manager/personal/gui/x/i3/default.nix56
-rw-r--r--modules/home-manager/personal/gui/x/i3/keybindings.nix47
-rw-r--r--modules/home-manager/personal/gui/x/i3/startup.nix25
5 files changed, 444 insertions, 0 deletions
diff --git a/modules/home-manager/personal/gui/x/i3/bar/default.nix b/modules/home-manager/personal/gui/x/i3/bar/default.nix
new file mode 100644
index 0000000..58d4bce
--- /dev/null
+++ b/modules/home-manager/personal/gui/x/i3/bar/default.nix
@@ -0,0 +1,27 @@
+{ config, lib, pkgs, ... }@extraArgs:
+
+let
+ statusPackage =
+ pkgs.personal.barista.override { i3statusGo = ./i3status.go; };
+in {
+ xsession.windowManager.i3.config.bars = [{
+ statusCommand = "${statusPackage}/bin/i3status";
+ fonts = {
+ names = [ "roboto" ];
+ size = 11.0;
+ };
+ colors.background = "#111111";
+ }];
+
+ home.packages = with pkgs;
+ lib.optionals
+ (config.xsession.enable && config.xsession.windowManager.i3.enable) [
+ material-design-icons
+ roboto
+ # source-code-pro
+ ];
+
+ # (Miscellaneous) Tray icons
+ services.blueman-applet.enable =
+ lib.mkDefault (extraArgs.osConfig.services.blueman.enable);
+}
diff --git a/modules/home-manager/personal/gui/x/i3/bar/i3status.go b/modules/home-manager/personal/gui/x/i3/bar/i3status.go
new file mode 100644
index 0000000..196e6cc
--- /dev/null
+++ b/modules/home-manager/personal/gui/x/i3/bar/i3status.go
@@ -0,0 +1,289 @@
+package main
+
+import (
+ "io"
+ "net/http"
+ "os"
+ "regexp"
+ "strconv"
+ "time"
+
+ "barista.run"
+ "barista.run/bar"
+ "barista.run/colors"
+ "barista.run/group"
+ "barista.run/modules/battery"
+ "barista.run/modules/clock"
+ "barista.run/modules/diskspace"
+ "barista.run/modules/funcs"
+ "barista.run/modules/netinfo"
+ "barista.run/modules/systemd"
+ "barista.run/modules/volume"
+ "barista.run/modules/volume/pulseaudio"
+ "barista.run/modules/wlan"
+ "barista.run/outputs"
+ "barista.run/pango"
+ "barista.run/pango/icons/mdi"
+)
+
+func main() {
+ // Constants
+ colors.LoadFromMap(map[string]string{
+ // Color palette of Cezanne's Vue de la Baie de Marseille
+ "good": "#C5D294",
+ "degraded": "#E9CC67",
+ "bad": "#FFBC88",
+ })
+ mdi.Load() // repo path will be inserted at build time
+
+ // Display status of several services
+ updateSuccessIcon := pango.Icon("mdi-reload")
+ updatingIcon := pango.Icon("mdi-update")
+ updateFailIcon := pango.Icon("mdi-reload-alert")
+ garbageFullIcon := pango.Icon("mdi-delete")
+ garbageEmptyingIcon := pango.Icon("mdi-delete-restore")
+ garbageEmptyIcon := pango.Icon("mdi-delete-outline")
+ barista.Add(group.Simple(systemd.Service("nixos-upgrade").Output(func(i systemd.ServiceInfo) bar.Output {
+ state := i.UnitInfo.State
+ var colorScheme string
+ var output *pango.Node
+ switch {
+ case state == systemd.StateInactive:
+ colorScheme = "good"
+ output = updateSuccessIcon
+ case state == systemd.StateActivating:
+ colorScheme = "degraded"
+ output = updatingIcon
+ default:
+ colorScheme = "bad"
+ output = updateFailIcon
+ }
+ return outputs.Pango(output).Color(colors.Scheme(colorScheme))
+ }),
+ systemd.Service("nix-gc").Output(func(i systemd.ServiceInfo) bar.Output {
+ state := i.UnitInfo.State
+ var colorScheme string
+ var output *pango.Node
+ switch {
+ case state == systemd.StateInactive:
+ colorScheme = "good"
+ output = garbageEmptyIcon
+ case state == systemd.StateActivating:
+ colorScheme = "degraded"
+ output = garbageEmptyingIcon
+ default:
+ colorScheme = "bad"
+ output = garbageFullIcon
+ }
+ return outputs.Pango(output).Color(colors.Scheme(colorScheme))
+ })))
+
+ // Display space left on /
+ storageIcon := pango.Icon("mdi-database")
+ barista.Add(diskspace.New("/").Output(func(i diskspace.Info) bar.Output {
+ used := i.UsedPct()
+ var colorScheme string
+ if used >= 90 {
+ colorScheme = "bad"
+ } else if used >= 50 {
+ colorScheme = "degraded"
+ } else {
+ colorScheme = "good"
+ }
+ return outputs.Pango(storageIcon, pango.Textf(" %d%%", used)).Color(colors.Scheme(colorScheme))
+ }))
+
+ // Check connection to the Mullvad VPN
+ mullvadIsUpRe := regexp.MustCompile(`^You are connected to Mullvad`)
+ mullvadServerRe := regexp.MustCompile(`\(server (.*)\)`)
+ mullvadIpRe := regexp.MustCompile(`Your IP address is (.*)`)
+ client := &http.Client{Timeout: 3 * time.Second}
+ incognitoIcon := pango.Icon("mdi-incognito")
+ incognitoOffIcon := pango.Icon("mdi-incognito-off")
+ barista.Add(funcs.Every(5*time.Second, func(s bar.Sink) {
+ icon := incognitoOffIcon
+ message := pango.Text("")
+ colorScheme := "bad"
+ res, err := client.Get("https://am.i.mullvad.net/connected")
+ if !s.Error(err) {
+ status, err := io.ReadAll(res.Body)
+ res.Body.Close()
+ if !s.Error(err) {
+ var re *regexp.Regexp
+ if mullvadIsUpRe.Match(status) {
+ re = mullvadServerRe
+ colorScheme = "good"
+ icon = incognitoIcon
+ } else {
+ re = mullvadIpRe
+ colorScheme = "degraded"
+ }
+ result := re.FindSubmatch(status)
+ if len(result) >= 2 {
+ message = pango.Textf(" %s", result[1])
+ }
+ }
+ }
+ client.CloseIdleConnections()
+ s.Output(outputs.Pango(icon, message).Color(colors.Scheme(colorScheme)))
+ }))
+
+ // Display the wifi status
+ wifiOffIcon := pango.Icon("mdi-wifi-off")
+ wifiRefreshIcon := pango.Icon("mdi-wifi-refresh")
+ wifiOnIcon := pango.Icon("mdi-wifi")
+ barista.Add(wlan.Named("wlp2s0").Output(func(w wlan.Info) bar.Output {
+ var output *pango.Node
+ var colorScheme string
+ switch {
+ case w.Connected():
+ output = pango.New(wifiOnIcon, pango.Textf(" %s", w.SSID))
+ colorScheme = "good"
+ case w.Connecting():
+ output = wifiRefreshIcon
+ colorScheme = "degraded"
+ default:
+ output = wifiOffIcon
+ colorScheme = "bad"
+ }
+ return outputs.Pango(output).Color(colors.Scheme(colorScheme))
+ }))
+
+ // Display the ethernet status
+ ethernetCableOnIcon := pango.Icon("mdi-ethernet-cable")
+ ethernetCableOffIcon := pango.Icon("mdi-ethernet-cable-off")
+ barista.Add(netinfo.Prefix("e").Output(func(s netinfo.State) bar.Output {
+ var output *pango.Node
+ var colorScheme string
+ switch {
+ case s.Connected():
+ ip := "<no ip>"
+ if len(s.IPs) > 0 {
+ ip = s.IPs[0].String()
+ }
+ output = pango.New(ethernetCableOnIcon, pango.Textf(" %s", ip))
+ colorScheme = "good"
+ case s.Connecting():
+ output = ethernetCableOnIcon
+ colorScheme = "degraded"
+ default:
+ output = ethernetCableOffIcon
+ colorScheme = "bad"
+ }
+ return outputs.Pango(output).Color(colors.Scheme(colorScheme))
+ }))
+
+ // Display the battery status
+ batteryIcons := [11]*pango.Node{pango.Icon("mdi-battery-outline"),
+ pango.Icon("mdi-battery-10"),
+ pango.Icon("mdi-battery-20"),
+ pango.Icon("mdi-battery-30"),
+ pango.Icon("mdi-battery-40"),
+ pango.Icon("mdi-battery-50"),
+ pango.Icon("mdi-battery-60"),
+ pango.Icon("mdi-battery-70"),
+ pango.Icon("mdi-battery-80"),
+ pango.Icon("mdi-battery-90"),
+ pango.Icon("mdi-battery")}
+ batteryChargingIcons := [11]*pango.Node{pango.Icon("mdi-battery-charging-outline"),
+ pango.Icon("mdi-battery-charging-10"),
+ pango.Icon("mdi-battery-charging-20"),
+ pango.Icon("mdi-battery-charging-30"),
+ pango.Icon("mdi-battery-charging-40"),
+ pango.Icon("mdi-battery-charging-50"),
+ pango.Icon("mdi-battery-charging-60"),
+ pango.Icon("mdi-battery-charging-70"),
+ pango.Icon("mdi-battery-charging-80"),
+ pango.Icon("mdi-battery-charging-90"),
+ pango.Icon("mdi-battery-charging-100")}
+ barista.Add(battery.All().Output(func(b battery.Info) bar.Output {
+ switch b.Status {
+ case battery.Disconnected, battery.Unknown:
+ return nil
+ default:
+ var icons [11]*pango.Node
+ var colorScheme string
+ if b.Status == battery.Charging {
+ icons = batteryChargingIcons
+ colorScheme = "good"
+ } else {
+ icons = batteryIcons
+ if b.RemainingPct() <= 10 {
+ colorScheme = "bad"
+ } else if b.RemainingPct() <= 20 {
+ colorScheme = "degraded"
+ } else {
+ colorScheme = "good"
+ }
+ }
+ icon := icons[b.RemainingPct()/10]
+ return outputs.Pango(icon, pango.Textf(" %d%%", b.RemainingPct())).Color(colors.Scheme(colorScheme))
+ }
+ }))
+
+ // Display brightness
+ brightnessHighIcon := pango.Icon("mdi-lightbulb-on")
+ brightnessMidIcon := pango.Icon("mdi-lightbulb-on-outline")
+ brightnessLowIcon := pango.Icon("mdi-lightbulb-outline")
+ ReadBrightness := func(name string) (int, error) {
+ valueStr, err := os.ReadFile("/sys/class/backlight/intel_backlight/" + name)
+ if err != nil {
+ return 0, err
+ }
+ return strconv.Atoi(string(valueStr[:len(valueStr)-1]))
+ }
+ brightnessMax, _ := ReadBrightness("max_brightness") // always non-zero, unless there's an error
+ barista.Add(funcs.Every(time.Second, func(s bar.Sink) {
+ brightness, err := ReadBrightness("brightness")
+ if !s.Error(err) {
+ value := (brightness * 100) / brightnessMax
+ var icon *pango.Node
+ if value <= 30 {
+ icon = brightnessLowIcon
+ } else if value < 70 {
+ icon = brightnessMidIcon
+ } else {
+ icon = brightnessHighIcon
+ }
+ s.Output(outputs.Pango(icon, pango.Textf(" %d%%", value)))
+ }
+ }))
+
+ // Display output volume
+ volumeOffIcon := pango.Icon("mdi-volume-variant-off")
+ volumeLowIcon := pango.Icon("mdi-volume-low")
+ volumeMidIcon := pango.Icon("mdi-volume-medium")
+ volumeHighIcon := pango.Icon("mdi-volume-high")
+ barista.Add(volume.New(pulseaudio.DefaultSink()).Output(func(v volume.Volume) bar.Output {
+ volume := v.Pct()
+ var icon *pango.Node
+ if volume == 0 || v.Mute {
+ icon = volumeOffIcon
+ } else if volume <= 30 {
+ icon = volumeLowIcon
+ } else if volume <= 70 {
+ icon = volumeMidIcon
+ } else {
+ icon = volumeHighIcon
+ }
+ return outputs.Pango(icon, pango.Textf(" %d%%", volume))
+ }))
+
+ // Display microphone volume
+ microphoneOffIcon := pango.Icon("mdi-microphone-off")
+ microphoneIcon := pango.Icon("mdi-microphone")
+ barista.Add(volume.New(pulseaudio.DefaultSource()).Output(func(v volume.Volume) bar.Output {
+ volume := v.Pct() // the value returned by pulseaudio may be weird
+ var icon *pango.Node
+ if volume == 0 || v.Mute {
+ icon = microphoneOffIcon
+ } else {
+ icon = microphoneIcon
+ }
+ return outputs.Pango(icon, pango.Textf(" %d%%", volume))
+ }))
+
+ barista.Add(clock.Local().OutputFormat("2006-01-02 15:04:05"))
+
+ panic(barista.Run())
+}
diff --git a/modules/home-manager/personal/gui/x/i3/default.nix b/modules/home-manager/personal/gui/x/i3/default.nix
new file mode 100644
index 0000000..beae770
--- /dev/null
+++ b/modules/home-manager/personal/gui/x/i3/default.nix
@@ -0,0 +1,56 @@
+{ config, lib, pkgs, ... }@extraArgs:
+
+let cfg = config.personal.x.i3;
+in {
+ imports = [ ./bar ./keybindings.nix ./startup.nix ];
+
+ options.personal.x.i3 = {
+ enable = lib.mkEnableOption "i3" // {
+ default =
+ extraArgs.osConfig.services.xserver.windowManager.i3.enable or false;
+ };
+ };
+
+ config = lib.mkIf cfg.enable {
+ xsession.windowManager.i3 = {
+ enable = cfg.enable;
+ package = lib.mkDefault pkgs.i3-gaps;
+
+ config = {
+ assigns = lib.optionalAttrs (config.personal.profiles.multimedia
+ && (extraArgs.osConfig.programs.steam.enable or true)) {
+ "8: multimedia" = [
+ { class = "^Steam$"; }
+ { class = "Netflix"; }
+ { class = "MUBI"; }
+ { class = "Deezer"; }
+ ];
+ } // lib.optionalAttrs config.personal.profiles.social {
+ "9: social" = [
+ { class = "^Mail$"; }
+ { class = "^thunderbird$"; }
+ { class = "^Signal$"; }
+ ];
+ } // {
+ "10: passwords" = [{
+ # matches <some db>.kbdx [Locked] - KeePassXC
+ title = ".*\\.kbdx \\[Locked\\] - KeePassXC$";
+ }];
+ };
+
+ workspaceAutoBackAndForth = lib.mkDefault true;
+
+ window = {
+ titlebar = lib.mkDefault false;
+ border = lib.mkDefault 0;
+ };
+ floating.titlebar = lib.mkDefault false;
+ gaps = {
+ inner = lib.mkDefault 15;
+ outer = lib.mkDefault 5;
+ };
+ };
+ };
+ programs.rofi.enable = lib.mkDefault true;
+ };
+}
diff --git a/modules/home-manager/personal/gui/x/i3/keybindings.nix b/modules/home-manager/personal/gui/x/i3/keybindings.nix
new file mode 100644
index 0000000..3781867
--- /dev/null
+++ b/modules/home-manager/personal/gui/x/i3/keybindings.nix
@@ -0,0 +1,47 @@
+{ config, lib, pkgs, ... }:
+
+let
+ brightnessctl = "${pkgs.brightnessctl}/bin/brightnessctl";
+ brightnessctlKbd = "${brightnessctl} --device dell:kbd_backlight";
+ volumectl = "${pkgs.pulseaudio}/bin/pactl";
+ screenshot = "${pkgs.shutter}/bin/shutter";
+
+ modifier = "Mod4";
+in {
+ xsession.windowManager.i3.config = {
+ inherit modifier;
+
+ keybindings = lib.mkOptionDefault {
+ # launching apps
+ "${modifier}+Control+Return" = ''exec "$EDITOR"'';
+ "${modifier}+Shift+Return" = ''exec "$BROWSER"'';
+ "${modifier}+d" = lib.mkIf config.programs.rofi.enable
+ ''exec "rofi -modi drun,filebrowser,run,window -show drun"'';
+
+ # exiting
+ "${modifier}+Shift+e" = "exec i3-msg exit";
+ "${modifier}+l" =
+ "exec ${config.personal.home.lockscreen}/bin/lockscreen.sh";
+
+ # media keys
+ "XF86MonBrightnessUp" = "exec ${brightnessctl} set 5%+";
+ "XF86MonBrightnessDown" = "exec ${brightnessctl} set 5%-";
+ "XF86AudioRaiseVolume" =
+ "exec ${volumectl} set-sink-volume @DEFAULT_SINK@ +5%";
+ "XF86AudioLowerVolume" =
+ "exec ${volumectl} set-sink-volume @DEFAULT_SINK@ -5%";
+ "XF86AudioMute" = "exec ${volumectl} set-sink-mute @DEFAULT_SINK@ toggle";
+ "Shift+XF86AudioRaiseVolume" =
+ "exec ${volumectl} set-source-volume @DEFAULT_SOURCE@ +5%";
+ "Shift+XF86AudioLowerVolume" =
+ "exec ${volumectl} set-source-volume @DEFAULT_SOURCE@ -5%";
+ "XF86AudioMicMute" =
+ "exec ${volumectl} set-source-mute @DEFAULT_SOURCE@ toggle";
+ "XF86KbdBrightnessUp" = ''
+ exec ${brightnessctlKbd} set \
+ $(( $(${brightnessctlKbd} max) - $(${brightnessctlKbd} get) ))
+ '';
+ "Print" = "exec ${screenshot}";
+ };
+ };
+}
diff --git a/modules/home-manager/personal/gui/x/i3/startup.nix b/modules/home-manager/personal/gui/x/i3/startup.nix
new file mode 100644
index 0000000..9baf388
--- /dev/null
+++ b/modules/home-manager/personal/gui/x/i3/startup.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+{
+ xsession.windowManager.i3.config.startup = let
+ autostart = { command, always ? false, notification ? false }: {
+ inherit command always notification;
+ };
+ autostartIf = cond: args: lib.optional cond (autostart args);
+ in [
+ (autostart { command = "rfkill block bluetooth"; })
+ (autostart { command = "keepassxc"; })
+ ]
+ ++ autostartIf config.programs.thunderbird.enable { command = "thunderbird"; }
+ ++ autostartIf config.personal.profiles.social { command = "signal-desktop"; }
+ # ++ autostartIf config.services.redshift.enable {
+ # command = "systemctl --user start redshift";
+ # }
+ ++ autostartIf (config.personal.home.wallpaper != null) {
+ command = "${pkgs.feh}/bin/feh --bg-scale ${config.personal.home.wallpaper}";
+ }
+ # ++ autostartIf config.services.xidlehook.enable {
+ # command = "systemctl --user start xidlehook.service";
+ # }
+ ;
+}