Compare commits
11 Commits
f059068873
...
fix/waybar
| Author | SHA1 | Date | |
|---|---|---|---|
| c053846adb | |||
| 2edad6c5df | |||
| 1d5acc09c6 | |||
| ee29e53f7c | |||
| 3c9d0ec886 | |||
| f0c5544b10 | |||
| 4dd0a5a4f5 | |||
| a4a795f7ee | |||
| ecdf877308 | |||
| d1face10ec | |||
| 337d9a2014 |
@@ -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`.
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
enable32Bit = true;
|
||||
};
|
||||
|
||||
networking.networkmanager.enable = true;
|
||||
modules.network.enable = true;
|
||||
|
||||
services.logind.settings.Login = {
|
||||
HandlePowerKey = "ignore";
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
inputs,
|
||||
lib,
|
||||
pkgs,
|
||||
util,
|
||||
...
|
||||
}:
|
||||
# Configures Firefox as the desktop browser.
|
||||
@@ -26,6 +27,11 @@ in
|
||||
};
|
||||
|
||||
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: {
|
||||
programs.firefox = {
|
||||
enable = true;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
util,
|
||||
...
|
||||
}:
|
||||
# Ghostty as the desktop terminal.
|
||||
@@ -16,10 +17,15 @@ in
|
||||
keys = "SUPER + Return";
|
||||
action = {
|
||||
kind = "exec";
|
||||
command = "${pkgs.ghostty}/bin/ghostty";
|
||||
command = lib.getExe pkgs.ghostty;
|
||||
};
|
||||
};
|
||||
|
||||
registry.window-icon.${config.networking.hostName}.ghostty = {
|
||||
class = "com.mitchellh.ghostty";
|
||||
icon = util.glyph "f120";
|
||||
};
|
||||
|
||||
home-manager.users.${user}.home.packages = [ pkgs.ghostty ];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
util,
|
||||
...
|
||||
}:
|
||||
# Obsidian as the desktop note-taking vault.
|
||||
@@ -14,5 +15,10 @@ in
|
||||
|
||||
config = lib.mkIf config.modules.desktop.apps.obsidian.enable {
|
||||
home-manager.users.${user}.home.packages = [ pkgs.obsidian ];
|
||||
|
||||
registry.window-icon.${config.networking.hostName}.obsidian = {
|
||||
class = "obsidian";
|
||||
icon = util.glyph "f02d";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
util,
|
||||
...
|
||||
}:
|
||||
# Signal desktop messenger.
|
||||
@@ -13,5 +14,10 @@ in
|
||||
|
||||
config = lib.mkIf config.modules.desktop.apps.signal.enable {
|
||||
home-manager.users.${user}.home.packages = [ pkgs.signal-desktop ];
|
||||
|
||||
registry.window-icon.${config.networking.hostName}.signal = {
|
||||
class = "[Ss]ignal";
|
||||
icon = util.glyph "f4ad";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
util,
|
||||
...
|
||||
}:
|
||||
# Steam game launcher and runtime integration.
|
||||
@@ -23,5 +24,10 @@
|
||||
environment.systemPackages = [
|
||||
pkgs.gamescope
|
||||
];
|
||||
|
||||
registry.window-icon.${config.networking.hostName}.steam = {
|
||||
class = "[Ss]team";
|
||||
icon = util.glyph "f1b6";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
util,
|
||||
...
|
||||
}:
|
||||
# Vesktop, a Discord client with Vencord built in.
|
||||
@@ -14,5 +15,10 @@ in
|
||||
|
||||
config = lib.mkIf config.modules.desktop.apps.vencord.enable {
|
||||
home-manager.users.${user}.home.packages = [ pkgs.vesktop ];
|
||||
|
||||
registry.window-icon.${config.networking.hostName}.vesktop = {
|
||||
class = "[Vv]esktop";
|
||||
icon = util.glyph "f1ff";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
util,
|
||||
...
|
||||
}:
|
||||
# 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);
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -21,10 +21,10 @@
|
||||
general = {
|
||||
# The guard drops the call when a locker is already up, so no idle
|
||||
# trigger stacks a second hyprlock over the first.
|
||||
lock_cmd = "${pkgs.procps}/bin/pidof hyprlock || ${pkgs.hyprlock}/bin/hyprlock";
|
||||
lock_cmd = "${lib.getExe' pkgs.procps "pidof"} hyprlock || ${lib.getExe pkgs.hyprlock}";
|
||||
before_sleep_cmd = "loginctl lock-session";
|
||||
# Waking pokes the compositor in case firmware leaves displays dark.
|
||||
after_sleep_cmd = "${config.programs.hyprland.package}/bin/hyprctl dispatch 'hl.dsp.dpms({ action = \"enable\" })'";
|
||||
after_sleep_cmd = "${lib.getExe' config.programs.hyprland.package "hyprctl"} dispatch 'hl.dsp.dpms({ action = \"enable\" })'";
|
||||
};
|
||||
|
||||
listener = [
|
||||
|
||||
@@ -2,39 +2,54 @@
|
||||
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
|
||||
|
||||
refresh_waybar() {
|
||||
${pkgs.procps}/bin/pkill -RTMIN+9 waybar >/dev/null 2>&1 || true
|
||||
${lib.getExe' pkgs.procps "pkill"} -RTMIN+9 waybar >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
if ${pkgs.systemd}/bin/systemctl --user is-active --quiet "$unit"; then
|
||||
${pkgs.systemd}/bin/systemctl --user stop "$unit"
|
||||
if ${lib.getExe' pkgs.systemd "systemctl"} --user is-active --quiet "$unit"; then
|
||||
${lib.getExe' pkgs.systemd "systemctl"} --user stop "$unit"
|
||||
refresh_waybar
|
||||
${pkgs.swayosd}/bin/swayosd-client --custom-message "${sleepIcon}"
|
||||
${lib.getExe' pkgs.swayosd "swayosd-client"} --custom-message "${sleepIcon}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ${pkgs.systemd}/bin/systemd-run \
|
||||
if ${lib.getExe' pkgs.systemd "systemd-run"} \
|
||||
--user \
|
||||
--collect \
|
||||
--unit=lockscreen-idle-inhibit \
|
||||
--description="Disable idle-triggered screen locking" \
|
||||
${pkgs.systemd}/bin/systemd-inhibit \
|
||||
${lib.getExe' pkgs.systemd "systemd-inhibit"} \
|
||||
--what=idle \
|
||||
--who=lockscreen-hotkey \
|
||||
--why="lock screen disabled by hotkey" \
|
||||
${pkgs.coreutils}/bin/sleep infinity >/dev/null; then
|
||||
${lib.getExe' pkgs.coreutils "sleep"} infinity >/dev/null; then
|
||||
refresh_waybar
|
||||
${pkgs.swayosd}/bin/swayosd-client --custom-message "${sleepOffIcon}"
|
||||
${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
|
||||
@@ -85,7 +100,7 @@ in
|
||||
kind = "exec";
|
||||
# The guard drops the call when a locker is already up, so no manual
|
||||
# trigger stacks a second hyprlock over the first.
|
||||
command = "${pkgs.procps}/bin/pidof hyprlock || ${pkgs.hyprlock}/bin/hyprlock";
|
||||
command = "${lib.getExe' pkgs.procps "pidof"} hyprlock || ${lib.getExe pkgs.hyprlock}";
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,9 +13,13 @@
|
||||
config = lib.mkIf config.modules.desktop.session.login.enable {
|
||||
services.greetd = {
|
||||
enable = true;
|
||||
settings.default_session.command =
|
||||
"${lib.getExe pkgs.tuigreet} --time --remember "
|
||||
+ "--cmd 'uwsm start -e -D Hyprland hyprland.desktop'";
|
||||
settings.default_session.command = lib.escapeShellArgs [
|
||||
(lib.getExe pkgs.tuigreet)
|
||||
"--time"
|
||||
"--remember"
|
||||
"--cmd"
|
||||
"${lib.getExe config.programs.uwsm.package} start -e -D Hyprland hyprland.desktop"
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,27 +2,29 @@
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
util,
|
||||
...
|
||||
}:
|
||||
# mako as the notification daemon: toasts, a do-not-disturb mode, and history recall.
|
||||
let
|
||||
user = config.user.name;
|
||||
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
|
||||
'';
|
||||
|
||||
makoctl = "${pkgs.mako}/bin/makoctl";
|
||||
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";
|
||||
|
||||
config = lib.mkIf config.modules.desktop.session.mako.enable {
|
||||
registry.keybind.${config.networking.hostName}.mako-history = {
|
||||
keys = "SUPER + N";
|
||||
action = {
|
||||
kind = "exec";
|
||||
command = "${makoctl} restore";
|
||||
};
|
||||
};
|
||||
|
||||
home-manager.users.${user} = {
|
||||
home-manager.users.${config.user.name} = {
|
||||
# Colors and the popup font come from Stylix's mako target, so only
|
||||
# behaviour is set here.
|
||||
services.mako = {
|
||||
@@ -39,7 +41,25 @@ in
|
||||
"mode=dnd".invisible = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
registry.keybind.${config.networking.hostName}.mako-history = {
|
||||
keys = "SUPER + N";
|
||||
action = {
|
||||
kind = "exec";
|
||||
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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,19 +6,17 @@
|
||||
}:
|
||||
# rofi as the desktop launcher and dmenu-style menu frontend.
|
||||
let
|
||||
user = config.user.name;
|
||||
|
||||
# A session menu built on the same themed rofi, so the power actions look
|
||||
# like every other menu the launcher drives.
|
||||
powerMenu = pkgs.writeShellScript "rofi-power-menu" ''
|
||||
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
|
||||
Lock) ${pkgs.systemd}/bin/loginctl lock-session ;;
|
||||
Suspend) ${pkgs.systemd}/bin/systemctl suspend ;;
|
||||
'Log out') ${pkgs.systemd}/bin/loginctl terminate-session "$XDG_SESSION_ID" ;;
|
||||
Reboot) ${pkgs.systemd}/bin/systemctl reboot ;;
|
||||
'Shut down') ${pkgs.systemd}/bin/systemctl poweroff ;;
|
||||
Lock) ${lib.getExe' pkgs.systemd "loginctl"} lock-session ;;
|
||||
Suspend) ${lib.getExe' pkgs.systemd "systemctl"} suspend ;;
|
||||
'Log out') ${lib.getExe' pkgs.systemd "loginctl"} terminate-session "$XDG_SESSION_ID" ;;
|
||||
Reboot) ${lib.getExe' pkgs.systemd "systemctl"} reboot ;;
|
||||
'Shut down') ${lib.getExe' pkgs.systemd "systemctl"} poweroff ;;
|
||||
esac
|
||||
'';
|
||||
in
|
||||
@@ -26,12 +24,34 @@ in
|
||||
options.modules.desktop.session.rofi.enable = lib.mkEnableOption "the rofi launcher";
|
||||
|
||||
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} = {
|
||||
rofi-launcher = {
|
||||
keys = "SUPER + R";
|
||||
action = {
|
||||
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;
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,97 +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}"'';
|
||||
user = config.user.name;
|
||||
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 ${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 }'
|
||||
'';
|
||||
modules = lib.attrByPath [ config.networking.hostName ] { } config.registry.waybar;
|
||||
in
|
||||
{
|
||||
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.
|
||||
fonts.packages = [ pkgs.nerd-fonts.symbols-only ];
|
||||
|
||||
home-manager.users.${user}.programs.waybar = {
|
||||
home-manager.users.${config.user.name}.programs.waybar = {
|
||||
enable = true;
|
||||
|
||||
# A user service bound to graphical-session.target, which uwsm activates,
|
||||
# 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 = {
|
||||
"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 = {
|
||||
registry.waybar.${config.networking.hostName} = {
|
||||
clock = {
|
||||
position = "center";
|
||||
order = 10;
|
||||
settings = {
|
||||
format = "{:%a %d %b %H:%M}";
|
||||
tooltip-format = "<tt>{calendar}</tt>";
|
||||
calendar = {
|
||||
@@ -172,77 +64,6 @@ in
|
||||
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;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,42 +2,24 @@
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
util,
|
||||
...
|
||||
}:
|
||||
# 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";
|
||||
|
||||
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.
|
||||
security.rtkit.enable = true;
|
||||
|
||||
@@ -46,5 +28,39 @@
|
||||
alsa.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";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,21 +6,15 @@
|
||||
}:
|
||||
# Clipboard history: cliphist records every copy, picked back through rofi.
|
||||
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 launcher drives.
|
||||
# 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.
|
||||
picker = pkgs.writeShellScript "clipboard-picker" ''
|
||||
${cliphist} list \
|
||||
| ${rofi} -dmenu -i -p Clipboard \
|
||||
| ${cliphist} decode \
|
||||
| ${wl-copy}
|
||||
${lib.getExe pkgs.cliphist} list \
|
||||
| ${lib.getExe pkgs.rofi} -dmenu -i -p Clipboard \
|
||||
| ${lib.getExe pkgs.cliphist} decode \
|
||||
| ${lib.getExe' pkgs.wl-clipboard "wl-copy"}
|
||||
'';
|
||||
in
|
||||
{
|
||||
@@ -28,15 +22,7 @@ in
|
||||
lib.mkEnableOption "cliphist clipboard history";
|
||||
|
||||
config = lib.mkIf config.modules.desktop.utilities.clipboard.enable {
|
||||
registry.keybind.${config.networking.hostName}.clipboard-history = {
|
||||
keys = "SUPER + SHIFT + V";
|
||||
action = {
|
||||
kind = "exec";
|
||||
command = "${picker}";
|
||||
};
|
||||
};
|
||||
|
||||
home-manager.users.${user} = {
|
||||
home-manager.users.${config.user.name} = {
|
||||
# wl-copy and wl-paste on PATH, so the shell can pipe into and out of the
|
||||
# clipboard.
|
||||
# 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.
|
||||
services.cliphist.enable = true;
|
||||
};
|
||||
|
||||
registry.keybind.${config.networking.hostName}.clipboard-history = {
|
||||
keys = "SUPER + SHIFT + V";
|
||||
action = {
|
||||
kind = "exec";
|
||||
command = "${picker}";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
57
configs/nixos/modules/network/default.nix
Normal file
57
configs/nixos/modules/network/default.nix
Normal 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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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} = {
|
||||
|
||||
29
configs/nixos/registry/waybar.nix
Normal file
29
configs/nixos/registry/waybar.nix
Normal 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.";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
19
configs/nixos/registry/window-icon.nix
Normal file
19
configs/nixos/registry/window-icon.nix
Normal 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 = "";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -25,6 +25,14 @@ in
|
||||
|
||||
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.uniquePerHost =
|
||||
|
||||
@@ -21,3 +21,32 @@ Do not call `lib.removeSuffix ".nix"` outside the utility definition.
|
||||
### Rationale
|
||||
|
||||
The utility centralizes Nix file-name normalization.
|
||||
|
||||
## Use executable helpers for package commands
|
||||
|
||||
### Rule
|
||||
|
||||
Use `lib.getExe` for a package's main executable.
|
||||
Use `lib.getExe'` for a package's non-main executable.
|
||||
Do not build executable paths with `"${pkg}/bin/name"` when an executable helper names the target.
|
||||
Use `lib.getExe' config.programs.<name>.package "tool"` when a command follows a configured package option.
|
||||
|
||||
### Rationale
|
||||
|
||||
Executable helpers select the package output that contains binaries.
|
||||
They make package ownership explicit.
|
||||
`lib.getExe'` prevents calls to a package's main executable when code needs another tool.
|
||||
|
||||
## Prefer NixOS package ownership for commands
|
||||
|
||||
### Rule
|
||||
|
||||
Use `pkgs.<name>` for Home Manager tools.
|
||||
Use `config.programs.<name>.package` when a NixOS module owns the package selection.
|
||||
Do not read `config.home-manager.users.<user>.programs.<name>.package` for command paths.
|
||||
|
||||
### Rationale
|
||||
|
||||
Home Manager uses the NixOS package set in this repository.
|
||||
`pkgs.<name>` avoids dependency paths through the user tree under Home Manager.
|
||||
Configured NixOS package options preserve package selection when a module owns the tool.
|
||||
|
||||
Reference in New Issue
Block a user