📡 Stage 1: Terminal Byte Decode
parse-keypress.ts converte sequencias de escape do terminal em objetos ParsedKey. Tres protocolos suportados:
Legacy VT sequences
Setas e function keys com info de modifier em parametros numericos (\x1b[1;5D = Ctrl+Left)
CSI u (Kitty protocol)
Formato "ESC [ codepoint ; modifier u" -- habilita combinacoes antes impossiveis como Shift+Enter
xterm modifyOtherKeys
Formato "ESC [ 27 ; modifier ; keycode ~" -- requer parsing explicito para evitar matches parciais
Modifier Bitmask (XTerm standard)
modifier = 1 + (shift?1:0) + (alt?2:0) + (ctrl?4:0) + (super?8:0)
📋 Stage 2: Default Bindings & Parsing
18 contextos suportados: Global, Chat, Autocomplete, Confirmation, Help, Transcript, HistorySearch, Task, ThemePicker, Settings, Tabs, Attachments, Footer, MessageSelector, DiffDialog, ModelPicker, Select, Plugin.
Bindings Notaveis (Chat context)
enter -- chat:submitescape -- chat:cancelctrl+x ctrl+k -- chat:killAgents (chord)ctrl+s -- chat:stashctrl+v / alt+v -- chat:imagePaste💡 Platform-aware dynamic keys
Windows usa alt+v para image paste porque Ctrl+V e system paste. Shift+Tab depende do modo VT do terminal (Node.js 24.2.0+ / 22.17.0+).
🔍 Stage 3: Key Matching
Alt/Meta Unification
Terminais legacy nao distinguem Alt de Meta -- ambos setam key.meta = true. O matcher aceita se alt OU meta for requerido.
Escape Special Case
Pressionar Escape envia \x1b, que Ink interpreta como key.meta = true. O codigo strip meta explicitamente ao fazer match de escape keys.
🎵 Stage 4: Chord Resolution
O resolver retorna um de cinco resultados:
flowchart TD
A["Key event arrives"] --> B["Build testChord\n(pending + currentKeystroke)"]
B --> C{"Escape key\n+ pending?"}
C -->|"Yes"| CANCEL["chord_cancelled"]
C -->|"No"| D{"Any live longer\nchord prefix?"}
D -->|"Yes"| WAIT["chord_started\n(store pending)"]
D -->|"No"| E{"Exact chord\nmatch?"}
E -->|"action = null"| UNBOUND["unbound\n(swallow event)"]
E -->|"action = string"| MATCH["match\n(fire action)"]
E -->|"No match"| F{"Was in chord?"}
F -->|"Yes"| CANCEL2["chord_cancelled"]
F -->|"No"| NONE["none\n(propagate)"]
⚛️ Stage 5: React Hooks & Context
Componentes registram interesse via useKeybinding() (acao unica) ou useKeybindings() (mapa de acoes).
False return convention
Um handler pode retornar false para sinalizar "nao consumido -- propagar adiante". ScrollKeybindingHandler usa isso para permitir que componentes filhos tratem wheel events quando o conteudo cabe na tela.
🔄 User Config & Hot-Reload
// ~/.claude/keybindings.json
{
"bindings": [{
"context": "Chat",
"bindings": {
"ctrl+y": "chat:submit",
"enter": null, // unbind Enter
"ctrl+shift+p": "command:compact"
}
}]
}
Merge strategy: User bindings append apos defaults. Scan linear com last-match-wins significa que entradas do usuario supersede defaults naturalmente.
Hot-reload: chokidar monitora o arquivo com delay de 500ms. Delecao do arquivo reseta para defaults.
🛡️ Validacao & Shortcuts Reservados
| Categoria | Keys | Severidade |
|---|---|---|
| Non-rebindable | ctrl+c, ctrl+d, ctrl+m | error |
| Terminal reserved | ctrl+z, ctrl+\ | warn/error |
| macOS only | cmd+c/v/x/q/w/tab/space | error |
💡 ctrl+s NAO e reservado
Flow control esta desabilitado em terminais modernos, e Claude Code usa ctrl+s para stashing.
🗺️ Pipeline Completo
flowchart LR
subgraph Terminal
B1["Raw bytes\n\\x1b[13;2u"]
end
subgraph Parse["parse-keypress.ts"]
B2["CSI-u / VT / SGR\ndecodeModifier()"]
B3["ParsedKey\n{name:'enter', shift:true}"]
B2 --> B3
end
subgraph Config["Binding Config"]
C1["defaultBindings.ts"]
C2["~/.claude/keybindings.json"]
C3["parseBindings()"]
C1 --> C3
C2 --> C3
end
subgraph Resolve["resolver.ts"]
R1["resolveKeyWithChordState()"]
R2["chord prefix check"]
R3["exact match\nlast-wins scan"]
R1 --> R2 --> R3
end
subgraph React["useKeybinding.ts"]
H1["ChordResolveResult"]
H2["handler()"]
H1 --> H2
end
Terminal --> Parse
Parse --> Resolve
Config --> Resolve
Resolve --> React