diff --git a/config/ghostty/config b/config/ghostty/config index 811320a78..d925f64db 100644 --- a/config/ghostty/config +++ b/config/ghostty/config @@ -20,6 +20,10 @@ copy-on-select = clipboard keybind = ctrl+shift+c=copy_to_clipboard keybind = ctrl+shift+v=paste_from_clipboard +# Framework+C/V arrive as plain Ctrl+C/V (keyd, see config/keyd/default.conf). +# performable: copy only when a selection exists; otherwise Ctrl+C stays SIGINT. +keybind = performable:ctrl+c=copy_to_clipboard +keybind = ctrl+v=paste_from_clipboard keybind = alt+backspace=text:\x1b\x7f keybind = shift+enter=text:\n keybind = ctrl+enter=text:\x1b[13;5u diff --git a/config/hyprland/hyprland.conf b/config/hyprland/hyprland.conf index 6cde7bb97..63532b2ef 100644 --- a/config/hyprland/hyprland.conf +++ b/config/hyprland/hyprland.conf @@ -41,7 +41,6 @@ env = NOCTALIA_PAM_SERVICE,noctalia-shell # ============================================================================= exec-once = dbus-update-activation-environment --systemd --all exec-once = systemctl --user import-environment WAYLAND_DISPLAY XDG_CURRENT_DESKTOP XDG_SESSION_TYPE XDG_RUNTIME_DIR HYPRLAND_INSTANCE_SIGNATURE XDG_SESSION_DESKTOP -exec-once = sleep 1 && systemctl --user restart xremap exec-once = sleep 1 && systemctl --user restart xdg-desktop-portal-hyprland && sleep 0.5 && systemctl --user restart xdg-desktop-portal-gtk && sleep 0.5 && systemctl --user restart xdg-desktop-portal # Blue light filter diff --git a/config/keyd/default.conf b/config/keyd/default.conf index 0c613f0a6..55a0cbfd9 100644 --- a/config/keyd/default.conf +++ b/config/keyd/default.conf @@ -4,22 +4,13 @@ # does not grab I2C HID consumer control devices, so the Framework key # bypasses keyd unless this id is listed explicitly. 32ac:0006 -# xremap creates its own virtual keyboard device via uinput. By default, xremap -# uses vendor/product 0x1234/0x5678 for that output device. -# -# When keyd matches all keyboards via `*`, it can end up grabbing xremap's -# virtual device too, which may cause event feedback/loops and effectively kill -# input. Exclude the default xremap device id to prevent that. --1234:5678 [main] # SUPER: CapsLock/RightAlt → Hyprland "$mod" binds (window mgmt, app launch) -# These skip xremap and go directly to Hyprland. capslock = layer(meta) rightalt = layer(meta) -# Hyper: Framework key → Ctrl+Alt+Shift+Super (all four modifiers) -# xremap intercepts these and remaps to Ctrl for macOS-style app shortcuts. +# Cmd: Framework key → macOS-style shortcuts (see [cmd_hyper]) # leftmeta is the actual keycode on Framework 13 AI 300 leftmeta = layer(cmd_hyper) prog1 = layer(cmd_hyper) @@ -31,20 +22,75 @@ rightshift = overload(shift, oneshot(rshift)) [rshift] rightshift = capslock -[cmd_hyper:C-A-S-M] -# 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. - -# Framework+Tab → Super+Tab for hyprshell window switcher -tab = M-tab - -# Clipboard: emit plain Ctrl+C/V directly, bypassing xremap. Electron apps -# (Slack) drop xremap's synthesized Hyper→Ctrl modifier swaps, so clipboard -# keys must not go through that dance. Ghostty converts C-c/C-v to -# C-S-c/C-S-v via its xremap keymap block. +# Cmd layer: Framework key acts like macOS Cmd. Base modifier is Super only +# (M) so holding Framework never asserts Ctrl/Alt/Shift speculatively — +# Slack treats Alt+Click as "mark unread", so a held Alt plus a stray +# trackpad tap kept marking chats unread. Every key below emits its combo +# atomically instead; no xremap middleman. +# +# Unmapped keys fall through as Super+key (inert — no Hyprland binds match). +# Framework+Tab is intentionally unmapped: it becomes a genuinely held +# Super+Tab, which lets hyprshell hold-to-cycle. +[cmd_hyper:M] +# macOS-style app shortcuts: Framework+key = Ctrl+key. +# Ghostty maps ctrl+c/ctrl+v to copy/paste itself (see config/ghostty/config). +a = C-a +b = C-b c = C-c +d = C-d +e = C-e +g = C-g +h = C-h +i = C-i +j = C-j +k = C-k +m = C-m +n = C-n +o = C-o +p = C-p +q = C-q +r = C-r +s = C-s +t = C-t +u = C-u v = C-v +w = C-w +x = C-x +y = C-y +z = C-z +0 = C-0 +1 = C-1 +2 = C-2 +6 = C-6 +7 = C-7 +8 = C-8 +9 = C-9 +semicolon = C-semicolon +dot = C-dot +comma = C-comma +slash = C-slash +grave = C-grave +backslash = C-backslash +leftbrace = C-leftbrace +rightbrace = C-rightbrace +apostrophe = C-apostrophe +backspace = C-backspace +left = C-left +right = C-right +up = C-up +down = C-down + +# Hyprland Hyper binds (Ctrl+Alt+Shift+Super chords, see hyprland.conf): +# f = fullscreen, l = lock, space = launcher, 3/4/5 = screenshot/recording, +# equal/minus = display zoom +f = C-A-S-M-f +l = C-A-S-M-l +space = C-A-S-M-space +3 = C-A-S-M-3 +4 = C-A-S-M-4 +5 = C-A-S-M-5 +equal = C-A-S-M-equal +minus = C-A-S-M-minus [cmd_hyper+control] # Framework+Ctrl+5 → terminal split in VS Code diff --git a/flake.lock b/flake.lock index a3f060b3d..2e1e8fb9d 100644 --- a/flake.lock +++ b/flake.lock @@ -142,21 +142,6 @@ "type": "github" } }, - "crane": { - "locked": { - "lastModified": 1766774972, - "narHash": "sha256-8qxEFpj4dVmIuPn9j9z6NTbU+hrcGjBOvaxTzre5HmM=", - "owner": "ipetkov", - "repo": "crane", - "rev": "01bc1d404a51a0a07e9d8759cd50a7903e218c82", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "repo": "crane", - "type": "github" - } - }, "crate2nix": { "flake": false, "locked": { @@ -361,24 +346,6 @@ "type": "github" } }, - "flake-parts_7": { - "inputs": { - "nixpkgs-lib": "nixpkgs-lib_3" - }, - "locked": { - "lastModified": 1765835352, - "narHash": "sha256-XswHlK/Qtjasvhd1nOa1e8MgZ8GS//jBoTqWtrS1Giw=", - "owner": "hercules-ci", - "repo": "flake-parts", - "rev": "a34fae9c08a15ad73f295041fec82323541400a9", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "flake-parts", - "type": "github" - } - }, "flake-utils": { "locked": { "lastModified": 1644229661, @@ -781,21 +748,6 @@ "type": "github" } }, - "nixpkgs-lib_3": { - "locked": { - "lastModified": 1765674936, - "narHash": "sha256-k00uTP4JNfmejrCLJOwdObYC9jHRrr/5M/a/8L2EIdo=", - "owner": "nix-community", - "repo": "nixpkgs.lib", - "rev": "2075416fcb47225d9b68ac469a5c4801a9c4dd85", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "nixpkgs.lib", - "type": "github" - } - }, "nixpkgs-nightly": { "locked": { "lastModified": 1782710094, @@ -908,8 +860,7 @@ "nixpkgs-unstable": "nixpkgs-unstable", "noctalia-shell": "noctalia-shell", "nur": "nur", - "treefmt-nix": "treefmt-nix_4", - "xremap": "xremap" + "treefmt-nix": "treefmt-nix_4" } }, "rust-overlay": { @@ -1062,46 +1013,6 @@ "repo": "treefmt-nix", "type": "github" } - }, - "xremap": { - "inputs": { - "crane": "crane", - "flake-parts": "flake-parts_7", - "nixpkgs": [ - "nixpkgs" - ], - "xremap": "xremap_2" - }, - "locked": { - "lastModified": 1780797316, - "narHash": "sha256-9zY+zK4F1C0JYnsDoWnlu7Puf4WxmOMXoBF3Fadulo4=", - "owner": "xremap", - "repo": "nix-flake", - "rev": "9bf3b5df82cfb129c56f522c36577c654d87e93f", - "type": "github" - }, - "original": { - "owner": "xremap", - "repo": "nix-flake", - "type": "github" - } - }, - "xremap_2": { - "flake": false, - "locked": { - "lastModified": 1780333767, - "narHash": "sha256-Ia5jG+wsdhFmX2ZreIHpRQDscOF6FB0zZlOWWboMqDk=", - "owner": "k0kubun", - "repo": "xremap", - "rev": "250341f29bc6e9084575aa191193018a36b83803", - "type": "github" - }, - "original": { - "owner": "k0kubun", - "ref": "v0.15.8", - "repo": "xremap", - "type": "github" - } } }, "root": "root", diff --git a/flake.nix b/flake.nix index e5740d613..03185f906 100644 --- a/flake.nix +++ b/flake.nix @@ -60,10 +60,6 @@ nixos-hardware = { url = "github:NixOS/nixos-hardware/master"; }; - xremap = { - url = "github:xremap/nix-flake"; - inputs.nixpkgs.follows = "nixpkgs"; - }; noctalia-shell = { # v5 (C++ rewrite): option is programs.noctalia, config is TOML, binary is # `noctalia`. Not a drop-in from the v4 Quickshell line - v4 settings are not diff --git a/home-manager/default.nix b/home-manager/default.nix index 0689bc67f..4b24be80e 100644 --- a/home-manager/default.nix +++ b/home-manager/default.nix @@ -34,7 +34,6 @@ in ++ services ++ [ inputs.agenix.homeManagerModules.default - inputs.xremap.homeManagerModules.default inputs.noctalia-shell.homeModules.default ] ++ lib.optionals (pkgs.stdenv.isLinux && inputs.host.isDesktop) [ diff --git a/home-manager/modules/default.nix b/home-manager/modules/default.nix index dd02595fb..7f33cca07 100644 --- a/home-manager/modules/default.nix +++ b/home-manager/modules/default.nix @@ -7,5 +7,4 @@ ./secure-dotenv ./tailscale ./uv-globals - ./xremap ] diff --git a/home-manager/modules/xremap/default.nix b/home-manager/modules/xremap/default.nix deleted file mode 100644 index 8cd6ba29c..000000000 --- a/home-manager/modules/xremap/default.nix +++ /dev/null @@ -1,132 +0,0 @@ -{ - lib, - pkgs, - inputs, - ... -}: -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-"; - # "f" is excluded — passes through as Hyper+F for Hyprland fullscreen bind. - # "l" is excluded — passes through as Hyper+L for Hyprland lock screen bind. - # "c"/"v" are excluded — keyd emits plain Ctrl+C/V for Framework+C/V directly, - # because Electron apps (Slack) drop xremap's synthesized Hyper→Ctrl modifier - # swaps. Hyper+C/V never reaches xremap. - # See: config/keyd/default.conf ([cmd_hyper] section) - # See: config/hyprland/hyprland.conf (Window Management section, Lock Screen section) - letters = [ - "a" - "b" - "d" - "e" - "g" - "h" - "i" - "j" - "k" - "m" - "n" - "o" - "p" - "q" - "r" - "s" - "t" - "u" - "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 - ); - # Framework+key -> Ctrl+key for all apps (macOS-style shortcuts) - globalRemap = mkRemap remapKeys; -in -{ - config = lib.mkMerge [ - { services.xremap.enable = lib.mkDefault false; } - (lib.mkIf (isDesktop && isLinux) { - services.xremap = { - enable = true; - withWlroots = true; - watch = true; - # Only intercept keyd output — SUPER (CapsLock/RightAlt) goes straight to Hyprland - deviceNames = [ "keyd virtual keyboard" ]; - config = { - # Slack/Electron on Wayland can misread synthesized Hyper->Ctrl - # shortcuts when xremap replays them with no delay. - keypress_delay_ms = 10; - keymap = [ - { - # Ghostty: clipboard is Ctrl+Shift+C/V (terminal convention). - # keyd already collapsed Framework+C/V to plain Ctrl+C/V, so - # both Framework+C/V and bare Ctrl+C/V become copy/paste here. - name = "Framework Command (Ghostty)"; - application.only = [ "com.mitchellh.ghostty" ]; - remap = globalRemap // { - "C-c" = "C-Shift-c"; - "C-v" = "C-Shift-v"; - }; - } - { - # Global: no application filter — applies unconditionally so window detection - # failures don't cause raw Hyper events to leak through to apps (e.g. Slack). - # Note: app ids are lowercase on Wayland ("slack", not "Slack"). - name = "Framework Command (Global)"; - remap = globalRemap; - } - ]; - }; - }; - - # Upstream module already sets After/PartOf/Restart; only add extras. - systemd.user.services.xremap = { - Unit.StartLimitIntervalSec = 0; - Service.RestartSec = 3; - }; - }) - ]; -}