From efb0179344e58dc2218bd7543dde7ca1243a587d Mon Sep 17 00:00:00 2001 From: Jelle Spreeuwenberg Date: Wed, 29 Apr 2026 13:50:20 +0200 Subject: [PATCH] feat: improve config UX --- modules/capabilities/niri/_bindings.nix | 154 ++++++++++++------ modules/capabilities/niri/default.nix | 197 +++++++++++++++++++++++- 2 files changed, 307 insertions(+), 44 deletions(-) diff --git a/modules/capabilities/niri/_bindings.nix b/modules/capabilities/niri/_bindings.nix index 9d69725..6be4230 100644 --- a/modules/capabilities/niri/_bindings.nix +++ b/modules/capabilities/niri/_bindings.nix @@ -1,6 +1,7 @@ { browserCommand, terminalCommand, + uxCommands, }: { "Mod+Return" = { @@ -19,6 +20,26 @@ ]; hotkey-overlay.title = "App Launcher"; }; + "Mod+E" = { + repeat = false; + action.spawn = uxCommands.neovimProjects; + hotkey-overlay.title = "Neovim Projects"; + }; + "Mod+Ctrl+Return" = { + repeat = false; + action.spawn = uxCommands.nixosTerminal; + hotkey-overlay.title = "NixOS Config Terminal"; + }; + "Mod+Ctrl+Shift+Return" = { + repeat = false; + action.spawn = uxCommands.nixosSwitch; + hotkey-overlay.title = "NixOS Switch"; + }; + "Mod+Ctrl+E" = { + repeat = false; + action.spawn = uxCommands.editSecrets; + hotkey-overlay.title = "Edit Secrets"; + }; "XF86AudioPlay" = { action.spawn-sh = "playerctl play-pause"; @@ -70,11 +91,11 @@ action.toggle-keyboard-shortcuts-inhibit = [ ]; allow-inhibiting = false; }; - "Mod+Alt+L" = { + "Mod+Ctrl+L" = { action.spawn-sh = "loginctl lock-session"; hotkey-overlay.title = "Lock Screen"; }; - "Mod+Shift+E".action.quit = [ ]; + "Mod+Ctrl+Shift+Q".action.quit = [ ]; "Ctrl+Alt+Delete".action.quit = [ ]; "Mod+Shift+P".action.power-off-monitors = [ ]; @@ -87,52 +108,47 @@ repeat = false; }; + "Mod+Left".action.focus-column-or-monitor-left = [ ]; + "Mod+Down".action.focus-window-down = [ ]; + "Mod+Up".action.focus-window-up = [ ]; + "Mod+Right".action.focus-column-or-monitor-right = [ ]; "Mod+H".action.focus-column-or-monitor-left = [ ]; "Mod+J".action.focus-window-down = [ ]; "Mod+K".action.focus-window-up = [ ]; "Mod+L".action.focus-column-or-monitor-right = [ ]; - "Mod+Ctrl+Left".action.move-column-left = [ ]; - "Mod+Ctrl+Down".action.move-window-down = [ ]; - "Mod+Ctrl+Up".action.move-window-up = [ ]; - "Mod+Ctrl+Right".action.move-column-right = [ ]; - "Mod+Ctrl+H".action.move-column-left = [ ]; - "Mod+Ctrl+J".action.move-window-down = [ ]; - "Mod+Ctrl+K".action.move-window-up = [ ]; - "Mod+Ctrl+L".action.move-column-right = [ ]; + "Mod+Alt+Left".action.move-column-left-or-to-monitor-left = [ ]; + "Mod+Alt+Down".action.move-window-down-or-to-workspace-down = [ ]; + "Mod+Alt+Up".action.move-window-up-or-to-workspace-up = [ ]; + "Mod+Alt+Right".action.move-column-right-or-to-monitor-right = [ ]; + "Mod+Alt+H".action.move-column-left-or-to-monitor-left = [ ]; + "Mod+Alt+J".action.move-window-down-or-to-workspace-down = [ ]; + "Mod+Alt+K".action.move-window-up-or-to-workspace-up = [ ]; + "Mod+Alt+L".action.move-column-right-or-to-monitor-right = [ ]; "Mod+Home".action.focus-column-first = [ ]; "Mod+End".action.focus-column-last = [ ]; - "Mod+Ctrl+Home".action.move-column-to-first = [ ]; - "Mod+Ctrl+End".action.move-column-to-last = [ ]; + "Mod+Alt+Home".action.move-column-to-first = [ ]; + "Mod+Alt+End".action.move-column-to-last = [ ]; + + "Mod+Tab".action.focus-monitor-next = [ ]; + "Mod+Alt+Tab".action.move-column-to-monitor-next = [ ]; + "Mod+Shift+Tab".action.move-workspace-to-monitor-next = [ ]; "Mod+Shift+Left".action.focus-monitor-left = [ ]; "Mod+Shift+Down".action.focus-monitor-down = [ ]; "Mod+Shift+Up".action.focus-monitor-up = [ ]; "Mod+Shift+Right".action.focus-monitor-right = [ ]; - "Mod+Shift+H".action.focus-monitor-left = [ ]; - "Mod+Shift+J".action.focus-monitor-down = [ ]; - "Mod+Shift+K".action.focus-monitor-up = [ ]; - "Mod+Shift+L".action.focus-monitor-right = [ ]; - - "Mod+Shift+Ctrl+Left".action.move-column-to-monitor-left = [ ]; - "Mod+Shift+Ctrl+Down".action.move-column-to-monitor-down = [ ]; - "Mod+Shift+Ctrl+Up".action.move-column-to-monitor-up = [ ]; - "Mod+Shift+Ctrl+Right".action.move-column-to-monitor-right = [ ]; - "Mod+Shift+Ctrl+H".action.move-column-to-monitor-left = [ ]; - "Mod+Shift+Ctrl+J".action.move-column-to-monitor-down = [ ]; - "Mod+Shift+Ctrl+K".action.move-column-to-monitor-up = [ ]; - "Mod+Shift+Ctrl+L".action.move-column-to-monitor-right = [ ]; "Mod+Page_Down".action.focus-workspace-down = [ ]; "Mod+Page_Up".action.focus-workspace-up = [ ]; "Mod+U".action.focus-workspace-down = [ ]; "Mod+I".action.focus-workspace-up = [ ]; - "Mod+Ctrl+Page_Down".action.move-column-to-workspace-down = [ ]; - "Mod+Ctrl+Page_Up".action.move-column-to-workspace-up = [ ]; - "Mod+Ctrl+U".action.move-column-to-workspace-down = [ ]; - "Mod+Ctrl+I".action.move-column-to-workspace-up = [ ]; + "Mod+Alt+Page_Down".action.move-column-to-workspace-down = [ ]; + "Mod+Alt+Page_Up".action.move-column-to-workspace-up = [ ]; + "Mod+Alt+U".action.move-column-to-workspace-down = [ ]; + "Mod+Alt+I".action.move-column-to-workspace-up = [ ]; "Mod+Shift+Page_Down".action.move-workspace-down = [ ]; "Mod+Shift+Page_Up".action.move-workspace-up = [ ]; @@ -147,23 +163,21 @@ action.focus-workspace-up = [ ]; cooldown-ms = 150; }; - "Mod+Ctrl+WheelScrollDown" = { + "Mod+Alt+WheelScrollDown" = { action.move-column-to-workspace-down = [ ]; cooldown-ms = 150; }; - "Mod+Ctrl+WheelScrollUp" = { + "Mod+Alt+WheelScrollUp" = { action.move-column-to-workspace-up = [ ]; cooldown-ms = 150; }; - "Mod+WheelScrollRight".action.focus-column-right = [ ]; - "Mod+WheelScrollLeft".action.focus-column-left = [ ]; - "Mod+Ctrl+WheelScrollRight".action.move-column-right = [ ]; - "Mod+Ctrl+WheelScrollLeft".action.move-column-left = [ ]; - "Mod+Shift+WheelScrollDown".action.focus-column-right = [ ]; - "Mod+Shift+WheelScrollUp".action.focus-column-left = [ ]; - "Mod+Ctrl+Shift+WheelScrollDown".action.move-column-right = [ ]; - "Mod+Ctrl+Shift+WheelScrollUp".action.move-column-left = [ ]; + "Mod+WheelScrollRight".action.focus-column-or-monitor-right = [ ]; + "Mod+WheelScrollLeft".action.focus-column-or-monitor-left = [ ]; + "Mod+Alt+WheelScrollRight".action.move-column-right-or-to-monitor-right = [ ]; + "Mod+Alt+WheelScrollLeft".action.move-column-left-or-to-monitor-left = [ ]; + "Mod+Shift+WheelScrollDown".action.focus-column-or-monitor-right = [ ]; + "Mod+Shift+WheelScrollUp".action.focus-column-or-monitor-left = [ ]; "Mod+1".action.focus-workspace = 1; "Mod+2".action.focus-workspace = 2; @@ -191,21 +205,75 @@ "Mod+Period".action.expel-window-from-column = [ ]; "Mod+R".action.switch-preset-column-width = [ ]; - "Mod+Shift+R".action.switch-preset-window-height = [ ]; + "Mod+Shift+R".action.switch-preset-column-width-back = [ ]; "Mod+Ctrl+R".action.reset-window-height = [ ]; + "Mod+Ctrl+Shift+R".action.switch-preset-window-height = [ ]; "Mod+F".action.maximize-column = [ ]; "Mod+Shift+F".action.fullscreen-window = [ ]; "Mod+M".action.maximize-window-to-edges = [ ]; - "Mod+Ctrl+F".action.expand-column-to-available-width = [ ]; "Mod+C".action.center-column = [ ]; - "Mod+Ctrl+C".action.center-visible-columns = [ ]; + "Mod+Shift+H".action.set-column-width = "-10%"; + "Mod+Shift+L".action.set-column-width = "+10%"; + "Mod+Shift+J".action.set-window-height = "+10%"; + "Mod+Shift+K".action.set-window-height = "-10%"; "Mod+Minus".action.set-column-width = "-10%"; "Mod+Equal".action.set-column-width = "+10%"; "Mod+Shift+Minus".action.set-window-height = "-10%"; "Mod+Shift+Equal".action.set-window-height = "+10%"; - "Mod+V".action.toggle-window-floating = [ ]; + "Mod+Ctrl+F" = { + repeat = false; + action.spawn = [ + uxCommands.vicinaeCommand + "files" + ]; + hotkey-overlay.title = "Find Files"; + }; + "Mod+V" = { + repeat = false; + action.spawn = uxCommands.clipboardHistory; + hotkey-overlay.title = "Clipboard History"; + }; + "Mod+Ctrl+N" = { + repeat = false; + action.spawn = [ + uxCommands.vicinaeCommand + "nix-options" + ]; + hotkey-overlay.title = "NixOS Options"; + }; + "Mod+Ctrl+H" = { + repeat = false; + action.spawn = [ + uxCommands.vicinaeCommand + "home-manager-options" + ]; + hotkey-overlay.title = "Home Manager Options"; + }; + "Mod+Ctrl+P" = { + repeat = false; + action.spawn = [ + uxCommands.vicinaeCommand + "nix-packages" + ]; + hotkey-overlay.title = "Nix Packages"; + }; + "Mod+Ctrl+W" = { + repeat = false; + action.spawn = [ + uxCommands.vicinaeCommand + "niri-windows" + ]; + hotkey-overlay.title = "Windows"; + }; + "Mod+Ctrl+C" = { + repeat = false; + action.spawn = uxCommands.pickColor; + hotkey-overlay.title = "Pick Color"; + }; + + "Mod+Alt+V".action.toggle-window-floating = [ ]; "Mod+Shift+V".action.switch-focus-between-floating-and-tiling = [ ]; "Mod+W".action.toggle-column-tabbed-display = [ ]; } diff --git a/modules/capabilities/niri/default.nix b/modules/capabilities/niri/default.nix index cc5b063..f70b37b 100644 --- a/modules/capabilities/niri/default.nix +++ b/modules/capabilities/niri/default.nix @@ -35,6 +35,9 @@ in browserCommand = config.meta.desktop.browser.command; fileManagerPackage = config.meta.desktop.fileManager.package; terminalCommand = config.meta.desktop.terminal.command; + terminalDesktopEntryName = config.meta.desktop.terminal.desktopEntryName; + nixosConfigDir = repo.account.nixosConfigurationPath; + secretsFile = "${nixosConfigDir}/modules/secrets/secrets.yaml"; outputs = lib.mapAttrs ( _: display: { @@ -59,6 +62,188 @@ in }; } ) osConfig.meta.machine.displays; + terminalCommandArg = lib.escapeShellArg terminalCommand; + mkTerminalScript = + { + name, + title, + appId ? "niri-ux-terminal", + workdir ? nixosConfigDir, + command ? null, + runtimeInputs ? [ ], + }: + let + terminalArgs = + if terminalDesktopEntryName == "kitty" then + [ + "--class" + appId + "--title" + title + "--directory" + workdir + ] + else if terminalDesktopEntryName == "foot" then + [ + "--app-id" + appId + "--title" + title + "--working-directory" + workdir + ] + else + [ ]; + commandArgs = + if command == null then + [ ] + else + [ + "--" + "${pkgs.bash}/bin/bash" + "-lc" + command + ]; + execArgs = lib.concatMapStringsSep " " lib.escapeShellArg (terminalArgs ++ commandArgs); + in + pkgs.writeShellApplication { + inherit name runtimeInputs; + checkPhase = ""; + text = '' + # shellcheck disable=SC2016 + cd ${lib.escapeShellArg workdir} + exec ${terminalCommandArg} ${execArgs} + ''; + }; + uxScripts = + let + vicinaePackage = config.programs.vicinae.package or pkgs.vicinae; + in + { + nixosTerminal = mkTerminalScript { + name = "niri-ux-nixos-terminal"; + title = "NixOS Config"; + }; + + nixosSwitch = mkTerminalScript { + name = "niri-ux-nixos-switch"; + title = "NixOS Switch"; + appId = "niri-ux-float"; + runtimeInputs = [ + pkgs.coreutils + pkgs.nh + ]; + command = '' + set -o pipefail + + log_dir="''${XDG_STATE_HOME:-$HOME/.local/state}/nixos-switch" + mkdir -p "$log_dir" + log="$log_dir/$(date +%Y%m%d-%H%M%S).log" + status_file="$(mktemp)" + + printf 'Running nh os switch in %s\n\n' ${lib.escapeShellArg nixosConfigDir} + ( + cd ${lib.escapeShellArg nixosConfigDir} + nh os switch + printf '%s' "$?" > "$status_file" + ) 2>&1 | tee "$log" + + status="$(cat "$status_file")" + rm -f "$status_file" + + printf '\nLog: %s\n' "$log" + if [ "$status" -eq 0 ]; then + printf 'NixOS switch completed successfully.\n' + else + printf 'NixOS switch failed with exit code %s.\n' "$status" + fi + + printf 'Press Enter to close...' + read -r _ + exit "$status" + ''; + }; + + editSecrets = mkTerminalScript { + name = "niri-ux-edit-secrets"; + title = "Edit Secrets"; + appId = "niri-ux-float"; + runtimeInputs = [ pkgs.sops ]; + command = '' + sops edit ${lib.escapeShellArg secretsFile} + ''; + }; + + neovimProjects = mkTerminalScript { + name = "niri-ux-neovim-projects"; + title = "Neovim Projects"; + command = '' + nvim -c 'Telescope projects' + ''; + }; + + vicinaeCommand = pkgs.writeShellApplication { + name = "niri-ux-vicinae-command"; + runtimeInputs = [ vicinaePackage ]; + text = '' + case "''${1:-}" in + files) + link="vicinae://extensions/sameoldlab/fuzzy-files/find" + ;; + nix-options) + link="vicinae://extensions/knoopx/nix/options" + ;; + home-manager-options) + link="vicinae://extensions/knoopx/nix/home-manager-options" + ;; + nix-packages) + link="vicinae://extensions/knoopx/nix/packages" + ;; + niri-windows) + link="vicinae://extensions/knoopx/niri/windows" + ;; + *) + printf 'unknown Vicinae command target: %s\n' "''${1:-}" >&2 + exit 64 + ;; + esac + + exec vicinae deeplink "$link" + ''; + }; + + clipboardHistory = pkgs.writeShellApplication { + name = "niri-ux-clipboard-history"; + runtimeInputs = [ + pkgs.cliphist + vicinaePackage + pkgs.wl-clipboard + ]; + text = '' + selection="$(cliphist list | vicinae dmenu --navigation-title Clipboard --placeholder 'Search clipboard' --no-metadata)" + if [ -z "$selection" ]; then + exit 0 + fi + + printf '%s' "$selection" | cliphist decode | wl-copy + ''; + }; + + pickColor = pkgs.writeShellApplication { + name = "niri-ux-pick-color"; + runtimeInputs = [ + pkgs.libnotify + pkgs.niri + pkgs.wl-clipboard + ]; + text = '' + color="$(niri msg pick-color)" + printf '%s' "$color" | wl-copy + notify-send 'Color picked' "$color" + ''; + }; + }; + uxCommands = lib.mapAttrs (_: lib.getExe) uxScripts; in { home.sessionVariables.NIXOS_OZONE_WL = "1"; @@ -76,7 +261,8 @@ in brightnessctl xwayland-satellite ] - ++ [ fileManagerPackage ]; + ++ [ fileManagerPackage ] + ++ lib.attrValues uxScripts; programs.niri.settings = { inherit outputs; @@ -128,9 +314,17 @@ in }; clip-to-geometry = true; } + { + matches = [ + { app-id = "^niri-ux-float$"; } + ]; + open-floating = true; + open-focused = true; + } ]; debug.honor-xdg-activation-with-invalid-serial = true; + gestures.hot-corners.enable = false; input = { focus-follows-mouse.enable = true; @@ -150,6 +344,7 @@ in inherit browserCommand terminalCommand + uxCommands ; }; };