From 5720bebbf2cd37040143db8b654c1ad4e88de6ca Mon Sep 17 00:00:00 2001 From: bddap-bot Date: Fri, 19 Jun 2026 18:38:37 -0700 Subject: [PATCH 1/3] =?UTF-8?q?zellij:=20tmux-faithful=20keybinds=20?= =?UTF-8?q?=E2=80=94=20capture=20only=20the=20C-\=20prefix=20globally?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner: "zellij hijacks my Ctrl-p/n, Ctrl-f and friends." Stock zellij binds a dozen Ctrl/Alt chords GLOBALLY (Ctrl-p/n/f/t/h/s/o/g/q, Alt-…) as mode-switches, stealing them from the shell. The previous config rebound the prefix to C-\ but left clear-defaults=false, so all those stock global binds still stole keys. Switch to clear-defaults=true (nukes every stock bind, global and in-mode) and rebind, tmux-style: - ONE global capture: `Ctrl \` -> tmux mode (shared_except tmux/scroll/renametab, so the prefix is free to send-prefix inside tmux mode and doesn't yank you out of the text/scroll sub-modes). Every other Ctrl/Alt key now falls through to the running program — verified with a headless pty: Ctrl-f/p/n/t reach a shell verbatim while `Ctrl \` enters tmux mode. - tmux-mode bindings modeled on tmux's default prefix table + ~/.tmux.conf: % / " splits, hjkl+arrows focus, o next-pane, z zoom, x close, Space next-layout, c/n/p/& tab new/next/prev/close, 1-9 tab jump, d detach, , rename, [ copy-mode/scroll. Single keys (post-prefix). One-shots return to normal; scroll/renametab get escape binds (clear-defaults wiped the stock ones, so without them you'd be stuck in-mode). - prefix prefix -> Write 28 (literal Ctrl-\) for send-prefix. The spiral plugin block + layout are untouched (the 4e7076c re-tile fix stays intact; its headless test still passes on the production artifacts). IMPORTANT (separate from this file): zellij only captures `Ctrl \` when the terminal delivers it via the kitty keyboard protocol (CSI-u). zellij's input parser (vendored termwiz) has no decode mapping for the legacy bare 0x1c byte, so a terminal that sends Ctrl-\ as 0x1c won't trigger the prefix. Our Alacritty has a `ReceiveChar` override on Ctrl-Backslash (added for tmux) that forces exactly that legacy byte — so until that's reconsidered, the prefix won't fire in Alacritty (the Ctrl-p/n/f passthrough fix works regardless). Flagged to the owner. Verified: `zellij setup --check` clean; home-manager build RC=0; headless pty proves prefix capture (prefix c/1/3 create+jump tabs), passthrough of freed Ctrl keys, and o/Space return-to-normal. Co-Authored-By: Claude Opus 4.8 --- nix/home-manager/zellij/config.kdl | 80 ++++++++++++++++++++++++++---- 1 file changed, 69 insertions(+), 11 deletions(-) diff --git a/nix/home-manager/zellij/config.kdl b/nix/home-manager/zellij/config.kdl index 7a4af77..d1fd2b1 100644 --- a/nix/home-manager/zellij/config.kdl +++ b/nix/home-manager/zellij/config.kdl @@ -1,20 +1,78 @@ -// Keybinds modeled on ~/.tmux.conf. Only the prefix and one chord differ from -// zellij's stock "tmux mode" (which already behaves like tmux); clear-defaults -// stays false so everything else is inherited. - -keybinds { - // tmux's prefix is `C-\` (.tmux.conf: `set -g prefix "C-\\"`), not zellij's - // `Ctrl b`. Rebind everywhere except inside tmux mode, so the same chord - // there can send a literal Ctrl-\ to the pane (below). - shared_except "locked" "scroll" "search" "tmux" { - unbind "Ctrl b" +// tmux-faithful keybinds. zellij's stock config binds many Ctrl/Alt chords +// GLOBALLY, stealing keys (Ctrl-p/n/f/t/h/s/o/…) the shell wants. tmux binds +// nothing globally except its prefix, so we clear EVERYTHING (clear-defaults=true) +// and rebind only the prefix globally; every other Ctrl/Alt key falls through to +// the running program, like tmux. +keybinds clear-defaults=true { + // The one and only global capture: tmux's prefix `C-\` (.tmux.conf: + // `set -g prefix "C-\\"`). Excluded inside tmux mode (so the same chord there + // is free to send-prefix) and inside the text-entry sub-modes scroll/renametab + // (so it doesn't yank you out of them mid-scroll / mid-rename). + shared_except "tmux" "scroll" "renametab" { bind "Ctrl \\" { SwitchToMode "tmux"; } } + // Post-prefix bindings, modeled on tmux's default prefix table. Single keys, + // since the prefix already gated them. One-shot actions return to normal like + // tmux's prefix; the two mode-entries (scroll via `[`, renametab via `,`) get + // escape binds below. (.tmux.conf only rebinds j/s to join-pane/send-pane + // prompts, which zellij has no equivalent for, so hjkl stay focus moves.) tmux { - // Prefix-then-prefix sends a literal Ctrl-\ (tmux's `bind "C-\\" + // prefix prefix -> literal Ctrl-\ to the pane (.tmux.conf `bind "C-\\" // send-prefix`); 28 = Ctrl-\. bind "Ctrl \\" { Write 28; SwitchToMode "normal"; } + + // splits: % = left/right divider, " = top/bottom (tmux's geometry) + bind "%" { NewPane "Right"; SwitchToMode "normal"; } + bind "\"" { NewPane "Down"; SwitchToMode "normal"; } + + // focus: arrows + hjkl + bind "Left" "h" { MoveFocus "Left"; SwitchToMode "normal"; } + bind "Right" "l" { MoveFocus "Right"; SwitchToMode "normal"; } + bind "Down" "j" { MoveFocus "Down"; SwitchToMode "normal"; } + bind "Up" "k" { MoveFocus "Up"; SwitchToMode "normal"; } + bind "o" { FocusNextPane; SwitchToMode "normal"; } + + // panes + bind "z" { ToggleFocusFullscreen; SwitchToMode "normal"; } + bind "x" { CloseFocus; SwitchToMode "normal"; } + bind "Space" { NextSwapLayout; SwitchToMode "normal"; } // tmux next-layout + + // tabs (tmux "windows"): new / next / prev / close, and 1-9 jump + bind "c" { NewTab; SwitchToMode "normal"; } + bind "n" { GoToNextTab; SwitchToMode "normal"; } + bind "p" { GoToPreviousTab; SwitchToMode "normal"; } + bind "&" { CloseTab; SwitchToMode "normal"; } // tmux kill-window + bind "1" { GoToTab 1; SwitchToMode "normal"; } + bind "2" { GoToTab 2; SwitchToMode "normal"; } + bind "3" { GoToTab 3; SwitchToMode "normal"; } + bind "4" { GoToTab 4; SwitchToMode "normal"; } + bind "5" { GoToTab 5; SwitchToMode "normal"; } + bind "6" { GoToTab 6; SwitchToMode "normal"; } + bind "7" { GoToTab 7; SwitchToMode "normal"; } + bind "8" { GoToTab 8; SwitchToMode "normal"; } + bind "9" { GoToTab 9; SwitchToMode "normal"; } + + bind "d" { Detach; } + bind "," { SwitchToMode "renametab"; TabNameInput 0; } // tmux rename-window + bind "[" { SwitchToMode "scroll"; } // tmux copy-mode + } + + // Escapes for the two sub-modes tmux mode can enter. clear-defaults wiped their + // stock binds, so without these you'd enter the mode and be stuck with no key + // back to the shell. + scroll { + bind "Esc" "q" { SwitchToMode "normal"; } + bind "Ctrl f" "PageDown" { PageScrollDown; } + bind "Ctrl b" "PageUp" { PageScrollUp; } + bind "j" "Down" { ScrollDown; } + bind "k" "Up" { ScrollUp; } + bind "g" { ScrollToTop; } + bind "G" { ScrollToBottom; SwitchToMode "normal"; } + } + renametab { + bind "Enter" { SwitchToMode "normal"; } + bind "Esc" { UndoRenameTab; SwitchToMode "normal"; } } } From c41759f61b17aff0eec488f196c944ac7c9b333e Mon Sep 17 00:00:00 2001 From: bddap-bot Date: Mon, 22 Jun 2026 11:11:32 -0700 Subject: [PATCH 2/3] zellij: let Esc / Ctrl-c abort the tmux-mode prefix clear-defaults=true wiped zellij's stock mode escapes, and an unbound key in tmux mode is swallowed (you stay in-mode). So a stray prefix `Ctrl \` left you in tmux mode able to leave only by triggering one of the action keys. tmux drops back to normal on Esc / C-c after a stray prefix; this matches that. Found in the pre-merge review of #21. Config parses (`zellij setup --check`). Co-Authored-By: Claude Opus 4.8 --- nix/home-manager/zellij/config.kdl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/nix/home-manager/zellij/config.kdl b/nix/home-manager/zellij/config.kdl index d1fd2b1..b06c4ac 100644 --- a/nix/home-manager/zellij/config.kdl +++ b/nix/home-manager/zellij/config.kdl @@ -22,6 +22,12 @@ keybinds clear-defaults=true { // send-prefix`); 28 = Ctrl-\. bind "Ctrl \\" { Write 28; SwitchToMode "normal"; } + // Abort the prefix. clear-defaults removed zellij's stock escapes, and an + // unbound key here is swallowed (stays in-mode), so without this an + // accidental prefix would strand you with only the action keys as a way + // out. tmux drops to normal on Esc / C-c after a stray prefix; match it. + bind "Esc" "Ctrl c" { SwitchToMode "normal"; } + // splits: % = left/right divider, " = top/bottom (tmux's geometry) bind "%" { NewPane "Right"; SwitchToMode "normal"; } bind "\"" { NewPane "Down"; SwitchToMode "normal"; } From 2a6cfaedfee00def3ff7de541551f67465a45282 Mon Sep 17 00:00:00 2001 From: bddap-bot Date: Mon, 22 Jun 2026 11:11:42 -0700 Subject: [PATCH 3/3] alacritty: drop the inert ReceiveChar binding on Ctrl-Backslash (dead config) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This binding looked load-bearing for the `Ctrl \` story — as if it forced the legacy 0x1c byte and starved zellij of the kitty CSI-u sequence its prefix needs. It does not. Alacritty's keyboard handler makes a `ReceiveChar` binding a no-op for byte output (`*suppress_chars.get_or_insert(true) &= action != ReceiveChar` leaves it unsuppressed, i.e. identical to no binding), and the legacy-vs-CSI-u choice is taken purely from terminal keyboard mode, never from a binding (verified against alacritty src v0.13–v0.15/master). So it neither blocked zellij's prefix nor was needed for tmux (Ctrl-\ produces 0x1c natively). Removed as cruft — no behavior change either way. The actual `Ctrl \`-in-both mechanism lives in the zellij keybinds + zellij's default `support_kitty_keyboard_protocol true`, not here. NOT live-tested (no headless terminal); the no-op claim is from Alacritty source. Co-Authored-By: Claude Opus 4.8 --- home/.config/alacritty/alacritty.toml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/home/.config/alacritty/alacritty.toml b/home/.config/alacritty/alacritty.toml index 8984419..10dd36f 100644 --- a/home/.config/alacritty/alacritty.toml +++ b/home/.config/alacritty/alacritty.toml @@ -14,10 +14,9 @@ style = "Italic" family = "Comic Mono" style = "Regular" -[[keyboard.bindings]] -action = "ReceiveChar" -key = "Backslash" -mods = "Control" +# No Ctrl-Backslash binding here, on purpose: a `ReceiveChar` binding is a no-op for +# byte output, and 0x1c-vs-kitty-CSI-u is chosen by terminal mode, not by bindings — +# so Ctrl-\ already reaches whatever's running (0x1c to tmux, CSI-u to zellij). [[keyboard.bindings]] action = "CreateNewWindow"