diff --git a/config/hyprland/hyprland.conf b/config/hyprland/hyprland.conf index d4419bcd8..fe5c6d7bb 100644 --- a/config/hyprland/hyprland.conf +++ b/config/hyprland/hyprland.conf @@ -439,4 +439,3 @@ bind = CTRL ALT SHIFT SUPER, L, exec, hyprlock # Notification Toggle # ============================================================================= bind = $mod SHIFT, D, exec, hyprpanel toggleWindow notifications-center - diff --git a/config/keyd/app.conf b/config/keyd/app.conf new file mode 100644 index 000000000..b673b3e2f --- /dev/null +++ b/config/keyd/app.conf @@ -0,0 +1,6 @@ +# keyd-application-mapper normalizes classes/titles to lowercase +# with punctuation collapsed to '-'. Slack's class becomes `slack`. +[slack] +leftmeta = layer(control) +prog1 = layer(control) +f13 = layer(control) diff --git a/config/keyd/default.conf b/config/keyd/default.conf index 92f31c9db..ef811097b 100644 --- a/config/keyd/default.conf +++ b/config/keyd/default.conf @@ -32,10 +32,6 @@ rightshift = capslock # xremap then converts Hyper+key → Ctrl+key for apps. # Keys excluded from xremap (e.g. 3/4/5) pass through as Hyper to Hyprland. -# Framework+C should behave like a real Ctrl+C in Slack/Electron. Emit it -# directly from keyd instead of sending Hyper+C through xremap first. -c = C-c - # Framework+Tab → Super+Tab for hyprshell window switcher tab = M-tab diff --git a/config/keyd/default.nix b/config/keyd/default.nix index b8bbcbe39..3392708da 100644 --- a/config/keyd/default.nix +++ b/config/keyd/default.nix @@ -1,4 +1,8 @@ -_: { +{ + username, + ... +}: +{ services.keyd.enable = true; # Optional: silence the setgid warning (nice to have, not required for functionality) @@ -14,4 +18,9 @@ _: { ]; environment.etc."keyd/default.conf".source = ./default.conf; + + home-manager.users.${username} = { + xdg.configFile."keyd/app.conf".source = ./app.conf; + services."keyd-application-mapper".enable = true; + }; } diff --git a/home-manager/modules/xremap/default.nix b/home-manager/modules/xremap/default.nix index 9fea2e04d..22244ba14 100644 --- a/home-manager/modules/xremap/default.nix +++ b/home-manager/modules/xremap/default.nix @@ -80,17 +80,23 @@ let value = "${ctrlPrefix}${key}"; }) keys ); - # Framework+key → Ctrl+key for all apps (macOS-style shortcuts) + # Framework+key -> Ctrl+key for all apps (macOS-style shortcuts) globalRemap = mkRemap remapKeys; - # Ghostty: Framework+C/V → Ctrl+Shift+C/V (terminal convention: Ctrl+C = SIGINT) - ghosttyRemap = globalRemap // { + # Terminal/editor clipboard convention: Framework+C/V -> Ctrl+Shift+C/V so + # copy/paste does not collide with SIGINT or app-specific bare Ctrl+C handlers. + terminalClipboardRemap = globalRemap // { "C-c" = "C-Shift-c"; "${hyperPrefix}c" = "C-Shift-c"; "${hyperPrefix}v" = "C-Shift-v"; }; - # Slack: same mapping as global but isolated so Slack-specific workarounds - # (modifier leak, thread mark-as-read on bare `c`/`Esc`) can be tuned here - # without affecting other apps. See commits e60e0df, 95679b8, 48ad8f5. + codeEditorAppIds = [ + "cursor" + "Cursor" + "code" + "Code" + ]; + # Slack-specific Framework->Ctrl behavior is handled by keyd's + # application mapper on hosts that enable ~/.config/keyd/app.conf. slackRemap = globalRemap; in { @@ -109,10 +115,16 @@ in keypress_delay_ms = 10; keymap = [ { - # Ghostty: Framework+C/V → Ctrl+Shift+C/V (terminal convention: Ctrl+C = SIGINT) + # Ghostty: Framework+C/V -> Ctrl+Shift+C/V (terminal convention: Ctrl+C = SIGINT) name = "Framework Command (Ghostty)"; application.only = [ "com.mitchellh.ghostty" ]; - remap = ghosttyRemap; + remap = terminalClipboardRemap; + } + { + # Cursor/VS Code: terminal-style clipboard shortcut, not bare Ctrl+C. + name = "Framework Command (Code Editors)"; + application.only = codeEditorAppIds; + remap = terminalClipboardRemap; } { # Slack: isolated block so modifier-leak / thread mark-as-read diff --git a/home-manager/services/default.nix b/home-manager/services/default.nix index 461c9505e..c7febbd75 100644 --- a/home-manager/services/default.nix +++ b/home-manager/services/default.nix @@ -16,6 +16,7 @@ let dockerPostgres = ./docker-postgres; dotfilesUpdater = import ./dotfiles-updater { inherit pkgs; }; gasTown = import ./gas-town { inherit pkgs; }; + keydApplicationMapper = ./keyd-application-mapper; makeUpdater = import ./make-updater { inherit pkgs; }; neversslKeepalive = import ./neverssl-keepalive { inherit pkgs; }; obsidian = import ./obsidian { inherit config pkgs inputs; }; @@ -37,6 +38,7 @@ in dockerPostgres dotfilesUpdater gasTown + keydApplicationMapper makeUpdater neversslKeepalive obsidian diff --git a/home-manager/services/keyd-application-mapper/default.nix b/home-manager/services/keyd-application-mapper/default.nix new file mode 100644 index 000000000..018bad54d --- /dev/null +++ b/home-manager/services/keyd-application-mapper/default.nix @@ -0,0 +1,47 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.services."keyd-application-mapper"; +in +{ + options.services."keyd-application-mapper" = { + enable = lib.mkEnableOption "keyd application mapper user service"; + }; + + config = lib.mkIf (pkgs.stdenv.isLinux && cfg.enable) { + assertions = [ + { + assertion = config.xdg.configFile ? "keyd/app.conf"; + message = "services.keyd-application-mapper requires xdg.configFile.\"keyd/app.conf\""; + } + ]; + + systemd.user.services.keyd-application-mapper = { + Unit = { + Description = "keyd application mapper"; + After = [ "graphical-session.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + Service = { + Type = "simple"; + # Force a unit restart on switch when app.conf changes. + Environment = [ + "KEYD_APP_CONF_HASH=${builtins.hashFile "sha256" config.xdg.configFile."keyd/app.conf".source}" + ]; + # User managers can start before refreshed supplementary groups + # are visible in the login session. Enter the keyd group + # explicitly so the mapper can always reach /var/run/keyd.socket. + ExecStart = "${pkgs.bash}/bin/bash -lc 'exec /run/wrappers/bin/sg keyd -c \"KEYD_BIN=${pkgs.keyd}/bin/keyd ${pkgs.keyd}/bin/keyd-application-mapper\"'"; + Restart = "on-failure"; + RestartSec = 3; + }; + Install = { + WantedBy = [ "graphical-session.target" ]; + }; + }; + }; +} diff --git a/named-hosts/matic/default.nix b/named-hosts/matic/default.nix index d14ec15d2..226d021aa 100644 --- a/named-hosts/matic/default.nix +++ b/named-hosts/matic/default.nix @@ -14,6 +14,7 @@ import ../../hosts/nixos { "video" "audio" "docker" + "keyd" ]; modules = [ # Framework 13" AMD AI 300 hardware support @@ -22,6 +23,9 @@ import ../../hosts/nixos { # Hardware configuration ./hardware-configuration.nix + # Shared keyd config and application mapper wiring + ../../config/keyd/default.nix + # Kolide launcher ./kolide.nix @@ -86,18 +90,6 @@ import ../../hosts/nixos { ExecStop = "${pkgs.e2fsprogs}/bin/chattr -i /"; }; }; - # 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 = ''