From 48f27bce7576955fe3567c2ec775ed6a45c86b90 Mon Sep 17 00:00:00 2001 From: alexion Date: Sun, 11 Oct 2026 19:34:12 -0400 Subject: [PATCH] refactor(waybar): register custom modules --- .../desktop/session/hyprland/hyprlock.nix | 18 ++ .../nixos/modules/desktop/session/mako.nix | 30 ++ .../nixos/modules/desktop/session/waybar.nix | 279 ++++++++---------- .../modules/desktop/utilities/recording.nix | 36 ++- configs/nixos/modules/network/vpn-client.nix | 29 ++ configs/nixos/registry/waybar-module.nix | 29 ++ 6 files changed, 254 insertions(+), 167 deletions(-) create mode 100644 configs/nixos/registry/waybar-module.nix diff --git a/configs/nixos/modules/desktop/session/hyprland/hyprlock.nix b/configs/nixos/modules/desktop/session/hyprland/hyprlock.nix index 0fde487..c6dc895 100644 --- a/configs/nixos/modules/desktop/session/hyprland/hyprlock.nix +++ b/configs/nixos/modules/desktop/session/hyprland/hyprlock.nix @@ -37,6 +37,14 @@ let ${lib.getExe' pkgs.swayosd "swayosd-client"} --custom-message "${sleepOffIcon}" fi ''; + + idleLockStatus = pkgs.writeShellScript "waybar-idle-lock-status" '' + if ${lib.getExe' pkgs.systemd "systemctl"} --user is-active --quiet lockscreen-idle-inhibit.service; then + printf '{"text":"${sleepOffIcon}","tooltip":"Idle locking inhibited","class":"inhibited"}\n' + else + printf '{"text":"","class":"idle"}\n' + fi + ''; in { options.modules.desktop.session.hyprland.hyprlock.enable = @@ -97,5 +105,15 @@ in }; }; }; + + registry.waybar-module.${config.networking.hostName}."custom/idle-lock" = { + order = 20; + settings = { + return-type = "json"; + exec = "${idleLockStatus}"; + interval = "once"; + signal = 9; + }; + }; }; } diff --git a/configs/nixos/modules/desktop/session/mako.nix b/configs/nixos/modules/desktop/session/mako.nix index 48b331d..2d2a4a9 100644 --- a/configs/nixos/modules/desktop/session/mako.nix +++ b/configs/nixos/modules/desktop/session/mako.nix @@ -5,6 +5,25 @@ ... }: # mako as the notification daemon: toasts, a do-not-disturb mode, and history recall. +let + glyph = code: builtins.fromJSON ''"\u${code}"''; + + # Renders and toggles mako's do-not-disturb mode. + # With no mako daemon answering, the status reads "notifications on", keeping + # the widget inert rather than broken. + dndStatus = pkgs.writeShellScript "waybar-dnd-status" '' + if ${lib.getExe' pkgs.mako "makoctl"} mode 2>/dev/null | ${lib.getExe pkgs.gnugrep} -qx dnd; then + printf '{"text":"${glyph "f1f6"}","tooltip":"Do not disturb: on","class":"dnd"}\n' + else + printf '{"text":"${glyph "f0f3"}","tooltip":"Notifications on","class":"active"}\n' + fi + ''; + + dndToggle = pkgs.writeShellScript "waybar-dnd-toggle" '' + ${lib.getExe' pkgs.mako "makoctl"} mode -t dnd >/dev/null 2>&1 + ${lib.getExe' pkgs.procps "pkill"} -RTMIN+8 waybar + ''; +in { options.modules.desktop.session.mako.enable = lib.mkEnableOption "the mako notification daemon"; @@ -35,5 +54,16 @@ command = "${lib.getExe' pkgs.mako "makoctl"} restore"; }; }; + + registry.waybar-module.${config.networking.hostName}."custom/dnd" = { + order = 80; + settings = { + return-type = "json"; + exec = "${dndStatus}"; + on-click = "${dndToggle}"; + interval = "once"; + signal = 8; + }; + }; }; } diff --git a/configs/nixos/modules/desktop/session/waybar.nix b/configs/nixos/modules/desktop/session/waybar.nix index 76447d0..6fbb102 100644 --- a/configs/nixos/modules/desktop/session/waybar.nix +++ b/configs/nixos/modules/desktop/session/waybar.nix @@ -6,64 +6,62 @@ }: # Waybar: a top status bar reading system state at a glance. let - vpnClient = config.modules.network.vpn-client; + waybarModulesFor = + position: + map (module: module.name) ( + lib.sort (a: b: a.order < b.order) ( + lib.filter (module: module.position == position) (staticWaybarModules ++ registeredWaybarModules) + ) + ); + + staticWaybarModules = [ + { + name = "hyprland/workspaces"; + position = "left"; + order = 10; + } + { + name = "clock"; + position = "center"; + order = 10; + } + { + name = "mpris"; + position = "right"; + order = 30; + } + { + name = "wireplumber"; + position = "right"; + order = 40; + } + { + name = "custom/network"; + position = "right"; + order = 60; + } + { + name = "battery"; + position = "right"; + order = 70; + } + ]; + + registeredWaybarModules = lib.mapAttrsToList ( + name: module: + module + // { + inherit name; + } + ) hostWaybarModules; + + hostWaybarModules = lib.attrByPath [ config.networking.hostName ] { } config.registry.waybar-module; # A Nerd Font Private-Use-Area codepoint as a literal character, decoded # through JSON so the glyph survives as bytes rather than an editor paste. g = code: builtins.fromJSON ''"\u${code}"''; - sleepOffIcon = builtins.fromJSON ''"\udb81\udcb3"''; - - # Renders and toggles mako's do-not-disturb mode. - # With no mako daemon answering, the status reads "notifications on", keeping - # the widget inert rather than broken. - dndStatus = pkgs.writeShellScript "waybar-dnd-status" '' - if ${lib.getExe' pkgs.mako "makoctl"} mode 2>/dev/null | ${lib.getExe pkgs.gnugrep} -qx dnd; then - printf '{"text":"${g "f1f6"}","tooltip":"Do not disturb: on","class":"dnd"}\n' - else - printf '{"text":"${g "f0f3"}","tooltip":"Notifications on","class":"active"}\n' - fi - ''; - - dndToggle = pkgs.writeShellScript "waybar-dnd-toggle" '' - ${lib.getExe' pkgs.mako "makoctl"} mode -t dnd >/dev/null 2>&1 - ${lib.getExe' pkgs.procps "pkill"} -RTMIN+8 waybar - ''; - - # Lit while a wf-recorder capture is running. - # Empty text collapses the widget when idle, so it shows nothing until then. - recStatus = pkgs.writeShellScript "waybar-recording-status" '' - if ${lib.getExe' pkgs.procps "pgrep"} -x wf-recorder >/dev/null; then - printf '{"text":"${g "f03d"}","tooltip":"Recording","class":"recording"}\n' - else - printf '{"text":"","class":"idle"}\n' - fi - ''; - - idleLockStatus = pkgs.writeShellScript "waybar-idle-lock-status" '' - if ${lib.getExe' pkgs.systemd "systemctl"} --user is-active --quiet lockscreen-idle-inhibit.service; then - printf '{"text":"${sleepOffIcon}","tooltip":"Idle locking inhibited","class":"inhibited"}\n' - else - printf '{"text":"","class":"idle"}\n' - fi - ''; - - vpnStatus = pkgs.writeShellScript "waybar-vpn-status" '' - interface=${lib.escapeShellArg vpnClient.interface} - - if ! ${lib.getExe' pkgs.iproute2 "ip"} link show dev "$interface" >/dev/null 2>&1; then - printf '{"text":"","class":"idle"}\n' - exit 0 - fi - - ipaddr=$(${lib.getExe' pkgs.iproute2 "ip"} -4 -o addr show dev "$interface" | ${lib.getExe' pkgs.gawk "awk"} '{ print $4; exit }') - - ${lib.getExe pkgs.jq} -cn \ - --arg text "󰲝" \ - --arg tooltip "VPN $interface: ''${ipaddr:-no IPv4}" \ - --arg class "active" \ - '{ text: $text, tooltip: $tooltip, class: $class }' - ''; + registeredWaybarModuleSettings = lib.mapAttrs (_name: module: module.settings) hostWaybarModules; networkStatus = pkgs.writeShellScript "waybar-network-status" '' active=$( @@ -122,120 +120,83 @@ in } ''; - settings.mainBar = { - layer = "top"; - position = "top"; - height = 30; - spacing = 8; + settings.mainBar = lib.mkMerge [ + { + layer = "top"; + position = "top"; + height = 30; + spacing = 8; - modules-left = [ "hyprland/workspaces" ]; - modules-center = [ "clock" ]; - modules-right = [ - "custom/recording" - "custom/idle-lock" - "mpris" - "wireplumber" - ] - ++ lib.optional vpnClient.enable "custom/vpn" - ++ [ - "custom/network" - "battery" - "custom/dnd" - ]; + modules-left = waybarModulesFor "left"; + modules-center = waybarModulesFor "center"; + modules-right = waybarModulesFor "right"; - # Per-application icons for the windows on each workspace, mapped from - # window class with a neutral glyph for anything unlisted. - "hyprland/workspaces" = { - on-click = "activate"; - format = "{id} {windows}"; - format-window-separator = " "; - window-rewrite-default = g "f2d0"; - window-rewrite = lib.mapAttrs' ( - _name: windowIcon: lib.nameValuePair "class<${windowIcon.class}>" (g windowIcon.icon) - ) (lib.attrByPath [ config.networking.hostName ] { } config.registry.window-icon); - }; - - clock = { - format = "{:%a %d %b %H:%M}"; - tooltip-format = "{calendar}"; - calendar = { - mode = "month"; - # Underline the current day so it stands out in the grid. - format.today = "{}"; + # Per-application icons for the windows on each workspace, mapped from + # window class with a neutral glyph for anything unlisted. + "hyprland/workspaces" = { + on-click = "activate"; + format = "{id} {windows}"; + format-window-separator = " "; + window-rewrite-default = g "f2d0"; + window-rewrite = lib.mapAttrs' ( + _name: windowIcon: lib.nameValuePair "class<${windowIcon.class}>" (g windowIcon.icon) + ) (lib.attrByPath [ config.networking.hostName ] { } config.registry.window-icon); }; - }; - mpris = { - format = "${g "f001"} {title}"; - format-paused = "${g "f04c"} {title}"; - max-length = 40; - on-click = "play-pause"; - on-scroll-up = "next"; - on-scroll-down = "previous"; - }; - - wireplumber = { - format = "{icon} {volume}%"; - format-muted = "${g "f026"} muted"; - format-icons = [ - (g "f026") - (g "f027") - (g "f028") - ]; - on-click = "${lib.getExe' pkgs.wireplumber "wpctl"} set-mute @DEFAULT_AUDIO_SINK@ toggle"; - }; - - "custom/vpn" = { - return-type = "json"; - exec = "${vpnStatus}"; - interval = 5; - }; - - "custom/network" = { - return-type = "json"; - exec = "${networkStatus}"; - interval = 5; - }; - - battery = { - format = "{icon} {capacity}%"; - format-charging = "${g "f0e7"} {capacity}%"; - format-icons = [ - (g "f244") - (g "f243") - (g "f242") - (g "f241") - (g "f240") - ]; - states = { - warning = 20; - critical = 10; + clock = { + format = "{:%a %d %b %H:%M}"; + tooltip-format = "{calendar}"; + calendar = { + mode = "month"; + # Underline the current day so it stands out in the grid. + format.today = "{}"; + }; }; - }; - "custom/dnd" = { - return-type = "json"; - exec = "${dndStatus}"; - on-click = "${dndToggle}"; - interval = "once"; - signal = 8; - }; + mpris = { + format = "${g "f001"} {title}"; + format-paused = "${g "f04c"} {title}"; + max-length = 40; + on-click = "play-pause"; + on-scroll-up = "next"; + on-scroll-down = "previous"; + }; - # Recording state has no event to hook, so the widget samples the - # wf-recorder process once a second. - "custom/recording" = { - return-type = "json"; - exec = "${recStatus}"; - interval = 1; - }; + wireplumber = { + format = "{icon} {volume}%"; + format-muted = "${g "f026"} muted"; + format-icons = [ + (g "f026") + (g "f027") + (g "f028") + ]; + on-click = "${lib.getExe' pkgs.wireplumber "wpctl"} set-mute @DEFAULT_AUDIO_SINK@ toggle"; + }; - "custom/idle-lock" = { - return-type = "json"; - exec = "${idleLockStatus}"; - interval = "once"; - signal = 9; - }; - }; + "custom/network" = { + return-type = "json"; + exec = "${networkStatus}"; + interval = 5; + }; + + battery = { + format = "{icon} {capacity}%"; + format-charging = "${g "f0e7"} {capacity}%"; + format-icons = [ + (g "f244") + (g "f243") + (g "f242") + (g "f241") + (g "f240") + ]; + states = { + warning = 20; + critical = 10; + }; + }; + } + registeredWaybarModuleSettings + ]; }; }; } diff --git a/configs/nixos/modules/desktop/utilities/recording.nix b/configs/nixos/modules/desktop/utilities/recording.nix index 1fa9e63..e73f43e 100644 --- a/configs/nixos/modules/desktop/utilities/recording.nix +++ b/configs/nixos/modules/desktop/utilities/recording.nix @@ -6,14 +6,16 @@ }: # Keybound screen recording: pick a region, then toggle a video-only capture. let - slurp = "${pkgs.slurp}/bin/slurp"; - wf-recorder = "${pkgs.wf-recorder}/bin/wf-recorder"; - notify-send = "${pkgs.libnotify}/bin/notify-send"; - pgrep = "${pkgs.procps}/bin/pgrep"; - pkill = "${pkgs.procps}/bin/pkill"; - date = "${pkgs.coreutils}/bin/date"; - mkdir = "${pkgs.coreutils}/bin/mkdir"; - xdgUserDir = "${pkgs.xdg-user-dirs}/bin/xdg-user-dir"; + slurp = lib.getExe pkgs.slurp; + wf-recorder = lib.getExe pkgs.wf-recorder; + notify-send = lib.getExe' pkgs.libnotify "notify-send"; + pgrep = lib.getExe' pkgs.procps "pgrep"; + pkill = lib.getExe' pkgs.procps "pkill"; + date = lib.getExe' pkgs.coreutils "date"; + mkdir = lib.getExe' pkgs.coreutils "mkdir"; + xdgUserDir = lib.getExe' pkgs.xdg-user-dirs "xdg-user-dir"; + + glyph = code: builtins.fromJSON ''"\u${code}"''; # A single key both starts and stops. # A running wf-recorder is stopped with SIGINT so it finalises the file. @@ -36,6 +38,16 @@ let ${wf-recorder} -g "$region" -f "$file" ${notify-send} -a "Screen recording" "Recording saved" "$file" ''; + + # Lit while a wf-recorder capture is running. + # Empty text collapses the widget when idle, so it shows nothing until then. + recordingStatus = pkgs.writeShellScript "waybar-recording-status" '' + if ${pgrep} -x wf-recorder >/dev/null; then + printf '{"text":"${glyph "f03d"}","tooltip":"Recording","class":"recording"}\n' + else + printf '{"text":"","class":"idle"}\n' + fi + ''; in { options.modules.desktop.utilities.recording.enable = @@ -50,5 +62,13 @@ in }; }; + registry.waybar-module.${config.networking.hostName}."custom/recording" = { + order = 10; + settings = { + return-type = "json"; + exec = "${recordingStatus}"; + interval = 1; + }; + }; }; } diff --git a/configs/nixos/modules/network/vpn-client.nix b/configs/nixos/modules/network/vpn-client.nix index 76bc080..3c4f964 100644 --- a/configs/nixos/modules/network/vpn-client.nix +++ b/configs/nixos/modules/network/vpn-client.nix @@ -1,10 +1,30 @@ { config, lib, + pkgs, ... }: let cfg = config.modules.network.vpn-client; # NOLINT(let_order) + + vpnIcon = builtins.fromJSON ''"\udb83\udc9d"''; + + vpnStatus = pkgs.writeShellScript "waybar-vpn-status" '' + interface=${lib.escapeShellArg cfg.interface} + + if ! ${lib.getExe' pkgs.iproute2 "ip"} link show dev "$interface" >/dev/null 2>&1; then + printf '{"text":"","class":"idle"}\n' + exit 0 + fi + + ipaddr=$(${lib.getExe' pkgs.iproute2 "ip"} -4 -o addr show dev "$interface" | ${lib.getExe' pkgs.gawk "awk"} '{ print $4; exit }') + + ${lib.getExe pkgs.jq} -cn \ + --arg text "${vpnIcon}" \ + --arg tooltip "VPN $interface: ''${ipaddr:-no IPv4}" \ + --arg class "active" \ + '{ text: $text, tooltip: $tooltip, class: $class }' + ''; in { options.modules.network.vpn-client = { @@ -57,6 +77,15 @@ in vpn-off = "sudo systemctl stop 'wireguard-${cfg.interface}*.service' wireguard-${cfg.interface}.target"; }; + registry.waybar-module.${config.networking.hostName}."custom/vpn" = { + order = 50; + settings = { + return-type = "json"; + exec = "${vpnStatus}"; + interval = 5; + }; + }; + systemd.targets."wireguard-${cfg.interface}".wantedBy = lib.mkForce [ ]; networking.wireguard.interfaces.${cfg.interface} = { diff --git a/configs/nixos/registry/waybar-module.nix b/configs/nixos/registry/waybar-module.nix new file mode 100644 index 0000000..413f63b --- /dev/null +++ b/configs/nixos/registry/waybar-module.nix @@ -0,0 +1,29 @@ +{ lib, ... }: +{ + type = lib.types.submodule { + options = { + position = lib.mkOption { + type = lib.types.enum [ + "left" + "center" + "right" + ]; + default = "right"; + description = "Selects the Waybar section that shows the module."; + }; + + order = lib.mkOption { + type = lib.types.int; + default = 100; + example = 50; + description = "Sorts the module within its Waybar section."; + }; + + settings = lib.mkOption { + type = lib.types.attrsOf lib.types.anything; + default = { }; + description = "Sets the Waybar module configuration."; + }; + }; + }; +}