diff --git a/configs/nixos/AGENTS.md b/configs/nixos/AGENTS.md index 2f538cf..5bfd948 100644 --- a/configs/nixos/AGENTS.md +++ b/configs/nixos/AGENTS.md @@ -8,7 +8,7 @@ Match the mapping on the window class. 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`. + Set Hyprland session environment variables in `modules/desktop/session/hyprland/default.nix` with Lua `env` call records. `home.sessionVariables` do **not** reach the Hyprland session because UWSM does not source `hm-session-vars.sh`. Source cursor values from `config.stylix.cursor`. Set `XCURSOR_*` and `HYPRCURSOR_*` to the same theme for Bibata. @@ -65,3 +65,10 @@ and Zeus), not in the generic host module. - Module-relative paths resolve from the module file. Update them when moving a module into a subdirectory. - On Pikachu, `uptime` does not support `-s`. Use `who -b` to report the boot time. +- Changing `networking.hostId` makes ZFS reject a previously imported pool. Force-import the pool once after confirming no other host uses it. +- Remote `ssh pikachu` starts Fish. Pass `sh -c` when a command needs POSIX shell syntax. +- `deploy-to` runs on the initiating host. Deploy its Fish definition to that host before testing workflow changes. +- Home Manager stores `programs.ssh.settings` blocks as DAG entries. Read configured values from each entry's `data` attribute. +- A `NOLINT(let_order)` comment before a `let` suppresses the whole let scope. Place it on an individual binding to limit suppression. +- `util.deriveMac` derives a deterministic locally administered MAC address from a name. +- GameMode needs membership in the `gamemode` group only when enabled settings require it, such as process renicing or core parking. diff --git a/configs/nixos/hosts/zeus/default.nix b/configs/nixos/hosts/zeus/default.nix index 9e49cd8..56bdf2f 100644 --- a/configs/nixos/hosts/zeus/default.nix +++ b/configs/nixos/hosts/zeus/default.nix @@ -49,8 +49,19 @@ modules.desktop.apps.vencord.enable = true; home-manager.users.${config.user.name}.wayland.windowManager.hyprland.settings.monitor = [ - "HDMI-A-1,1920x1080@120,0x0,1,transform,3" - "DP-3,2560x1440@239.96,1080x0,1" + { + output = "HDMI-A-1"; + mode = "1920x1080@120"; + position = "0x0"; + scale = 1; + transform = 3; + } + { + output = "DP-3"; + mode = "2560x1440@239.96"; + position = "1080x0"; + scale = 1; + } ]; } diff --git a/configs/nixos/modules/desktop/session/hyprland/default.nix b/configs/nixos/modules/desktop/session/hyprland/default.nix index 7cc25db..6487cf1 100644 --- a/configs/nixos/modules/desktop/session/hyprland/default.nix +++ b/configs/nixos/modules/desktop/session/hyprland/default.nix @@ -5,16 +5,40 @@ ... }: # The Hyprland compositor. -let +let # NOLINT(let_order) cfg = config.modules.desktop.session.hyprland; # NOLINT(let_order) cursor = config.stylix.cursor; + bind = key: dispatcher: { + _args = [ + key + dispatcher + ]; + }; + luaCall = function: args: mkLuaInline "${function}(${lib.concatMapStringsSep ", " toLua args})"; + terminal = "ghostty"; + execBindWith = + key: command: options: + bindWith key (luaCall "hl.dsp.exec_cmd" [ command ]) options; + bindWith = key: dispatcher: options: { + _args = [ + key + dispatcher + options + ]; + }; # Numbered-workspace switch and move for 1..9, the operator's i3 muscle memory. workspaceBinds = lib.concatMap (n: [ - "$mod, ${toString n}, workspace, ${toString n}" - "$mod SHIFT, ${toString n}, movetoworkspace, ${toString n}" + (bind "${mod} + ${toString n}" (luaCall "hl.dsp.focus" [ { workspace = n; } ])) + (bind "${mod} + SHIFT + ${toString n}" (luaCall "hl.dsp.window.move" [ { workspace = n; } ])) ]) (lib.range 1 9); + execBind = key: command: bind key (luaCall "hl.dsp.exec_cmd" [ command ]); + + inherit (lib.generators) mkLuaInline; + + mod = "SUPER"; + toLua = lib.generators.toLua { }; user = config.user.name; # Flip the tiling strategy between the two built-in layouts, since neither a @@ -56,14 +80,16 @@ in # uwsm owns the systemd graphical-session targets. systemd.enable = false; - # Write the native hyprlang hyprland.conf, whose variable and bind syntax - # the settings below are expressed in. - configType = "hyprlang"; + # Write the native Lua Hyprland config. Home Manager renders the Nix + # settings below as hl.* calls. + configType = "lua"; + importantPrefixes = [ + "config" + "curve" + "animation" + ]; settings = { - "$mod" = "SUPER"; - "$terminal" = "ghostty"; - # Session variables handed to the compositor directly. # UWSM launches the session without the shell profile that would carry # them, so a variable the compositor or its children must see is set @@ -72,119 +98,232 @@ in # Chromium and Electron apps read this to select native Wayland; # nixpkgs wrappers (Obsidian's included) gate their Wayland flags on # it, so without it they run under XWayland and blur at this DPI. - "NIXOS_OZONE_WL,1" + { + _args = [ + "NIXOS_OZONE_WL" + "1" + ]; + } ] # Bibata ships XCursor only. # The hyprcursor variables name the same theme, which Hyprland # resolves through its XCursor fallback. ++ lib.optionals (cursor != null) [ - "XCURSOR_THEME,${cursor.name}" - "XCURSOR_SIZE,${toString cursor.size}" - "HYPRCURSOR_THEME,${cursor.name}" - "HYPRCURSOR_SIZE,${toString cursor.size}" + { + _args = [ + "XCURSOR_THEME" + cursor.name + ]; + } + { + _args = [ + "XCURSOR_SIZE" + (toString cursor.size) + ]; + } + { + _args = [ + "HYPRCURSOR_THEME" + cursor.name + ]; + } + { + _args = [ + "HYPRCURSOR_SIZE" + (toString cursor.size) + ]; + } ]; - input = { - kb_layout = "us"; - # Caps is a second Escape. - # Shift+Caps still toggles a real CapsLock. - kb_options = "caps:escape_shifted_capslock"; - # Snappy key repeat. - repeat_delay = 250; - repeat_rate = 45; - accel_profile = "flat"; - touchpad = { - natural_scroll = true; - tap-to-click = true; - disable_while_typing = true; + config = { + input = { + kb_layout = "us"; + # Caps is a second Escape. + # Shift+Caps still toggles a real CapsLock. + kb_options = "caps:escape_shifted_capslock"; + # Snappy key repeat. + repeat_delay = 250; + repeat_rate = 45; + accel_profile = "flat"; + touchpad = { + natural_scroll = true; + tap_to_click = true; + disable_while_typing = true; + }; + }; + + general = { + gaps_in = 4; + gaps_out = 8; + border_size = 2; + layout = "dwindle"; + }; + + decoration = { + rounding = 6; + blur.enabled = cfg.blur; + }; + + # XWayland clients render at the panel's native resolution instead of + # being raster-scaled by the compositor at the fractional monitor scale. + xwayland.force_zero_scaling = true; + + animations.enabled = true; + + dwindle = { + preserve_split = true; }; }; - general = { - gaps_in = 4; - gaps_out = 8; - border_size = 2; - layout = "dwindle"; - }; - - decoration = { - rounding = 6; - blur.enabled = cfg.blur; - }; - - # XWayland clients render at the panel's native resolution instead of - # being raster-scaled by the compositor at the fractional monitor scale. - xwayland.force_zero_scaling = true; - - animations = { - enabled = true; - bezier = [ "ease, 0.25, 0.1, 0.25, 1.0" ]; - # Durations are in centiseconds. - # Short values keep the motion subtle. - animation = [ - "windows, 1, 3, ease" - "fade, 1, 3, ease" - # Layer surfaces like the launcher fade in a touch quicker than windows. - "layersIn, 1, 2, ease" - "fadeLayersIn, 1, 2, ease" - "workspaces, 1, 3, ease" - "border, 1, 3, ease" + curve = { + _args = [ + "ease" + { + type = "bezier"; + points = [ + [ + 0.25 + 0.1 + ] + [ + 0.25 + 1.0 + ] + ]; + } ]; }; - dwindle = { - preserve_split = true; - }; + # Durations are in centiseconds. + # Short values keep the motion subtle. + animation = [ + { + leaf = "windows"; + enabled = true; + speed = 3; + bezier = "ease"; + } + { + leaf = "fade"; + enabled = true; + speed = 3; + bezier = "ease"; + } + # Layer surfaces like the launcher fade in a touch quicker than windows. + { + leaf = "layersIn"; + enabled = true; + speed = 2; + bezier = "ease"; + } + { + leaf = "fadeLayersIn"; + enabled = true; + speed = 2; + bezier = "ease"; + } + { + leaf = "workspaces"; + enabled = true; + speed = 3; + bezier = "ease"; + } + { + leaf = "border"; + enabled = true; + speed = 3; + bezier = "ease"; + } + ]; bind = [ - "$mod, Return, exec, $terminal" + (execBind "${mod} + Return" terminal) # Move focus. - "$mod, H, movefocus, l" - "$mod, J, movefocus, d" - "$mod, K, movefocus, u" - "$mod, L, movefocus, r" + (bind "${mod} + H" (luaCall "hl.dsp.focus" [ { direction = "left"; } ])) + (bind "${mod} + J" (luaCall "hl.dsp.focus" [ { direction = "down"; } ])) + (bind "${mod} + K" (luaCall "hl.dsp.focus" [ { direction = "up"; } ])) + (bind "${mod} + L" (luaCall "hl.dsp.focus" [ { direction = "right"; } ])) # Move the window within the layout. - "$mod SHIFT, H, movewindow, l" - "$mod SHIFT, J, movewindow, d" - "$mod SHIFT, K, movewindow, u" - "$mod SHIFT, L, movewindow, r" + (bind "${mod} + SHIFT + H" (luaCall "hl.dsp.window.move" [ { direction = "left"; } ])) + (bind "${mod} + SHIFT + J" (luaCall "hl.dsp.window.move" [ { direction = "down"; } ])) + (bind "${mod} + SHIFT + K" (luaCall "hl.dsp.window.move" [ { direction = "up"; } ])) + (bind "${mod} + SHIFT + L" (luaCall "hl.dsp.window.move" [ { direction = "right"; } ])) # Resize the active window. - "$mod ALT, H, resizeactive, -40 0" - "$mod ALT, J, resizeactive, 0 40" - "$mod ALT, K, resizeactive, 0 -40" - "$mod ALT, L, resizeactive, 40 0" + (bind "${mod} + ALT + H" ( + luaCall "hl.dsp.window.resize" [ + { + x = -40; + y = 0; + relative = true; + } + ] + )) + (bind "${mod} + ALT + J" ( + luaCall "hl.dsp.window.resize" [ + { + x = 0; + y = 40; + relative = true; + } + ] + )) + (bind "${mod} + ALT + K" ( + luaCall "hl.dsp.window.resize" [ + { + x = 0; + y = -40; + relative = true; + } + ] + )) + (bind "${mod} + ALT + L" ( + luaCall "hl.dsp.window.resize" [ + { + x = 40; + y = 0; + relative = true; + } + ] + )) - "$mod, Space, togglefloating," - "$mod, F, fullscreen," - # togglesplit is a dwindle layout message, reached through layoutmsg. - "$mod, T, layoutmsg, togglesplit" - "$mod SHIFT, T, exec, ${toggleLayout}" - "$mod SHIFT, Q, killactive," - "$mod CTRL, Q, forcekillactive," + (bind "${mod} + Space" (luaCall "hl.dsp.window.float" [ { action = "toggle"; } ])) + (bind "${mod} + F" (luaCall "hl.dsp.window.fullscreen" [ ])) + # togglesplit is a dwindle layout message, reached through layout. + (bind "${mod} + T" (luaCall "hl.dsp.layout" [ "togglesplit" ])) + (execBind "${mod} + SHIFT + T" "${toggleLayout}") + (bind "${mod} + SHIFT + Q" (luaCall "hl.dsp.window.close" [ ])) + (bind "${mod} + CTRL + Q" (luaCall "hl.dsp.window.kill" [ ])) + + # Volume and brightness keys repeat while held, each raising a popup + # through the OSD client. + # Volume is capped at 100 percent. + # The client floors brightness so a full hold cannot black the screen out. + (execBindWith "XF86AudioRaiseVolume" + "${pkgs.swayosd}/bin/swayosd-client --output-volume raise --max-volume 100" + { repeating = true; } + ) + (execBindWith "XF86AudioLowerVolume" "${pkgs.swayosd}/bin/swayosd-client --output-volume lower" { + repeating = true; + }) + (execBindWith "XF86MonBrightnessUp" "${pkgs.swayosd}/bin/swayosd-client --brightness raise" { + repeating = true; + }) + (execBindWith "XF86MonBrightnessDown" "${pkgs.swayosd}/bin/swayosd-client --brightness lower" { + repeating = true; + }) + + # Mute and media transport still fire while the session is locked. + (execBindWith "XF86AudioMute" "${pkgs.swayosd}/bin/swayosd-client --output-volume mute-toggle" { + locked = true; + }) + (execBindWith "XF86AudioPlay" "${pkgs.playerctl}/bin/playerctl play-pause" { locked = true; }) + (execBindWith "XF86AudioNext" "${pkgs.playerctl}/bin/playerctl next" { locked = true; }) + (execBindWith "XF86AudioPrev" "${pkgs.playerctl}/bin/playerctl previous" { locked = true; }) ] ++ workspaceBinds; - - # Volume and brightness keys repeat while held, each raising a popup - # through the OSD client. - # Volume is capped at 100 percent. - # The client floors brightness so a full hold cannot black the screen out. - binde = [ - ", XF86AudioRaiseVolume, exec, ${pkgs.swayosd}/bin/swayosd-client --output-volume raise --max-volume 100" - ", XF86AudioLowerVolume, exec, ${pkgs.swayosd}/bin/swayosd-client --output-volume lower" - ", XF86MonBrightnessUp, exec, ${pkgs.swayosd}/bin/swayosd-client --brightness raise" - ", XF86MonBrightnessDown, exec, ${pkgs.swayosd}/bin/swayosd-client --brightness lower" - ]; - - # Mute and media transport still fire while the session is locked. - bindl = [ - ", XF86AudioMute, exec, ${pkgs.swayosd}/bin/swayosd-client --output-volume mute-toggle" - ", XF86AudioPlay, exec, ${pkgs.playerctl}/bin/playerctl play-pause" - ", XF86AudioNext, exec, ${pkgs.playerctl}/bin/playerctl next" - ", XF86AudioPrev, exec, ${pkgs.playerctl}/bin/playerctl previous" - ]; }; }; }; diff --git a/configs/nixos/modules/desktop/session/hyprland/hyprlock.nix b/configs/nixos/modules/desktop/session/hyprland/hyprlock.nix index b83a453..de12b73 100644 --- a/configs/nixos/modules/desktop/session/hyprland/hyprlock.nix +++ b/configs/nixos/modules/desktop/session/hyprland/hyprlock.nix @@ -6,6 +6,16 @@ }: # The lock screen: a hyprlock session-lock surface the compositor owns. let + execBind = key: command: { + _args = [ + key + (luaCall "hl.dsp.exec_cmd" [ command ]) + ]; + }; + luaCall = + function: args: + lib.generators.mkLuaInline "${function}(${lib.concatMapStringsSep ", " toLua args})"; + user = config.user.name; # The hyprlock this module installs, used by the lock keybind below. @@ -15,6 +25,7 @@ let sleepOffIcon = builtins.fromJSON ''"\udb81\udcb3"''; idleLockToggle = pkgs.writeShellScript "lockscreen-idle-inhibit-toggle" '' + unit=lockscreen-idle-inhibit.service refresh_waybar() { @@ -42,6 +53,7 @@ let ${pkgs.swayosd}/bin/swayosd-client --custom-message "${sleepOffIcon}" fi ''; + toLua = lib.generators.toLua { }; in { options.modules.desktop.session.hyprland.hyprlock.enable = @@ -86,8 +98,8 @@ in # The guard drops the keypress when a locker is already up, so a second # hyprlock never stacks over the first. wayland.windowManager.hyprland.settings.bind = [ - "$mod, X, exec, ${pkgs.procps}/bin/pidof hyprlock || ${hyprlock}" - "$mod CTRL, X, exec, ${idleLockToggle}" + (execBind "SUPER + X" "${pkgs.procps}/bin/pidof hyprlock || ${hyprlock}") + (execBind "SUPER + CTRL + X" "${idleLockToggle}") ]; }; }; diff --git a/configs/nixos/modules/desktop/session/mako.nix b/configs/nixos/modules/desktop/session/mako.nix index f335717..9dc9e3f 100644 --- a/configs/nixos/modules/desktop/session/mako.nix +++ b/configs/nixos/modules/desktop/session/mako.nix @@ -6,9 +6,21 @@ }: # mako as the notification daemon: toasts, a do-not-disturb mode, and history recall. let + execBind = key: command: { + _args = [ + key + (luaCall "hl.dsp.exec_cmd" [ command ]) + ]; + }; + luaCall = + function: args: + lib.generators.mkLuaInline "${function}(${lib.concatMapStringsSep ", " toLua args})"; + user = config.user.name; makoctl = "${pkgs.mako}/bin/makoctl"; + + toLua = lib.generators.toLua { }; in { options.modules.desktop.session.mako.enable = lib.mkEnableOption "the mako notification daemon"; @@ -34,9 +46,8 @@ in # Recall the last notification from history, keyboard-only like the rest # of the session. - # $mod is defined by the compositor config these binds share. wayland.windowManager.hyprland.settings.bind = [ - "$mod, N, exec, ${makoctl} restore" + (execBind "SUPER + N" "${makoctl} restore") ]; }; }; diff --git a/configs/nixos/modules/desktop/session/rofi.nix b/configs/nixos/modules/desktop/session/rofi.nix index 261cb7d..2f38201 100644 --- a/configs/nixos/modules/desktop/session/rofi.nix +++ b/configs/nixos/modules/desktop/session/rofi.nix @@ -6,6 +6,16 @@ }: # rofi as the desktop launcher and dmenu-style menu frontend. let + execBind = key: command: { + _args = [ + key + (luaCall "hl.dsp.exec_cmd" [ command ]) + ]; + }; + luaCall = + function: args: + lib.generators.mkLuaInline "${function}(${lib.concatMapStringsSep ", " toLua args})"; + user = config.user.name; # A session menu built on the same themed rofi, so the power actions look @@ -21,6 +31,8 @@ let 'Shut down') ${pkgs.systemd}/bin/systemctl poweroff ;; esac ''; + + toLua = lib.generators.toLua { }; in { options.modules.desktop.session.rofi.enable = lib.mkEnableOption "the rofi launcher"; @@ -41,10 +53,9 @@ in }; # The launcher and the session menu, kept next to the tool they drive. - # $mod is defined by the compositor config these binds share. wayland.windowManager.hyprland.settings.bind = [ - "$mod, R, exec, ${pkgs.rofi}/bin/rofi -show drun" - "$mod SHIFT, X, exec, ${powerMenu}" + (execBind "SUPER + R" "${pkgs.rofi}/bin/rofi -show drun") + (execBind "SUPER + SHIFT + X" "${powerMenu}") ]; }; }; diff --git a/configs/nixos/modules/desktop/utilities/clipboard.nix b/configs/nixos/modules/desktop/utilities/clipboard.nix index 4579982..6a676fc 100644 --- a/configs/nixos/modules/desktop/utilities/clipboard.nix +++ b/configs/nixos/modules/desktop/utilities/clipboard.nix @@ -6,6 +6,16 @@ }: # Clipboard history: cliphist records every copy, picked back through rofi. let + execBind = key: command: { + _args = [ + key + (luaCall "hl.dsp.exec_cmd" [ command ]) + ]; + }; + luaCall = + function: args: + lib.generators.mkLuaInline "${function}(${lib.concatMapStringsSep ", " toLua args})"; + user = config.user.name; cliphist = "${pkgs.cliphist}/bin/cliphist"; @@ -22,6 +32,8 @@ let | ${cliphist} decode \ | ${wl-copy} ''; + + toLua = lib.generators.toLua { }; in { options.modules.desktop.utilities.clipboard.enable = @@ -39,9 +51,8 @@ in # session so uwsm starts and stops them with it. services.cliphist.enable = true; - # $mod is defined by the compositor config these binds share. wayland.windowManager.hyprland.settings.bind = [ - "$mod SHIFT, V, exec, ${picker}" + (execBind "SUPER + SHIFT + V" "${picker}") ]; }; }; diff --git a/configs/nixos/modules/desktop/utilities/recording.nix b/configs/nixos/modules/desktop/utilities/recording.nix index 50385d5..5f87bd4 100644 --- a/configs/nixos/modules/desktop/utilities/recording.nix +++ b/configs/nixos/modules/desktop/utilities/recording.nix @@ -6,6 +6,16 @@ }: # Keybound screen recording: pick a region, then toggle a video-only capture. let + execBind = key: command: { + _args = [ + key + (luaCall "hl.dsp.exec_cmd" [ command ]) + ]; + }; + luaCall = + function: args: + lib.generators.mkLuaInline "${function}(${lib.concatMapStringsSep ", " toLua args})"; + user = config.user.name; slurp = "${pkgs.slurp}/bin/slurp"; @@ -38,6 +48,8 @@ let ${wf-recorder} -g "$region" -f "$file" ${notify-send} -a "Screen recording" "Recording saved" "$file" ''; + + toLua = lib.generators.toLua { }; in { options.modules.desktop.utilities.recording.enable = @@ -45,9 +57,8 @@ in config = lib.mkIf config.modules.desktop.utilities.recording.enable { home-manager.users.${user} = { - # $mod is defined by the compositor config these binds share. wayland.windowManager.hyprland.settings.bind = [ - "$mod SHIFT, R, exec, ${toggle}" + (execBind "SUPER + SHIFT + R" "${toggle}") ]; }; }; diff --git a/configs/nixos/modules/desktop/utilities/screenshot.nix b/configs/nixos/modules/desktop/utilities/screenshot.nix index 1d723ef..6b16894 100644 --- a/configs/nixos/modules/desktop/utilities/screenshot.nix +++ b/configs/nixos/modules/desktop/utilities/screenshot.nix @@ -7,6 +7,16 @@ # Keyboard-driven screenshots copy to the clipboard before Satty opens for # annotation. Satty copies annotated screenshots when it saves them. let + execBind = key: command: { + _args = [ + key + (luaCall "hl.dsp.exec_cmd" [ command ]) + ]; + }; + luaCall = + function: args: + lib.generators.mkLuaInline "${function}(${lib.concatMapStringsSep ", " toLua args})"; + user = config.user.name; grimblast = "${pkgs.grimblast}/bin/grimblast"; @@ -31,6 +41,8 @@ let --early-exit \ --actions-on-enter save-to-clipboard ''; + + toLua = lib.generators.toLua { }; in { options.modules.desktop.utilities.screenshot.enable = @@ -40,19 +52,21 @@ in home-manager.users.${user} = { # satty is a one-shot annotation surface, so float it rather than letting # it claim a tile in the layout. - # windowrule v3 syntax: space-separated field/value tokens, comma - # separated, with matchers under a match: prefix and effects bare. - wayland.windowManager.hyprland.settings.windowrule = [ - "float 1, match:class ^(com\\.gabm\\.satty)$" + wayland.windowManager.hyprland.settings.window_rule = [ + { + name = "satty-float"; + match.class = "^(com\\.gabm\\.satty)$"; + float = true; + } ]; # Print with plain/Shift/Ctrl for region/window/full. # Super+L is already the hjkl focus and movement bind, so screenshots take # the Print key instead. wayland.windowManager.hyprland.settings.bind = [ - ", Print, exec, ${capture "area"}" - "SHIFT, Print, exec, ${capture "active"}" - "CTRL, Print, exec, ${capture "screen"}" + (execBind "Print" "${capture "area"}") + (execBind "SHIFT + Print" "${capture "active"}") + (execBind "CTRL + Print" "${capture "screen"}") ]; }; };