Compare commits

...

7 Commits

22 changed files with 419 additions and 308 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.
@@ -26,6 +27,11 @@ in
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
registry.window-icon.${config.networking.hostName}.firefox = {
class = ".*[Ff]irefox.*";
icon = util.glyph "f269";
};
home-manager.users.${config.user.name} = hm: { home-manager.users.${config.user.name} = hm: {
programs.firefox = { programs.firefox = {
enable = true; enable = true;

View File

@@ -2,6 +2,7 @@
config, config,
lib, lib,
pkgs, pkgs,
util,
... ...
}: }:
# Ghostty as the desktop terminal. # Ghostty as the desktop terminal.
@@ -20,6 +21,11 @@ in
}; };
}; };
registry.window-icon.${config.networking.hostName}.ghostty = {
class = "com.mitchellh.ghostty";
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.
@@ -14,5 +15,10 @@ in
config = lib.mkIf config.modules.desktop.apps.obsidian.enable { config = lib.mkIf config.modules.desktop.apps.obsidian.enable {
home-manager.users.${user}.home.packages = [ pkgs.obsidian ]; home-manager.users.${user}.home.packages = [ pkgs.obsidian ];
registry.window-icon.${config.networking.hostName}.obsidian = {
class = "obsidian";
icon = util.glyph "f02d";
};
}; };
} }

View File

@@ -2,6 +2,7 @@
config, config,
lib, lib,
pkgs, pkgs,
util,
... ...
}: }:
# Signal desktop messenger. # Signal desktop messenger.
@@ -13,5 +14,10 @@ in
config = lib.mkIf config.modules.desktop.apps.signal.enable { config = lib.mkIf config.modules.desktop.apps.signal.enable {
home-manager.users.${user}.home.packages = [ pkgs.signal-desktop ]; home-manager.users.${user}.home.packages = [ pkgs.signal-desktop ];
registry.window-icon.${config.networking.hostName}.signal = {
class = "[Ss]ignal";
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.
@@ -23,5 +24,10 @@
environment.systemPackages = [ environment.systemPackages = [
pkgs.gamescope pkgs.gamescope
]; ];
registry.window-icon.${config.networking.hostName}.steam = {
class = "[Ss]team";
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.
@@ -14,5 +15,10 @@ in
config = lib.mkIf config.modules.desktop.apps.vencord.enable { config = lib.mkIf config.modules.desktop.apps.vencord.enable {
home-manager.users.${user}.home.packages = [ pkgs.vesktop ]; home-manager.users.${user}.home.packages = [ pkgs.vesktop ];
registry.window-icon.${config.networking.hostName}.vesktop = {
class = "[Vv]esktop";
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,19 +6,17 @@
}: }:
# rofi as the desktop launcher and dmenu-style menu frontend. # rofi as the desktop launcher and dmenu-style menu frontend.
let let
user = config.user.name;
# A session menu built on the same themed rofi, so the power actions look # A session menu built on the same themed rofi, so the power actions look
# like every other menu the launcher drives. # like every other menu the launcher drives.
powerMenu = pkgs.writeShellScript "rofi-power-menu" '' powerMenu = pkgs.writeShellScript "rofi-power-menu" ''
chosen=$(printf '%s\n' Lock Suspend 'Log out' Reboot 'Shut down' \ chosen=$(printf '%s\n' Lock Suspend 'Log out' Reboot 'Shut down' \
| ${pkgs.rofi}/bin/rofi -dmenu -i -p Power) | ${lib.getExe pkgs.rofi} -dmenu -i -p Power)
case "$chosen" in case "$chosen" in
Lock) ${pkgs.systemd}/bin/loginctl lock-session ;; Lock) ${lib.getExe' pkgs.systemd "loginctl"} lock-session ;;
Suspend) ${pkgs.systemd}/bin/systemctl suspend ;; Suspend) ${lib.getExe' pkgs.systemd "systemctl"} suspend ;;
'Log out') ${pkgs.systemd}/bin/loginctl terminate-session "$XDG_SESSION_ID" ;; 'Log out') ${lib.getExe' pkgs.systemd "loginctl"} terminate-session "$XDG_SESSION_ID" ;;
Reboot) ${pkgs.systemd}/bin/systemctl reboot ;; Reboot) ${lib.getExe' pkgs.systemd "systemctl"} reboot ;;
'Shut down') ${pkgs.systemd}/bin/systemctl poweroff ;; 'Shut down') ${lib.getExe' pkgs.systemd "systemctl"} poweroff ;;
esac esac
''; '';
in in
@@ -26,12 +24,34 @@ in
options.modules.desktop.session.rofi.enable = lib.mkEnableOption "the rofi launcher"; options.modules.desktop.session.rofi.enable = lib.mkEnableOption "the rofi launcher";
config = lib.mkIf config.modules.desktop.session.rofi.enable { config = lib.mkIf config.modules.desktop.session.rofi.enable {
home-manager.users.${config.user.name} = hm: {
# Stylix supplies the Nord theme.
programs.rofi = {
enable = true;
font = lib.mkForce "JetBrains Mono 12";
# Application launch is the only mode, kept minimal so the prompt opens
# as fast as rofi can read the desktop entries.
# Icons are off: resolving one per entry is the bulk of drun's startup.
extraConfig = {
modi = "drun";
show-icons = false;
};
theme.window = {
border = hm.config.lib.formats.rasi.mkLiteral "2px";
border-color = hm.config.lib.formats.rasi.mkLiteral "#FFFFFF";
border-radius = hm.config.lib.formats.rasi.mkLiteral "8px";
};
};
};
registry.keybind.${config.networking.hostName} = { registry.keybind.${config.networking.hostName} = {
rofi-launcher = { rofi-launcher = {
keys = "SUPER + R"; keys = "SUPER + R";
action = { action = {
kind = "exec"; kind = "exec";
command = "${pkgs.rofi}/bin/rofi -show drun"; command = "${lib.getExe pkgs.rofi} -show drun";
}; };
}; };
@@ -43,21 +63,5 @@ in
}; };
}; };
}; };
home-manager.users.${user} = {
# No theme is set here: Stylix's rofi target supplies the Nord one.
programs.rofi = {
enable = true;
# Application launch is the only mode, kept minimal so the prompt opens
# as fast as rofi can read the desktop entries.
# Icons are off: resolving one per entry is the bulk of drun's startup.
extraConfig = {
modi = "drun";
show-icons = false;
};
};
};
}; };
} }

View File

@@ -6,97 +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;
user = config.user.name;
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 ${pkgs.mako}/bin/makoctl mode 2>/dev/null | ${pkgs.gnugrep}/bin/grep -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" ''
${pkgs.mako}/bin/makoctl mode -t dnd >/dev/null 2>&1
${pkgs.procps}/bin/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 ${pkgs.procps}/bin/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 ${pkgs.systemd}/bin/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 ! ${pkgs.iproute2}/bin/ip link show dev "$interface" >/dev/null 2>&1; then
printf '{"text":"","class":"idle"}\n'
exit 0
fi
ipaddr=$(${pkgs.iproute2}/bin/ip -4 -o addr show dev "$interface" | ${pkgs.gawk}/bin/awk '{ print $4; exit }')
${pkgs.jq}/bin/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=$(
${pkgs.networkmanager}/bin/nmcli -t -f DEVICE,TYPE,STATE,CONNECTION device status \
| ${pkgs.gawk}/bin/awk -F: '$3 == "connected" && $2 != "loopback" { print; exit }'
)
if [ -z "$active" ]; then
${pkgs.jq}/bin/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=$(${pkgs.iproute2}/bin/ip -4 -o addr show dev "$device" | ${pkgs.gawk}/bin/awk '{ print $4; exit }')
${pkgs.jq}/bin/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";
@@ -106,64 +28,34 @@ in
# codepoints Stylix's monospace font does not carry. # codepoints Stylix's monospace font does not carry.
fonts.packages = [ pkgs.nerd-fonts.symbols-only ]; fonts.packages = [ pkgs.nerd-fonts.symbols-only ];
home-manager.users.${user}.programs.waybar = { home-manager.users.${config.user.name}.programs.waybar = {
enable = true; enable = true;
# A user service bound to graphical-session.target, which uwsm activates, # A user service bound to graphical-session.target, which uwsm activates,
# 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 = {
"class<com.mitchellh.ghostty>" = g "f120";
"class<.*[Ff]irefox.*>" = g "f269";
"class<chromium.*>" = g "f268";
"class<[Cc]ode>" = g "f121";
"class<obsidian>" = g "f02d";
"class<[Ss]ignal>" = g "f4ad";
"class<[Ss]team>" = g "f1b6";
"class<[Vv]esktop>" = g "f1ff";
};
};
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 = {
@@ -172,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 = "${pkgs.wireplumber}/bin/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

@@ -0,0 +1,19 @@
{ lib, ... }:
{
type = lib.types.submodule {
options = {
class = lib.mkOption {
description = "Matches the window class reported by the compositor.";
type = lib.types.str;
example = ".*[Ff]irefox.*";
};
icon = lib.mkOption {
description = "Sets the icon shown for matching windows.";
type = lib.types.str;
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 =