Compare commits

...

5 Commits

21 changed files with 348 additions and 282 deletions

View File

@@ -2,10 +2,10 @@
## Desktop ## Desktop
- TODO: Verify this path after the NixOS configuration is migrated. - When adding a graphical application, register its workspace icon in `registry.window-icon`.
When adding a graphical application, add a `window-rewrite` icon mapping in `modules/desktop/session/waybar.nix`.
The workspace indicator uses this mapping to show an application icon instead of the generic default glyph. The workspace indicator uses this mapping to show an application icon instead of the generic default glyph.
Match the mapping on the window class. 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. 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. - 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`. Set Hyprland session environment variables in `modules/desktop/session/hyprland/default.nix` with `env = KEY,VALUE`.

View File

@@ -1,5 +1,6 @@
{ {
inputs, inputs,
util,
... ...
}: }:
# neogaia — Dell XPS 13 9380 laptop. # neogaia — Dell XPS 13 9380 laptop.
@@ -14,8 +15,7 @@
modules.host.cachyos.enable = true; modules.host.cachyos.enable = true;
# So wifi can be joined from the console. modules.network.enable = true;
networking.networkmanager.enable = true;
registry.address.neogaia.self = "10.23.50.146"; registry.address.neogaia.self = "10.23.50.146";
@@ -40,4 +40,23 @@
modules.desktop.apps.obsidian.enable = true; modules.desktop.apps.obsidian.enable = true;
modules.desktop.apps.steam.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;
};
};
};
} }

View File

@@ -21,7 +21,7 @@
enable32Bit = true; enable32Bit = true;
}; };
networking.networkmanager.enable = true; modules.network.enable = true;
services.logind.settings.Login = { services.logind.settings.Login = {
HandlePowerKey = "ignore"; HandlePowerKey = "ignore";

View File

@@ -3,6 +3,7 @@
inputs, inputs,
lib, lib,
pkgs, pkgs,
util,
... ...
}: }:
# Configures Firefox as the desktop browser. # Configures Firefox as the desktop browser.
@@ -28,7 +29,7 @@ in
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
registry.window-icon.${config.networking.hostName}.firefox = { registry.window-icon.${config.networking.hostName}.firefox = {
class = ".*[Ff]irefox.*"; class = ".*[Ff]irefox.*";
icon = "f269"; icon = util.glyph "f269";
}; };
home-manager.users.${config.user.name} = hm: { home-manager.users.${config.user.name} = hm: {

View File

@@ -2,6 +2,7 @@
config, config,
lib, lib,
pkgs, pkgs,
util,
... ...
}: }:
# Ghostty as the desktop terminal. # Ghostty as the desktop terminal.
@@ -22,7 +23,7 @@ in
registry.window-icon.${config.networking.hostName}.ghostty = { registry.window-icon.${config.networking.hostName}.ghostty = {
class = "com.mitchellh.ghostty"; class = "com.mitchellh.ghostty";
icon = "f120"; icon = util.glyph "f120";
}; };
home-manager.users.${user}.home.packages = [ pkgs.ghostty ]; home-manager.users.${user}.home.packages = [ pkgs.ghostty ];

View File

@@ -2,6 +2,7 @@
config, config,
lib, lib,
pkgs, pkgs,
util,
... ...
}: }:
# Obsidian as the desktop note-taking vault. # Obsidian as the desktop note-taking vault.
@@ -17,7 +18,7 @@ in
registry.window-icon.${config.networking.hostName}.obsidian = { registry.window-icon.${config.networking.hostName}.obsidian = {
class = "obsidian"; class = "obsidian";
icon = "f02d"; icon = util.glyph "f02d";
}; };
}; };
} }

View File

@@ -2,6 +2,7 @@
config, config,
lib, lib,
pkgs, pkgs,
util,
... ...
}: }:
# Signal desktop messenger. # Signal desktop messenger.
@@ -16,7 +17,7 @@ in
registry.window-icon.${config.networking.hostName}.signal = { registry.window-icon.${config.networking.hostName}.signal = {
class = "[Ss]ignal"; class = "[Ss]ignal";
icon = "f4ad"; icon = util.glyph "f4ad";
}; };
}; };
} }

View File

@@ -2,6 +2,7 @@
config, config,
lib, lib,
pkgs, pkgs,
util,
... ...
}: }:
# Steam game launcher and runtime integration. # Steam game launcher and runtime integration.
@@ -26,7 +27,7 @@
registry.window-icon.${config.networking.hostName}.steam = { registry.window-icon.${config.networking.hostName}.steam = {
class = "[Ss]team"; class = "[Ss]team";
icon = "f1b6"; icon = util.glyph "f1b6";
}; };
}; };
} }

View File

@@ -2,6 +2,7 @@
config, config,
lib, lib,
pkgs, pkgs,
util,
... ...
}: }:
# Vesktop, a Discord client with Vencord built in. # Vesktop, a Discord client with Vencord built in.
@@ -17,7 +18,7 @@ in
registry.window-icon.${config.networking.hostName}.vesktop = { registry.window-icon.${config.networking.hostName}.vesktop = {
class = "[Vv]esktop"; class = "[Vv]esktop";
icon = "f1ff"; icon = util.glyph "f1ff";
}; };
}; };
} }

View File

@@ -1,6 +1,7 @@
{ {
config, config,
lib, lib,
util,
... ...
}: }:
# The Hyprland compositor. # The Hyprland compositor.
@@ -227,5 +228,18 @@
}; };
} }
]; ];
registry.waybar.${config.networking.hostName}."hyprland/workspaces" = {
position = "left";
order = 10;
settings = {
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);
};
};
}; };
} }

View File

@@ -2,12 +2,19 @@
config, config,
lib, lib,
pkgs, pkgs,
util,
... ...
}: }:
# The lock screen: a hyprlock session-lock surface the compositor owns. # The lock screen: a hyprlock session-lock surface the compositor owns.
let let
sleepIcon = builtins.fromJSON ''"\udb81\udcb2"''; sleepIcon = util.glyph [
sleepOffIcon = builtins.fromJSON ''"\udb81\udcb3"''; "db81"
"dcb2"
];
sleepOffIcon = util.glyph [
"db81"
"dcb3"
];
idleLockToggle = pkgs.writeShellScript "lockscreen-idle-inhibit-toggle" '' idleLockToggle = pkgs.writeShellScript "lockscreen-idle-inhibit-toggle" ''
unit=lockscreen-idle-inhibit.service unit=lockscreen-idle-inhibit.service
@@ -37,6 +44,14 @@ let
${lib.getExe' pkgs.swayosd "swayosd-client"} --custom-message "${sleepOffIcon}" ${lib.getExe' pkgs.swayosd "swayosd-client"} --custom-message "${sleepOffIcon}"
fi 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 in
{ {
options.modules.desktop.session.hyprland.hyprlock.enable = 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;
};
};
}; };
} }

View File

@@ -2,9 +2,24 @@
config, config,
lib, lib,
pkgs, pkgs,
util,
... ...
}: }:
# mako as the notification daemon: toasts, a do-not-disturb mode, and history recall. # 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"; options.modules.desktop.session.mako.enable = lib.mkEnableOption "the mako notification daemon";
@@ -35,5 +50,16 @@
command = "${lib.getExe' pkgs.mako "makoctl"} restore"; 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;
};
};
}; };
} }

View File

@@ -6,96 +6,19 @@
}: }:
# Waybar: a top status bar reading system state at a glance. # Waybar: a top status bar reading system state at a glance.
let 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 moduleEntries = lib.mapAttrsToList (name: module: {
# through JSON so the glyph survives as bytes rather than an editor paste. inherit name module;
g = code: builtins.fromJSON ''"\u${code}"''; }) modules;
sleepOffIcon = builtins.fromJSON ''"\udb81\udcb3"''; modules = lib.attrByPath [ config.networking.hostName ] { } config.registry.waybar;
# 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 }'
'';
in in
{ {
options.modules.desktop.session.waybar.enable = lib.mkEnableOption "the Waybar status bar"; 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. # so the bar comes up with the session and no compositor exec-once.
systemd.enable = true; systemd.enable = true;
style = '' settings.mainBar = lib.mkMerge [
.modules-right { {
margin-right: 8px; 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 { registry.waybar.${config.networking.hostName} = {
margin-right: 8px; clock = {
} position = "center";
''; order = 10;
settings = {
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 = {
format = "{:%a %d %b %H:%M}"; format = "{:%a %d %b %H:%M}";
tooltip-format = "<tt>{calendar}</tt>"; tooltip-format = "<tt>{calendar}</tt>";
calendar = { calendar = {
@@ -164,77 +64,6 @@ in
format.today = "<b><u>{}</u></b>"; format.today = "<b><u>{}</u></b>";
}; };
}; };
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;
};
}; };
}; };
}; };

View File

@@ -2,42 +2,24 @@
config, config,
lib, lib,
pkgs, pkgs,
util,
... ...
}: }:
# PipeWire as the desktop audio server. # PipeWire as the desktop audio server.
let
mediaKey = keys: command: {
inherit keys;
locked = true;
action = {
kind = "exec";
command = "${lib.getExe pkgs.playerctl} ${command}";
};
};
in
{ {
options.modules.desktop.utilities.audio.enable = lib.mkEnableOption "the PipeWire audio server"; options.modules.desktop.utilities.audio.enable = lib.mkEnableOption "the PipeWire audio server";
config = lib.mkIf config.modules.desktop.utilities.audio.enable { config = lib.mkIf config.modules.desktop.utilities.audio.enable {
registry.keybind.${config.networking.hostName} = {
media-play = {
keys = "XF86AudioPlay";
locked = true;
action = {
kind = "exec";
command = "${pkgs.playerctl}/bin/playerctl play-pause";
};
};
media-next = {
keys = "XF86AudioNext";
locked = true;
action = {
kind = "exec";
command = "${pkgs.playerctl}/bin/playerctl next";
};
};
media-previous = {
keys = "XF86AudioPrev";
locked = true;
action = {
kind = "exec";
command = "${pkgs.playerctl}/bin/playerctl previous";
};
};
};
# Realtime scheduling for the audio threads, so playback survives load. # Realtime scheduling for the audio threads, so playback survives load.
security.rtkit.enable = true; security.rtkit.enable = true;
@@ -46,5 +28,39 @@
alsa.enable = true; alsa.enable = true;
pulse.enable = true; pulse.enable = true;
}; };
registry.keybind.${config.networking.hostName} = {
media-play = mediaKey "XF86AudioPlay" "play-pause";
media-next = mediaKey "XF86AudioNext" "next";
media-previous = mediaKey "XF86AudioPrev" "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";
};
};
};
}; };
} }

View File

@@ -6,21 +6,15 @@
}: }:
# Clipboard history: cliphist records every copy, picked back through rofi. # Clipboard history: cliphist records every copy, picked back through rofi.
let let
user = config.user.name;
cliphist = "${pkgs.cliphist}/bin/cliphist";
rofi = "${pkgs.rofi}/bin/rofi";
wl-copy = "${pkgs.wl-clipboard}/bin/wl-copy";
# The picker reuses the themed rofi, so history looks like every other menu # The picker reuses the themed rofi, so history looks like every other menu
# the launcher drives. # the launcher drives.
# decode is needed because list emits id-prefixed lines rather than the copied # decode is needed because list emits id-prefixed lines rather than the copied
# bytes, so the chosen id has to be resolved back before it can be re-copied. # bytes, so the chosen id has to be resolved back before it can be re-copied.
picker = pkgs.writeShellScript "clipboard-picker" '' picker = pkgs.writeShellScript "clipboard-picker" ''
${cliphist} list \ ${lib.getExe pkgs.cliphist} list \
| ${rofi} -dmenu -i -p Clipboard \ | ${lib.getExe pkgs.rofi} -dmenu -i -p Clipboard \
| ${cliphist} decode \ | ${lib.getExe pkgs.cliphist} decode \
| ${wl-copy} | ${lib.getExe' pkgs.wl-clipboard "wl-copy"}
''; '';
in in
{ {
@@ -28,15 +22,7 @@ in
lib.mkEnableOption "cliphist clipboard history"; lib.mkEnableOption "cliphist clipboard history";
config = lib.mkIf config.modules.desktop.utilities.clipboard.enable { config = lib.mkIf config.modules.desktop.utilities.clipboard.enable {
registry.keybind.${config.networking.hostName}.clipboard-history = { home-manager.users.${config.user.name} = {
keys = "SUPER + SHIFT + V";
action = {
kind = "exec";
command = "${picker}";
};
};
home-manager.users.${user} = {
# wl-copy and wl-paste on PATH, so the shell can pipe into and out of the # wl-copy and wl-paste on PATH, so the shell can pipe into and out of the
# clipboard. # clipboard.
# The watchers and picker above reach wl-clipboard by store path, so this # The watchers and picker above reach wl-clipboard by store path, so this
@@ -47,5 +33,13 @@ in
# session so uwsm starts and stops them with it. # session so uwsm starts and stops them with it.
services.cliphist.enable = true; services.cliphist.enable = true;
}; };
registry.keybind.${config.networking.hostName}.clipboard-history = {
keys = "SUPER + SHIFT + V";
action = {
kind = "exec";
command = "${picker}";
};
};
}; };
} }

View File

@@ -2,19 +2,11 @@
config, config,
lib, lib,
pkgs, pkgs,
util,
... ...
}: }:
# Keybound screen recording: pick a region, then toggle a video-only capture. # Keybound screen recording: pick a region, then toggle a video-only capture.
let 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 single key both starts and stops.
# A running wf-recorder is stopped with SIGINT so it finalises the file. # 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 # Otherwise slurp picks a region and wf-recorder runs in the foreground until
@@ -22,19 +14,29 @@ let
# reports it saved. # reports it saved.
# No -a means no audio is captured. # No -a means no audio is captured.
toggle = pkgs.writeShellScript "screen-record-toggle" '' toggle = pkgs.writeShellScript "screen-record-toggle" ''
if ${pgrep} -x wf-recorder >/dev/null; then if ${lib.getExe' pkgs.procps "pgrep"} -x wf-recorder >/dev/null; then
${pkill} -INT -x wf-recorder ${lib.getExe' pkgs.procps "pkill"} -INT -x wf-recorder
exit 0 exit 0
fi fi
region=$(${slurp}) || exit 0 region=$(${lib.getExe pkgs.slurp}) || exit 0
dir="$(${xdgUserDir} VIDEOS)/Recordings" dir="$(${lib.getExe' pkgs.xdg-user-dirs "xdg-user-dir"} VIDEOS)/Recordings"
${mkdir} -p "$dir" ${lib.getExe' pkgs.coreutils "mkdir"} -p "$dir"
file="$dir/recording-$(${date} +%Y%m%d-%H%M%S).mp4" 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." ${lib.getExe' pkgs.libnotify "notify-send"} -a "Screen recording" "Recording started" "Region capture, no audio."
${wf-recorder} -g "$region" -f "$file" ${lib.getExe pkgs.wf-recorder} -g "$region" -f "$file"
${notify-send} -a "Screen recording" "Recording saved" "$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 in
{ {
@@ -50,5 +52,13 @@ in
}; };
}; };
registry.waybar.${config.networking.hostName}."custom/recording" = {
order = 10;
settings = {
return-type = "json";
exec = "${recordingStatus}";
interval = 1;
};
};
}; };
} }

View File

@@ -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;
};
};
};
}

View File

@@ -1,10 +1,34 @@
{ {
config, config,
lib, lib,
pkgs,
util,
... ...
}: }:
let let
cfg = config.modules.network.vpn-client; # NOLINT(let_order) 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 in
{ {
options.modules.network.vpn-client = { options.modules.network.vpn-client = {
@@ -57,6 +81,15 @@ in
vpn-off = "sudo systemctl stop 'wireguard-${cfg.interface}*.service' wireguard-${cfg.interface}.target"; 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 [ ]; systemd.targets."wireguard-${cfg.interface}".wantedBy = lib.mkForce [ ];
networking.wireguard.interfaces.${cfg.interface} = { networking.wireguard.interfaces.${cfg.interface} = {

View File

@@ -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.";
};
};
};
}

View File

@@ -10,9 +10,9 @@
}; };
icon = lib.mkOption { 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; type = lib.types.str;
example = "f269"; example = "";
}; };
}; };
}; };

View File

@@ -25,6 +25,14 @@ in
removeNixSuffix = path: lib.removeSuffix ".nix" (builtins.baseNameOf path); removeNixSuffix = path: lib.removeSuffix ".nix" (builtins.baseNameOf path);
glyph =
code:
builtins.fromJSON (
"\""
+ (if builtins.isList code then lib.concatMapStrings (part: "\\u${part}") code else "\\u${code}")
+ "\""
);
registry.predicates.uniqueValue = uniqueValue; registry.predicates.uniqueValue = uniqueValue;
registry.predicates.uniquePerHost = registry.predicates.uniquePerHost =