diff --git a/configs/nixos/AGENTS.md b/configs/nixos/AGENTS.md
index 2f538cf..867195c 100644
--- a/configs/nixos/AGENTS.md
+++ b/configs/nixos/AGENTS.md
@@ -2,10 +2,10 @@
## Desktop
-- TODO: Verify this path after the NixOS configuration is migrated.
- When adding a graphical application, add a `window-rewrite` icon mapping in `modules/desktop/session/waybar.nix`.
+- When adding a graphical application, register its workspace icon in `registry.window-icon`.
The workspace indicator uses this mapping to show an application icon instead of the generic default glyph.
Match the mapping on the window class.
+ Store the icon as the rendered string.
Run `hyprctl clients -j | jq -r '.[].class' | sort -u` to list classes in the running session.
- TODO: Verify this path after the NixOS configuration is migrated.
Set Hyprland session environment variables in `modules/desktop/session/hyprland/default.nix` with `env = KEY,VALUE`.
diff --git a/configs/nixos/hosts/neogaia/default.nix b/configs/nixos/hosts/neogaia/default.nix
index 45e7d36..d96111c 100644
--- a/configs/nixos/hosts/neogaia/default.nix
+++ b/configs/nixos/hosts/neogaia/default.nix
@@ -1,5 +1,6 @@
{
inputs,
+ util,
...
}:
# neogaia — Dell XPS 13 9380 laptop.
@@ -14,8 +15,7 @@
modules.host.cachyos.enable = true;
- # So wifi can be joined from the console.
- networking.networkmanager.enable = true;
+ modules.network.enable = true;
registry.address.neogaia.self = "10.23.50.146";
@@ -40,4 +40,23 @@
modules.desktop.apps.obsidian.enable = true;
modules.desktop.apps.steam.enable = true;
+ registry.waybar.neogaia.battery = {
+ order = 70;
+ settings = {
+ format = "{icon} {capacity}%";
+ format-charging = "${util.glyph "f0e7"} {capacity}%";
+ format-icons = [
+ (util.glyph "f244")
+ (util.glyph "f243")
+ (util.glyph "f242")
+ (util.glyph "f241")
+ (util.glyph "f240")
+ ];
+ states = {
+ warning = 20;
+ critical = 10;
+ };
+ };
+ };
+
}
diff --git a/configs/nixos/hosts/zeus/default.nix b/configs/nixos/hosts/zeus/default.nix
index f30db78..14f8b19 100644
--- a/configs/nixos/hosts/zeus/default.nix
+++ b/configs/nixos/hosts/zeus/default.nix
@@ -21,7 +21,7 @@
enable32Bit = true;
};
- networking.networkmanager.enable = true;
+ modules.network.enable = true;
services.logind.settings.Login = {
HandlePowerKey = "ignore";
diff --git a/configs/nixos/modules/desktop/apps/firefox.nix b/configs/nixos/modules/desktop/apps/firefox.nix
index 5021d13..65140b7 100644
--- a/configs/nixos/modules/desktop/apps/firefox.nix
+++ b/configs/nixos/modules/desktop/apps/firefox.nix
@@ -3,6 +3,7 @@
inputs,
lib,
pkgs,
+ util,
...
}:
# Configures Firefox as the desktop browser.
@@ -28,7 +29,7 @@ in
config = lib.mkIf cfg.enable {
registry.window-icon.${config.networking.hostName}.firefox = {
class = ".*[Ff]irefox.*";
- icon = "f269";
+ icon = util.glyph "f269";
};
home-manager.users.${config.user.name} = hm: {
diff --git a/configs/nixos/modules/desktop/apps/ghostty.nix b/configs/nixos/modules/desktop/apps/ghostty.nix
index 05d2ba6..d8e1e73 100644
--- a/configs/nixos/modules/desktop/apps/ghostty.nix
+++ b/configs/nixos/modules/desktop/apps/ghostty.nix
@@ -2,6 +2,7 @@
config,
lib,
pkgs,
+ util,
...
}:
# Ghostty as the desktop terminal.
@@ -22,7 +23,7 @@ in
registry.window-icon.${config.networking.hostName}.ghostty = {
class = "com.mitchellh.ghostty";
- icon = "f120";
+ icon = util.glyph "f120";
};
home-manager.users.${user}.home.packages = [ pkgs.ghostty ];
diff --git a/configs/nixos/modules/desktop/apps/obsidian.nix b/configs/nixos/modules/desktop/apps/obsidian.nix
index fb018cd..49648aa 100644
--- a/configs/nixos/modules/desktop/apps/obsidian.nix
+++ b/configs/nixos/modules/desktop/apps/obsidian.nix
@@ -2,6 +2,7 @@
config,
lib,
pkgs,
+ util,
...
}:
# Obsidian as the desktop note-taking vault.
@@ -17,7 +18,7 @@ in
registry.window-icon.${config.networking.hostName}.obsidian = {
class = "obsidian";
- icon = "f02d";
+ icon = util.glyph "f02d";
};
};
}
diff --git a/configs/nixos/modules/desktop/apps/signal.nix b/configs/nixos/modules/desktop/apps/signal.nix
index a28387f..534d0f0 100644
--- a/configs/nixos/modules/desktop/apps/signal.nix
+++ b/configs/nixos/modules/desktop/apps/signal.nix
@@ -2,6 +2,7 @@
config,
lib,
pkgs,
+ util,
...
}:
# Signal desktop messenger.
@@ -16,7 +17,7 @@ in
registry.window-icon.${config.networking.hostName}.signal = {
class = "[Ss]ignal";
- icon = "f4ad";
+ icon = util.glyph "f4ad";
};
};
}
diff --git a/configs/nixos/modules/desktop/apps/steam.nix b/configs/nixos/modules/desktop/apps/steam.nix
index 6b111ed..eb252ce 100644
--- a/configs/nixos/modules/desktop/apps/steam.nix
+++ b/configs/nixos/modules/desktop/apps/steam.nix
@@ -2,6 +2,7 @@
config,
lib,
pkgs,
+ util,
...
}:
# Steam game launcher and runtime integration.
@@ -26,7 +27,7 @@
registry.window-icon.${config.networking.hostName}.steam = {
class = "[Ss]team";
- icon = "f1b6";
+ icon = util.glyph "f1b6";
};
};
}
diff --git a/configs/nixos/modules/desktop/apps/vencord.nix b/configs/nixos/modules/desktop/apps/vencord.nix
index 79d9824..53a634c 100644
--- a/configs/nixos/modules/desktop/apps/vencord.nix
+++ b/configs/nixos/modules/desktop/apps/vencord.nix
@@ -2,6 +2,7 @@
config,
lib,
pkgs,
+ util,
...
}:
# Vesktop, a Discord client with Vencord built in.
@@ -17,7 +18,7 @@ in
registry.window-icon.${config.networking.hostName}.vesktop = {
class = "[Vv]esktop";
- icon = "f1ff";
+ icon = util.glyph "f1ff";
};
};
}
diff --git a/configs/nixos/modules/desktop/session/hyprland/default.nix b/configs/nixos/modules/desktop/session/hyprland/default.nix
index 2490799..0d39fd8 100644
--- a/configs/nixos/modules/desktop/session/hyprland/default.nix
+++ b/configs/nixos/modules/desktop/session/hyprland/default.nix
@@ -1,6 +1,7 @@
{
config,
lib,
+ util,
...
}:
# The Hyprland compositor.
@@ -227,5 +228,19 @@
};
}
];
+
+ registry.waybar.${config.networking.hostName}."hyprland/workspaces" = {
+ position = "left";
+ order = 10;
+ settings = {
+ on-click = "activate";
+ format = "{id} {windows}";
+ format-window-separator = " ";
+ window-rewrite-default = util.glyph "f2d0";
+ window-rewrite = lib.mapAttrs' (
+ _name: windowIcon: lib.nameValuePair "class<${windowIcon.class}>" windowIcon.icon
+ ) (lib.attrByPath [ config.networking.hostName ] { } config.registry.window-icon);
+ };
+ };
};
}
diff --git a/configs/nixos/modules/desktop/session/hyprland/hyprlock.nix b/configs/nixos/modules/desktop/session/hyprland/hyprlock.nix
index 0fde487..ae8fbcb 100644
--- a/configs/nixos/modules/desktop/session/hyprland/hyprlock.nix
+++ b/configs/nixos/modules/desktop/session/hyprland/hyprlock.nix
@@ -2,12 +2,19 @@
config,
lib,
pkgs,
+ util,
...
}:
# The lock screen: a hyprlock session-lock surface the compositor owns.
let
- sleepIcon = builtins.fromJSON ''"\udb81\udcb2"'';
- sleepOffIcon = builtins.fromJSON ''"\udb81\udcb3"'';
+ sleepIcon = util.glyph [
+ "db81"
+ "dcb2"
+ ];
+ sleepOffIcon = util.glyph [
+ "db81"
+ "dcb3"
+ ];
idleLockToggle = pkgs.writeShellScript "lockscreen-idle-inhibit-toggle" ''
unit=lockscreen-idle-inhibit.service
@@ -37,6 +44,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 +112,15 @@ in
};
};
};
+
+ registry.waybar.${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..d237b56 100644
--- a/configs/nixos/modules/desktop/session/mako.nix
+++ b/configs/nixos/modules/desktop/session/mako.nix
@@ -2,9 +2,24 @@
config,
lib,
pkgs,
+ util,
...
}:
# mako as the notification daemon: toasts, a do-not-disturb mode, and history recall.
+let
+ 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":"${util.glyph "f1f6"}","tooltip":"Do not disturb: on","class":"dnd"}\n'
+ else
+ printf '{"text":"${util.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 +50,16 @@
command = "${lib.getExe' pkgs.mako "makoctl"} restore";
};
};
+
+ registry.waybar.${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..0e57a11 100644
--- a/configs/nixos/modules/desktop/session/waybar.nix
+++ b/configs/nixos/modules/desktop/session/waybar.nix
@@ -6,96 +6,19 @@
}:
# Waybar: a top status bar reading system state at a glance.
let
- vpnClient = config.modules.network.vpn-client;
+ moduleNames =
+ position:
+ map (entry: entry.name) (
+ lib.sort (left: right: left.module.order < right.module.order) (
+ lib.filter (entry: entry.module.position == position) moduleEntries
+ )
+ );
- # 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}"'';
+ moduleEntries = lib.mapAttrsToList (name: module: {
+ inherit name module;
+ }) modules;
- 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 }'
- '';
-
- networkStatus = pkgs.writeShellScript "waybar-network-status" ''
- active=$(
- ${lib.getExe' pkgs.networkmanager "nmcli"} -t -f DEVICE,TYPE,STATE,CONNECTION device status \
- | ${lib.getExe' pkgs.gawk "awk"} -F: '$3 == "connected" && $2 != "loopback" { print; exit }'
- )
-
- if [ -z "$active" ]; then
- ${lib.getExe pkgs.jq} -cn \
- --arg text "${g "f127"} off" \
- --arg tooltip "Network disconnected" \
- --arg class "disconnected" \
- '{ text: $text, tooltip: $tooltip, class: $class }'
- exit 0
- fi
-
- IFS=: read -r device type _state connection <<< "$active"
-
- case "$type" in
- wifi) icon="${g "f1eb"}" ;;
- ethernet) icon="LAN" ;;
- *) icon="NET" ;;
- esac
-
- ipaddr=$(${lib.getExe' pkgs.iproute2 "ip"} -4 -o addr show dev "$device" | ${lib.getExe' pkgs.gawk "awk"} '{ print $4; exit }')
-
- ${lib.getExe pkgs.jq} -cn \
- --arg text "$icon $connection" \
- --arg tooltip "$device: ''${ipaddr:-no IPv4}" \
- --arg class "$type" \
- '{ text: $text, tooltip: $tooltip, class: $class }'
- '';
+ modules = lib.attrByPath [ config.networking.hostName ] { } config.registry.waybar;
in
{
options.modules.desktop.session.waybar.enable = lib.mkEnableOption "the Waybar status bar";
@@ -112,50 +35,27 @@ in
# so the bar comes up with the session and no compositor exec-once.
systemd.enable = true;
- style = ''
- .modules-right {
- margin-right: 8px;
+ settings.mainBar = lib.mkMerge [
+ {
+ layer = "top";
+ position = "top";
+ height = 30;
+ spacing = 8;
+ margin-right = 8;
+
+ modules-left = moduleNames "left";
+ modules-center = moduleNames "center";
+ modules-right = moduleNames "right";
}
+ (lib.mapAttrs (_name: module: module.settings) modules)
+ ];
+ };
- #custom-dnd {
- margin-right: 8px;
- }
- '';
-
- settings.mainBar = {
- 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"
- ];
-
- # 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 = {
+ registry.waybar.${config.networking.hostName} = {
+ clock = {
+ position = "center";
+ order = 10;
+ settings = {
format = "{:%a %d %b %H:%M}";
tooltip-format = "{calendar}";
calendar = {
@@ -164,77 +64,6 @@ in
format.today = "{}";
};
};
-
- 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;
- };
- };
-
- "custom/dnd" = {
- return-type = "json";
- exec = "${dndStatus}";
- on-click = "${dndToggle}";
- interval = "once";
- signal = 8;
- };
-
- # 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;
- };
-
- "custom/idle-lock" = {
- return-type = "json";
- exec = "${idleLockStatus}";
- interval = "once";
- signal = 9;
- };
};
};
};
diff --git a/configs/nixos/modules/desktop/utilities/audio.nix b/configs/nixos/modules/desktop/utilities/audio.nix
index bbbb140..d34ef91 100644
--- a/configs/nixos/modules/desktop/utilities/audio.nix
+++ b/configs/nixos/modules/desktop/utilities/audio.nix
@@ -2,6 +2,7 @@
config,
lib,
pkgs,
+ util,
...
}:
# PipeWire as the desktop audio server.
@@ -15,7 +16,7 @@
locked = true;
action = {
kind = "exec";
- command = "${pkgs.playerctl}/bin/playerctl play-pause";
+ command = "${lib.getExe pkgs.playerctl} play-pause";
};
};
@@ -24,7 +25,7 @@
locked = true;
action = {
kind = "exec";
- command = "${pkgs.playerctl}/bin/playerctl next";
+ command = "${lib.getExe pkgs.playerctl} next";
};
};
@@ -33,7 +34,35 @@
locked = true;
action = {
kind = "exec";
- command = "${pkgs.playerctl}/bin/playerctl previous";
+ command = "${lib.getExe pkgs.playerctl} previous";
+ };
+ };
+ };
+
+ registry.waybar.${config.networking.hostName} = {
+ mpris = {
+ order = 30;
+ settings = {
+ format = "${util.glyph "f001"} {title}";
+ format-paused = "${util.glyph "f04c"} {title}";
+ max-length = 40;
+ on-click = "play-pause";
+ on-scroll-up = "next";
+ on-scroll-down = "previous";
+ };
+ };
+
+ wireplumber = {
+ order = 40;
+ settings = {
+ format = "{icon} {volume}%";
+ format-muted = "${util.glyph "f026"} muted";
+ format-icons = [
+ (util.glyph "f026")
+ (util.glyph "f027")
+ (util.glyph "f028")
+ ];
+ on-click = "${lib.getExe' pkgs.wireplumber "wpctl"} set-mute @DEFAULT_AUDIO_SINK@ toggle";
};
};
};
diff --git a/configs/nixos/modules/desktop/utilities/recording.nix b/configs/nixos/modules/desktop/utilities/recording.nix
index 1fa9e63..11b5455 100644
--- a/configs/nixos/modules/desktop/utilities/recording.nix
+++ b/configs/nixos/modules/desktop/utilities/recording.nix
@@ -2,19 +2,11 @@
config,
lib,
pkgs,
+ util,
...
}:
# 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";
-
# A single key both starts and stops.
# A running wf-recorder is stopped with SIGINT so it finalises the file.
# Otherwise slurp picks a region and wf-recorder runs in the foreground until
@@ -22,19 +14,29 @@ let
# reports it saved.
# No -a means no audio is captured.
toggle = pkgs.writeShellScript "screen-record-toggle" ''
- if ${pgrep} -x wf-recorder >/dev/null; then
- ${pkill} -INT -x wf-recorder
+ if ${lib.getExe' pkgs.procps "pgrep"} -x wf-recorder >/dev/null; then
+ ${lib.getExe' pkgs.procps "pkill"} -INT -x wf-recorder
exit 0
fi
- region=$(${slurp}) || exit 0
- dir="$(${xdgUserDir} VIDEOS)/Recordings"
- ${mkdir} -p "$dir"
- file="$dir/recording-$(${date} +%Y%m%d-%H%M%S).mp4"
+ region=$(${lib.getExe pkgs.slurp}) || exit 0
+ dir="$(${lib.getExe' pkgs.xdg-user-dirs "xdg-user-dir"} VIDEOS)/Recordings"
+ ${lib.getExe' pkgs.coreutils "mkdir"} -p "$dir"
+ file="$dir/recording-$(${lib.getExe' pkgs.coreutils "date"} +%Y%m%d-%H%M%S).mp4"
- ${notify-send} -a "Screen recording" "Recording started" "Region capture, no audio."
- ${wf-recorder} -g "$region" -f "$file"
- ${notify-send} -a "Screen recording" "Recording saved" "$file"
+ ${lib.getExe' pkgs.libnotify "notify-send"} -a "Screen recording" "Recording started" "Region capture, no audio."
+ ${lib.getExe pkgs.wf-recorder} -g "$region" -f "$file"
+ ${lib.getExe' pkgs.libnotify "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 ${lib.getExe' pkgs.procps "pgrep"} -x wf-recorder >/dev/null; then
+ printf '{"text":"${util.glyph "f03d"}","tooltip":"Recording","class":"recording"}\n'
+ else
+ printf '{"text":"","class":"idle"}\n'
+ fi
'';
in
{
@@ -50,5 +52,13 @@ in
};
};
+ registry.waybar.${config.networking.hostName}."custom/recording" = {
+ order = 10;
+ settings = {
+ return-type = "json";
+ exec = "${recordingStatus}";
+ interval = 1;
+ };
+ };
};
}
diff --git a/configs/nixos/modules/network/default.nix b/configs/nixos/modules/network/default.nix
new file mode 100644
index 0000000..c524db1
--- /dev/null
+++ b/configs/nixos/modules/network/default.nix
@@ -0,0 +1,57 @@
+{
+ config,
+ lib,
+ pkgs,
+ util,
+ ...
+}:
+# NetworkManager-managed networking.
+let
+ networkStatus = pkgs.writeShellScript "waybar-network-status" ''
+ active=$(
+ ${lib.getExe' pkgs.networkmanager "nmcli"} -t -f DEVICE,TYPE,STATE,CONNECTION device status \
+ | ${lib.getExe' pkgs.gawk "awk"} -F: '$3 == "connected" && $2 != "loopback" { print; exit }'
+ )
+
+ if [ -z "$active" ]; then
+ ${lib.getExe pkgs.jq} -cn \
+ --arg text "${util.glyph "f127"} off" \
+ --arg tooltip "Network disconnected" \
+ --arg class "disconnected" \
+ '{ text: $text, tooltip: $tooltip, class: $class }'
+ exit 0
+ fi
+
+ IFS=: read -r device type _state connection <<< "$active"
+
+ case "$type" in
+ wifi) icon="${util.glyph "f1eb"}" ;;
+ ethernet) icon="LAN" ;;
+ *) icon="NET" ;;
+ esac
+
+ ipaddr=$(${lib.getExe' pkgs.iproute2 "ip"} -4 -o addr show dev "$device" | ${lib.getExe' pkgs.gawk "awk"} '{ print $4; exit }')
+
+ ${lib.getExe pkgs.jq} -cn \
+ --arg text "$icon $connection" \
+ --arg tooltip "$device: ''${ipaddr:-no IPv4}" \
+ --arg class "$type" \
+ '{ text: $text, tooltip: $tooltip, class: $class }'
+ '';
+in
+{
+ options.modules.network.enable = lib.mkEnableOption "NetworkManager-managed networking";
+
+ config = lib.mkIf config.modules.network.enable {
+ networking.networkmanager.enable = true;
+
+ registry.waybar.${config.networking.hostName}."custom/network" = {
+ order = 60;
+ settings = {
+ return-type = "json";
+ exec = "${networkStatus}";
+ interval = 5;
+ };
+ };
+ };
+}
diff --git a/configs/nixos/modules/network/vpn-client.nix b/configs/nixos/modules/network/vpn-client.nix
index 76bc080..624f6df 100644
--- a/configs/nixos/modules/network/vpn-client.nix
+++ b/configs/nixos/modules/network/vpn-client.nix
@@ -1,10 +1,34 @@
{
config,
lib,
+ pkgs,
+ util,
...
}:
let
cfg = config.modules.network.vpn-client; # NOLINT(let_order)
+
+ vpnIcon = util.glyph [
+ "db83"
+ "dc9d"
+ ];
+
+ 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 +81,15 @@ in
vpn-off = "sudo systemctl stop 'wireguard-${cfg.interface}*.service' wireguard-${cfg.interface}.target";
};
+ registry.waybar.${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.nix b/configs/nixos/registry/waybar.nix
new file mode 100644
index 0000000..413f63b
--- /dev/null
+++ b/configs/nixos/registry/waybar.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.";
+ };
+ };
+ };
+}
diff --git a/configs/nixos/registry/window-icon.nix b/configs/nixos/registry/window-icon.nix
index 76ffa09..574ae94 100644
--- a/configs/nixos/registry/window-icon.nix
+++ b/configs/nixos/registry/window-icon.nix
@@ -10,9 +10,9 @@
};
icon = lib.mkOption {
- description = "Sets the Nerd Font codepoint shown for matching windows.";
+ description = "Sets the icon shown for matching windows.";
type = lib.types.str;
- example = "f269";
+ example = "";
};
};
};