From macos-plugin
Debug dead TUI/terminal keybindings — terminal interception (kitty), layout-impossible chords, legacy key aliasing. Use when a configured key does nothing in a TUI, fzf, or shell app.
How this skill is triggered — by the user, by Claude, or both
Slash command
/macos-plugin:tui-keybinding-debugThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
| Use this skill when... | Use something else when... |
| Use this skill when... | Use something else when... |
|---|---|
| A configured key does nothing in a TUI (ratatui/crossterm/promkit app), fzf, or a shell picker — no error, no reaction | The app throws a visible error on the key — that's an app bug, debug the app |
| A keybind works on one terminal and is dead on another | You're setting up a new keybind and want conventions — this is for diagnosing a dead one |
An fzf/gh picker's ctrl-/ (or other punctuation-Ctrl) toggle does nothing on a non-US keyboard | The key is dead in the shell line editor itself — that's zsh/readline keymap territory, not terminal interception |
| You need to prove where a keystroke dies before touching code | General terminal/session recovery — see kitty-session-persistence |
When a TUI key "does nothing," the instinct is to debug the app — its keymap, event matching, focus model. That instinct is wrong about half the time. A keystroke passes through three layers before the app can match it, and it can die silently at any of them:
Ctrl+J == Enter), so the binding fires the wrong action.Diagnose the layer first. One debug run settles where the key dies in seconds, converting an open-ended app-side hunt into a targeted fix. Prefer a key the terminal forwards: bare Ctrl+<letter> is the only Ctrl-space that exists on every keyboard layout and passes through every terminal.
Diagnose this dead-keybinding failure by proving which of the three layers is eating the key. Work top-down — cheapest, most-common cause first.
Run these two kitty probes and press the dead key. (Adapt for other terminals — most have a key-debug mode; the reasoning is identical.)
kitty +kitten show_key -m kitty
Press the dead key. Nothing printed = the terminal ate it (layer 1 or 2). A clean CSI ... u / escape sequence printed = the key is reaching the app, so the bug is app-side (jump to Step 4).
kitty --debug-keyboard
Press the dead key and read the log. The smoking gun is a line like:
KeyPress matched action: <X>, handled as shortcut
handled as shortcut means the terminal claimed the key — confirm layer 1 and go to Step 2. If show_key printed nothing and the chord involves a shifted/AltGr character on your layout, suspect layer 2 (Step 3) before the terminal.
The terminal grabbed the key. Identify whether it's a user-config map (changeable) or a built-in default (usually not). Grep the kitty config for a map on the offending key:
grep -niE '^\s*map\s+' ~/.config/kitty/kitty.conf
Match the dead chord against this table of commonly-intercepted combos:
| Combo | Frequently grabbed by | Default or user config? |
|---|---|---|
Shift+Arrow | kitty move_window (user config); macOS text-selection (moveDownAndModifySelection:) | User config (kitty) — doubly spoken-for on macOS |
Ctrl+Shift+Arrow | kitty scrollback scroll | Built-in default — won't appear in kitty.conf |
Ctrl+Arrow | kitty neighboring_window / word-motion remaps | Often user-mapped |
Cmd+*, Ctrl+Shift+* | the terminal's entire shortcut namespace | Reserve for the terminal |
bare Ctrl+<letter> | passes through to the app (except flow-control Ctrl+S/Ctrl+Q, suspend Ctrl+Z) | The safe space for app keybinds |
Fix, in order of preference:
Ctrl+<letter> the app doesn't already use (e.g. switch_mode = ["Ctrl+T"]). This is the robust fix: it dodges interception everywhere.map shift+down no_op in kitty.conf. A built-in default (like Ctrl+Shift+Arrow scrollback) usually can't be freed cleanly without overriding it.Distinguishing default from user config matters: a user map is changeable; a built-in default is not (without an override that may cost you the terminal feature).
Canonical break (jnv on kitty): the JSON-viewer nav keys looked completely dead. Root cause was not jnv — kitty intercepted
shift+down/shift+up(mapped tomove_windowin the user'skitty.conf), so jnv'sswitch_modefocus-switch never fired. Two replacement guesses also collided:ctrl+shift+arrowis a kitty built-in scrollback default. The fix was bareCtrl+T— a key kitty forwards.
On non-US layouts the character itself needs a modifier, so a Ctrl-combo over it can never be typed — the terminal never emits it, and nothing intercepts it, it simply doesn't exist. The failure is extra-invisible because a header hint (^/ preview) reads as a truncated or garbled label rather than an impossible chord.
Finnish / Nordic ISO layout examples — each of these requires Shift/AltGr, so the bare Ctrl-combo is unproducible:
| Character | How it's typed on Finnish ISO | Ctrl-combo over it |
|---|---|---|
/ | Shift+7 | Ctrl+/ can never fire |
? | Shift++ | Ctrl+? dead |
\ | AltGr+< | Ctrl+\ dead |
Rule of thumb: punctuation Ctrl-combos (ctrl-/, ctrl-?, ctrl-\, ctrl-], ctrl-; …) are layout-dependent — treat them as unavailable when authoring binds meant to work across keyboards. The diagnosis is the same show_key run: if pressing the chord prints nothing and the character is shifted/AltGr on your layout, the layout is the culprit, not the terminal.
Fix: rebind to a bare Ctrl+<letter> — the only Ctrl-space that exists on every layout and passes through every terminal.
Canonical break (dotfiles): every
ghfzf picker boundctrl-/:toggle-preview; on a Finnish layout the preview toggle was dead in all of them because the keyboard has no bare/key. Fix: rebind to bareCtrl+T.
The key reached the app (Step 1's show_key printed a sequence), but legacy terminal encoding aliases distinct keys to the same byte, so a binding fires the wrong action or is unreachable:
| Bound key | Legacy byte | Actually fires |
|---|---|---|
Ctrl+J | 0x0A | Enter — a Ctrl+J binding triggers Enter |
Ctrl+I | 0x09 | Tab |
Ctrl+M | 0x0D | Enter |
Ctrl+H | (decodes to Char('h')+CONTROL in crossterm — not Backspace) | Ctrl+H |
Fix — negotiate the keyboard enhancement protocol so these disambiguate. Push KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES when supports_keyboard_enhancement() is true, and pop it on teardown:
use crossterm::event::{
KeyboardEnhancementFlags, PushKeyboardEnhancementFlags, PopKeyboardEnhancementFlags,
};
use crossterm::terminal::supports_keyboard_enhancement;
if supports_keyboard_enhancement().unwrap_or(false) {
execute!(stdout, PushKeyboardEnhancementFlags(
KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES,
))?;
}
// ... run the app ...
execute!(stdout, PopKeyboardEnhancementFlags)?; // on teardown
Caveat — normalize events first, or the protocol breaks exact matches. If the app matches keybinds by exact Event equality, enabling the protocol also makes release/repeat events and KeyEventState lock-bits (Caps/Num Lock) possible — both break exact matches. At the single event-read chokepoint:
Press events (ignore release/repeat).KeyEventState to NONE.DISAMBIGUATE_ESCAPE_CODES — not REPORT_EVENT_TYPES — to keep the stream close to legacy.| Context | Command |
|---|---|
| Does the app receive the key at all? | kitty +kitten show_key -m kitty (press key; nothing printed = terminal ate it) |
| Does the terminal claim the key? | kitty --debug-keyboard (grep the log for handled as shortcut) |
| List user key-maps in kitty config | grep -niE '^\s*map\s+' ~/.config/kitty/kitty.conf |
| Check for a map on a specific chord | grep -niE '^\s*map\s+ctrl\+t' ~/.config/kitty/kitty.conf |
| Query terminal capabilities | kitten query_terminal |
| Symptom | Likely layer | Fix |
|---|---|---|
show_key prints nothing; --debug-keyboard shows handled as shortcut | 1 — terminal interception | Rebind app to bare Ctrl+<letter>, or no_op the user map |
show_key prints nothing; chord is a shifted/AltGr punctuation on your layout | 2 — layout-impossible | Rebind to bare Ctrl+<letter> |
show_key prints a sequence; wrong action fires (Ctrl+J→Enter, Ctrl+I→Tab) | 3 — legacy aliasing | Enable DISAMBIGUATE_ESCAPE_CODES + normalize events |
show_key prints a clean sequence; app still doesn't react | app keymap/focus | Debug the app — the key is arriving |
kitty-session-persistence — kitty session snapshot/restore (sibling kitty skill)kitty-agent-interaction.md rule — the read-only kitty remote-control surface; this skill is its keyboard-interception debugging complement.npx claudepluginhub laurigates/claude-plugins --plugin macos-pluginProvides design patterns for terminal user interfaces: layout paradigms, keyboard navigation, visual systems, and TUI anti-pattern validation. Works with Ratatui, Ink, Textual, Bubbletea, or any TUI framework.
Debug interactive CLIs, REPLs, TUIs, watchers, and long-running terminal processes using tmux to preserve live state, send controlled input, and capture evidence. Use when commands hang, wait for input, or behave differently in a real terminal.
Automates interactive terminal programs (REPLs, debuggers, TUIs) via PTY. Start sessions, type text/keystrokes, wait for screen stability, snapshot output, and manage multiple sessions.