From af770059263beadb18a89909a89dae3125ecdfde Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Mon, 9 Feb 2026 17:36:32 +0900 Subject: [PATCH 1/4] feat(xremap): add xremap module and integrate with home-manager --- config/hyprland/hyprland.conf | 3 +- home-manager/default.nix | 1 + home-manager/modules/default.nix | 1 + home-manager/modules/xremap/default.nix | 119 +++++++++++++++++++++ named-hosts/matic/default.nix | 132 +++--------------------- 5 files changed, 134 insertions(+), 122 deletions(-) create mode 100644 home-manager/modules/xremap/default.nix diff --git a/config/hyprland/hyprland.conf b/config/hyprland/hyprland.conf index 23064fb78..690a2e2c2 100644 --- a/config/hyprland/hyprland.conf +++ b/config/hyprland/hyprland.conf @@ -257,8 +257,7 @@ bind = $mod, E, exec, emote # Window Management # ============================================================================= bind = $mod, W, killactive, -bind = $mod SHIFT, F, togglefloating, -bind = CTRL ALT SHIFT SUPER, F, exec, hyprctl --batch "dispatch movetoworkspace empty; dispatch fullscreen 0" +bind = $mod SHIFT, F, exec, hyprctl --batch "dispatch movetoworkspace empty; dispatch fullscreen 0" bind = SUPER CTRL, F, fullscreen, 0 bind = $mod SHIFT, Q, exec, hyprpanel toggleWindow power-menu bind = $mod, TAB, hyprexpo:expo, toggle diff --git a/home-manager/default.nix b/home-manager/default.nix index c83866c39..6f207375b 100644 --- a/home-manager/default.nix +++ b/home-manager/default.nix @@ -28,6 +28,7 @@ in ++ services ++ [ inputs.agenix.homeManagerModules.default + inputs.xremap.homeManagerModules.default ]; home.username = username; diff --git a/home-manager/modules/default.nix b/home-manager/modules/default.nix index 2e2e32809..414a17539 100644 --- a/home-manager/modules/default.nix +++ b/home-manager/modules/default.nix @@ -5,5 +5,6 @@ ./npm-globals ./tailscale ./uv-globals + ./xremap ./yek ] diff --git a/home-manager/modules/xremap/default.nix b/home-manager/modules/xremap/default.nix new file mode 100644 index 000000000..ed8408789 --- /dev/null +++ b/home-manager/modules/xremap/default.nix @@ -0,0 +1,119 @@ +{ + config, + lib, + pkgs, + inputs, + ... +}: +let + inherit (inputs.host) isDesktop; + inherit (pkgs.stdenv) isLinux; + + hyperPrefix = "C-Alt-Shift-Super-"; + ctrlPrefix = "C-"; + letters = [ + "a" + "b" + "c" + "d" + "e" + "f" + "g" + "h" + "i" + "j" + "k" + "l" + "m" + "n" + "o" + "p" + "q" + "r" + "s" + "t" + "u" + "v" + "w" + "x" + "y" + "z" + ]; + # Numbers 3, 4, 5 are intentionally excluded — they pass through as + # Hyper+3/4/5 for Hyprland screenshot/recording bindings. + # See: config/hyprland/hyprland.conf (screenshot section) + numbers = [ + "0" + "1" + "2" + "6" + "7" + "8" + "9" + ]; + symbols = [ + "semicolon" + "dot" + "comma" + "slash" + "grave" + "backslash" + "leftbrace" + "rightbrace" + "apostrophe" + ]; + navigation = [ + "tab" + "backspace" + "left" + "right" + "up" + "down" + ]; + remapKeys = letters ++ numbers ++ symbols ++ navigation; + mkRemap = + keys: + builtins.listToAttrs ( + map (key: { + name = "${hyperPrefix}${key}"; + value = "${ctrlPrefix}${key}"; + }) keys + ); + globalRemap = mkRemap remapKeys; + # Override copy/paste to Ctrl+Shift (terminal convention: Ctrl+C = SIGINT). + # All other keys (including z for undo) inherit from globalRemap. + ghosttyRemap = globalRemap // { + "${hyperPrefix}c" = "C-Shift-c"; + "${hyperPrefix}v" = "C-Shift-v"; + }; +in +{ + config = lib.mkIf (isDesktop && isLinux) { + services.xremap = { + enable = true; + withWlroots = true; + watch = true; + deviceNames = [ "keyd virtual keyboard" ]; + config = { + keymap = [ + { + name = "Framework Command (Ghostty)"; + application.only = [ "com.mitchellh.ghostty" ]; + remap = ghosttyRemap; + } + { + name = "Framework Command (Global)"; + application.not = [ "com.mitchellh.ghostty" ]; + remap = globalRemap; + } + ]; + }; + }; + + # Upstream module already sets After/PartOf/Restart; only add extras. + systemd.user.services.xremap = { + Unit.StartLimitIntervalSec = 0; + Service.RestartSec = 3; + }; + }; +} diff --git a/named-hosts/matic/default.nix b/named-hosts/matic/default.nix index e6a09e399..05998e22d 100644 --- a/named-hosts/matic/default.nix +++ b/named-hosts/matic/default.nix @@ -33,12 +33,6 @@ inputs.nixpkgs.lib.nixosSystem { # Kolide launcher ./kolide.nix - # Keyd configuration - ../../config/keyd - - # Xremap module - inputs.xremap.nixosModules.default - # Base system configuration ( { config, lib, ... }: @@ -76,6 +70,18 @@ inputs.nixpkgs.lib.nixosSystem { security.sudo.wheelNeedsPassword = false; + # Keyd configuration (Linux desktop only) + services.keyd.enable = true; + users.groups.keyd = { }; + systemd.services.keyd.serviceConfig = { + CapabilityBoundingSet = [ "CAP_SETGID" ]; + AmbientCapabilities = [ "CAP_SETGID" ]; + }; + systemd.services.keyd.restartTriggers = [ + (builtins.hashFile "sha256" ../../config/keyd/default.conf) + ]; + environment.etc."keyd/default.conf".source = ../../config/keyd/default.conf; + # Input remapping (xremap) hardware.uinput.enable = true; services.udev.extraRules = '' @@ -83,120 +89,6 @@ inputs.nixpkgs.lib.nixosSystem { KERNEL=="event*", ATTRS{name}=="keyd virtual keyboard", GROUP="input", MODE:="0660" KERNEL=="event*", ATTRS{name}=="keyd virtual pointer", GROUP="input", MODE:="0660" ''; - services.xremap = - let - hyperPrefix = "C-Alt-Shift-Super-"; - ctrlPrefix = "C-"; - letters = [ - "a" - "b" - "c" - "d" - "e" - "f" - "g" - "h" - "i" - "j" - "k" - "l" - "m" - "n" - "o" - "p" - "q" - "r" - "s" - "t" - "u" - "v" - "w" - "x" - "y" - "z" - ]; - # Numbers 3, 4, 5 are intentionally excluded — they pass through as - # Hyper+3/4/5 for Hyprland screenshot/recording bindings. - # See: config/hyprland/hyprland.conf (screenshot section) - numbers = [ - "0" - "1" - "2" - "6" - "7" - "8" - "9" - ]; - symbols = [ - "semicolon" - "dot" - "comma" - "slash" - "grave" - "backslash" - "leftbrace" - "rightbrace" - "apostrophe" - ]; - navigation = [ - "tab" - "backspace" - "left" - "right" - "up" - "down" - ]; - remapKeys = letters ++ numbers ++ symbols ++ navigation; - mkRemap = - keys: - builtins.listToAttrs ( - map (key: { - name = "${hyperPrefix}${key}"; - value = "${ctrlPrefix}${key}"; - }) keys - ); - globalRemap = mkRemap remapKeys; - # Override copy/paste to Ctrl+Shift (terminal convention: Ctrl+C = SIGINT). - # All other keys (including z for undo) inherit from globalRemap. - ghosttyRemap = globalRemap // { - "${hyperPrefix}c" = "C-Shift-c"; - "${hyperPrefix}v" = "C-Shift-v"; - }; - in - { - enable = true; - serviceMode = "user"; - userName = username; - withWlroots = true; - watch = true; - deviceNames = [ "keyd virtual keyboard" ]; - config = { - keymap = [ - { - name = "Framework Command (Ghostty)"; - application.only = [ "com.mitchellh.ghostty" ]; - remap = ghosttyRemap; - } - { - name = "Framework Command (Global)"; - application.not = [ "com.mitchellh.ghostty" ]; - remap = globalRemap; - } - ]; - }; - }; - - # Ensure xremap starts after Hyprland and auto-restarts on failure - systemd.user.services.xremap = { - after = [ "graphical-session.target" ]; - partOf = [ "graphical-session.target" ]; - serviceConfig = { - Restart = "on-failure"; - RestartSec = 3; - }; - # StartLimitIntervalSec is a [Unit] directive, not [Service]. - unitConfig.StartLimitIntervalSec = 0; - }; # AMD graphics with hardware acceleration hardware.graphics.enable = true; From 6be207b8f040bb26e74861dce805bc29a3d8176e Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Mon, 9 Feb 2026 17:43:29 +0900 Subject: [PATCH 2/4] fix(xremap): clarify comments for key remapping and ghostty configuration --- home-manager/modules/xremap/default.nix | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/home-manager/modules/xremap/default.nix b/home-manager/modules/xremap/default.nix index ed8408789..fb65f3fc5 100644 --- a/home-manager/modules/xremap/default.nix +++ b/home-manager/modules/xremap/default.nix @@ -9,6 +9,7 @@ let inherit (inputs.host) isDesktop; inherit (pkgs.stdenv) isLinux; + # Hyper = Framework key (keyd: [cmd_hyper:C-A-S-M]) → remapped to Ctrl for macOS-style shortcuts hyperPrefix = "C-Alt-Shift-Super-"; ctrlPrefix = "C-"; letters = [ @@ -79,9 +80,9 @@ let value = "${ctrlPrefix}${key}"; }) keys ); + # Framework+key → Ctrl+key for all apps (macOS-style shortcuts) globalRemap = mkRemap remapKeys; - # Override copy/paste to Ctrl+Shift (terminal convention: Ctrl+C = SIGINT). - # All other keys (including z for undo) inherit from globalRemap. + # Ghostty: Framework+C/V → Ctrl+Shift+C/V (terminal convention: Ctrl+C = SIGINT) ghosttyRemap = globalRemap // { "${hyperPrefix}c" = "C-Shift-c"; "${hyperPrefix}v" = "C-Shift-v"; @@ -93,6 +94,7 @@ in enable = true; withWlroots = true; watch = true; + # Only intercept keyd output — SUPER (CapsLock/RightAlt) goes straight to Hyprland deviceNames = [ "keyd virtual keyboard" ]; config = { keymap = [ From d0f8bc24109b6c26c1624556feaaa660e525529e Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Mon, 9 Feb 2026 17:44:10 +0900 Subject: [PATCH 3/4] docs: add key modifier comments to keyd and hyprland configs Clarify SUPER vs Hyper key pipeline in keyd comments and revert incorrect hyprland bind change (restore togglefloating on $mod+Shift+F). --- config/hyprland/hyprland.conf | 3 ++- config/keyd/default.conf | 12 +++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/config/hyprland/hyprland.conf b/config/hyprland/hyprland.conf index 690a2e2c2..23064fb78 100644 --- a/config/hyprland/hyprland.conf +++ b/config/hyprland/hyprland.conf @@ -257,7 +257,8 @@ bind = $mod, E, exec, emote # Window Management # ============================================================================= bind = $mod, W, killactive, -bind = $mod SHIFT, F, exec, hyprctl --batch "dispatch movetoworkspace empty; dispatch fullscreen 0" +bind = $mod SHIFT, F, togglefloating, +bind = CTRL ALT SHIFT SUPER, F, exec, hyprctl --batch "dispatch movetoworkspace empty; dispatch fullscreen 0" bind = SUPER CTRL, F, fullscreen, 0 bind = $mod SHIFT, Q, exec, hyprpanel toggleWindow power-menu bind = $mod, TAB, hyprexpo:expo, toggle diff --git a/config/keyd/default.conf b/config/keyd/default.conf index 09694f528..ac5225181 100644 --- a/config/keyd/default.conf +++ b/config/keyd/default.conf @@ -9,13 +9,13 @@ -1234:5678 [main] -# Caps Lock = pure Super (for Hyprland hotkeys) +# SUPER: CapsLock/RightAlt → Hyprland "$mod" binds (window mgmt, app launch) +# These skip xremap and go directly to Hyprland. capslock = layer(meta) - -# Right Alt = pure Super (right-hand mirror of Caps Lock) rightalt = layer(meta) -# Framework key = macOS-style Command (Hyper for all keys) +# Hyper: Framework key → Ctrl+Alt+Shift+Super (all four modifiers) +# xremap intercepts these and remaps to Ctrl for macOS-style app shortcuts. # leftmeta is the actual keycode on Framework 13 AI 300 leftmeta = layer(cmd_hyper) prog1 = layer(cmd_hyper) @@ -28,7 +28,9 @@ rightshift = overload(shift, oneshot(rshift)) rightshift = capslock [cmd_hyper:C-A-S-M] -# macOS-style Command → Hyper (Ctrl+Alt+Shift+Super) +# Every Framework+key emits Ctrl+Alt+Shift+Super+key (Hyper). +# xremap then converts Hyper+key → Ctrl+key for apps. +# Keys excluded from xremap (e.g. 3/4/5) pass through as Hyper to Hyprland. [cmd_hyper+control] # Framework+Ctrl+5 → terminal split in VS Code From 500cf2076f1dd371d1adb71e0a4228b153bebf8d Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Mon, 9 Feb 2026 17:49:47 +0900 Subject: [PATCH 4/4] feat(xremap): exclude f from remap for Hyprland fullscreen bind Framework+F now passes through as Hyper+F to Hyprland instead of being remapped to Ctrl+F by xremap. --- home-manager/modules/xremap/default.nix | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/home-manager/modules/xremap/default.nix b/home-manager/modules/xremap/default.nix index fb65f3fc5..7b5d076ec 100644 --- a/home-manager/modules/xremap/default.nix +++ b/home-manager/modules/xremap/default.nix @@ -12,13 +12,14 @@ let # Hyper = Framework key (keyd: [cmd_hyper:C-A-S-M]) → remapped to Ctrl for macOS-style shortcuts hyperPrefix = "C-Alt-Shift-Super-"; ctrlPrefix = "C-"; + # "f" is excluded — passes through as Hyper+F for Hyprland fullscreen bind. + # See: config/hyprland/hyprland.conf (Window Management section) letters = [ "a" "b" "c" "d" "e" - "f" "g" "h" "i"