快捷键
从原始终端字节到键入的操作 - Claude Code 如何将击键转换为命令、支持和弦序列、加载用户覆盖以及保护保留的快捷方式。
在屏幕上发生任何事情之前,您在 Claude Code 中按下的每个键都会经过分层管道。 原始终端字节作为转义序列到达,被解码为
ParsedKey 对象,与上下文相关的绑定表进行匹配,最后分派到 React 处理程序。 整个系统是可配置的:用户可以覆盖默认绑定、添加和弦序列、绑定斜线命令或空解除绑定不需要的键——所有这些都通过一个 ~/.claude/keybindings.json file.
keybindings/defaultBindings.ts →
keybindings/parser.ts →
keybindings/match.ts →
keybindings/resolver.ts →
keybindings/useKeybinding.ts →
keybindings/KeybindingContext.tsx →
keybindings/loadUserBindings.ts →
keybindings/validate.ts →
keybindings/reservedShortcuts.ts →
ink/parse-keypress.ts
该管道有五个概念阶段:
终端解码
parse-keypress.ts — 转义序列、CSI u、kitty 协议、SGR 鼠标 → ParsedKey
绑定配置
defaultBindings.ts + loadUserBindings.ts — 上下文块合并,解析为 ParsedBinding[]
按键匹配
match.ts — 规范化修饰符,将 Ink 标志映射到 ParsedKeystroke 用于比较
和弦解析
resolver.ts — 单键和多步和弦、上下文优先、最后胜利覆盖
反应调度
useKeybinding.ts + KeybindingContext.tsx — 钩子消耗已解决的操作,停止传播
终端以转义序列说话,而不是按键名称。 当您按下 Ctrl+↑, pty 写道 \x1b[1;5A 进入进程标准输入。 的工作
ink/parse-keypress.ts 就是把字节汤变成结构化的
ParsedKey.
ParsedKey 类型
export type ParsedKey = {
kind: 'key'
fn: boolean // function key (F1–F12)
name: string | undefined // 'enter', 'escape', 'up', 'a', …
ctrl: boolean
meta: boolean // Alt/Option in legacy terminals
shift: boolean
option: boolean // iTerm "option as meta" quirk
super: boolean // Cmd/Win — only via kitty protocol
sequence: string | undefined
raw: string | undefined
isPasted: boolean
}
处理三种键盘协议
\x1b[1;5D = Ctrl+左键。
ESC [ codepoint ; modifier u。 携带 Unicode 代码点和位掩码修饰符。 实现以前不可能的组合,例如 Shift+Enter,
Ctrl+Space,以及 super (Cmd/Win) 修饰符。 由 kitty、WezTerm、ghostty、iTerm2 支持。
ESC [ 27 ; modifier ; keycode ~。 由 Ghostty/tmux/xterm 使用
modifyOtherKeys=2 是活跃的。 源代码包含明确的注释,解释它必须运行 before FN_KEY_RE 以避免部分匹配。
修饰符位掩码解码
所有三种现代协议共享相同的 XTerm 修饰符编码。 这 decodeModifier 函数于 parse-keypress.ts 解压它:
// modifier = 1 + (shift?1:0) + (alt?2:0) + (ctrl?4:0) + (super?8:0)
function decodeModifier(modifier: number) {
const m = modifier - 1
return {
shift: !!(m & 1),
meta: !!(m & 2),
ctrl: !!(m & 4),
super: !!(m & 8),
}
}
SGR鼠标事件
鼠标点击和拖动被解析为单独的 ParsedMouse 键入并且永远不会到达键绑定系统。 轮事件(位 0x40 在按钮代码中)故意保留为 ParsedKey 所以滚动绑定(scroll:pageUp, scroll:lineUp等)通过正常的解析器路径工作。
粘贴包围
解析器跟踪 PASTE_START / PASTE_END CSI 序列。 括号内的文本被收集在缓冲区中并作为单个文本发出
isPasted: true 钥匙。 即使是空粘贴也会发出一个键,以便下游处理程序可以检测 macOS 上的剪贴板图像粘贴尝试。
keybindings/defaultBindings.ts 导出单个常数,
DEFAULT_BINDINGS,这是一个数组 KeybindingBlock 对象。 每个块按 UI 上下文对绑定进行分组。
KeybindingBlock 结构
type KeybindingBlock = {
context: KeybindingContextName // 'Global' | 'Chat' | 'Autocomplete' | …
bindings: Record<string, string | null> // key string → action ID (null = unbind)
}
所有 18 个键绑定上下文
| Context | 活跃时 |
|---|---|
| Global | 始终活跃 |
| Chat | 聊天输入集中 |
| Autocomplete | 自动完成菜单可见 |
| Confirmation | 显示许可/确认对话框 |
| Help | 帮助覆盖打开 |
| Transcript | 查看成绩单 |
| HistorySearch | Ctrl+R 历史搜索 |
| Task | Agent/bash 任务在前台运行 |
| ThemePicker | 主题选择器打开 |
| Settings | 设置菜单打开 |
| Tabs | 选项卡导航处于活动状态 |
| Attachments | 图像附件选择对话框 |
| Footer | 页脚指示器聚焦 |
| MessageSelector | 快退消息选择器打开 |
| DiffDialog | 差异对话框打开 |
| ModelPicker | 模型选择器打开 |
| Select | 选择/列出重点组件 |
| Plugin | 插件对话框打开 |
选定的默认绑定 - 聊天上下文
| Key | Action | Note |
|---|---|---|
| enter | chat:submit | 发送消息 |
| escape | chat:cancel | 取消串流 |
| shift+tab | chat:cycleMode | Windows 后备: meta+m |
| ctrl+x ctrl+k | chat:killAgents | Chord — 避免遮蔽 readline |
| ctrl+x ctrl+e | chat:externalEditor | Readline-本机编辑绑定 |
| ctrl+_ | chat:undo | 旧终端 ctrl+shift+- |
| ctrl+shift+- | chat:undo | Kitty协议版本 |
| ctrl+s | chat:stash | 隐藏当前草稿 |
| ctrl+v / alt+v | chat:imagePaste | 特定于平台(Windows 使用 alt+v) |
平台感知动态键
默认文件不会为每个操作硬编码单个绑定。 根据运行时平台和运行时版本,在模块加载时计算两个密钥:
// Windows uses alt+v because ctrl+v is system paste
const IMAGE_PASTE_KEY = getPlatform() === 'windows' ? 'alt+v' : 'ctrl+v'
// shift+tab doesn't work on old Windows Terminal without VT mode
// Node enabled VT mode in 24.2.0 / 22.17.0; Bun in 1.2.23
const SUPPORTS_TERMINAL_VT_MODE =
getPlatform() !== 'windows' ||
(isRunningWithBun()
? satisfies(process.versions.bun, '>=1.2.23')
: satisfies(process.versions.node, '>=22.17.0 <23.0.0 || >=24.2.0'))
const MODE_CYCLE_KEY = SUPPORTS_TERMINAL_VT_MODE ? 'shift+tab' : 'meta+m'
feature('KAIROS'), feature('QUICK_SEARCH'),
feature('VOICE_MODE'), ETC。)。 如果标志关闭,则绑定不会出现在 DEFAULT_BINDINGS 根本不。
字符串 "ctrl+shift+k" 来自 JSON 配置块的数据不能直接与 ParsedKey. parser.ts 将其转换为结构化的 ParsedKeystroke (以及和弦的数组)。
ParsedKeystroke
type ParsedKeystroke = {
key: string // canonical name: 'escape', 'enter', 'up', 'k', …
ctrl: boolean
alt: boolean
shift: boolean
meta: boolean // alias for alt in legacy terminals
super: boolean // cmd / win
}
修饰符别名
parseKeystroke() 标准化一组丰富的修饰符名称,因此用户无需记住确切的拼写:
// All of these are equivalent:
"ctrl+k" // ctrl
"control+k" // also ctrl
"alt+k" // alt
"opt+k" // also alt
"option+k" // also alt
"cmd+k" // super
"command+k" // also super
"win+k" // also super
// Special key aliases:
"esc" → key: 'escape'
"return" → key: 'enter'
"space" → key: ' '
"↑" → key: 'up' // unicode arrow symbols work
和弦解析
和弦是按顺序键入的两个或多个击键,在配置字符串中用空格分隔。 parseChord() 在空白处分割并映射每个部分 parseKeystroke()。 明确处理的一种边缘情况:单独的字符串 " " (单个空格字符)是 空格键,不是空和弦。
parseChord("ctrl+x ctrl+k")
// → [{ key:'x', ctrl:true }, { key:'k', ctrl:true }]
parseChord(" ") // lone space = space key, not empty
// → [{ key:' ', ctrl:false, … }]
match.ts 桥梁墨水 Key 类型(布尔标志,例如
key.upArrow, key.escape)到 ParsedKeystroke
绑定表使用的格式。
替代/元统一怪癖
在传统终端编码中,Alt/Option 键与 Meta 修饰符无法区分 — 两者均设置 key.meta = true 在墨水中。 代码库通过明确的注释承认了这一点 match.ts:
// Alt and meta both map to key.meta in Ink (terminal limitation)
// So we check if EITHER alt OR meta is required in target
const targetNeedsMeta = target.alt || target.meta
if (inkMods.meta !== targetNeedsMeta) return false
逃脱/元怪癖
按 Escape 发送 \x1b。 按 Alt+字母发送 \x1b
接下来是这封信。 墨因此定 key.meta = true 关于所有逃脱事件——遗留文物。 无需特殊处理,简单
"escape" 绑定永远不会匹配,因为解析器还需要元修饰符。 修复方法是一行:
if (key.escape) {
return modifiersMatch({ ...inkMods, meta: false }, target)
}
resolver.ts 是系统的大脑。 给定 Ink 按键、活动上下文、所有解析的绑定以及可选的挂起和弦状态,它会返回五个结果之一:
| 结果类型 | Meaning |
|---|---|
| match | 绑定发射了。 action 字段携带动作ID。 |
| none | 没有匹配的绑定。 让事件传播。 |
| unbound | 键明确为空未绑定。 吞噬事件。 |
| chord_started | 此击键开始一个和弦。 店铺 pending 数组并等待。 |
| chord_cancelled | 和弦中止(转义或死胡同)。 清除挂起状态。 |
最后胜利覆盖模型
线性搜索绑定。 这 last 匹配绑定获胜。 因为用户绑定附加在默认值之后([...defaultBindings, ...userParsed]),用户条目自然会覆盖相同键+上下文对的默认值。
和弦前缀检测
在声明单键匹配之前,解析器会检查是否有任何
longer 活动上下文中的和弦使用此击键作为前缀。 如果是这样,则进入 chord_started 状态而不是触发单键绑定——即使存在。 只有当不再可能和弦时,它才会退回到精确匹配。
// chordWinners maps chord strings to their action (null = unbound override)
// This ensures null-unbinding a chord doesn't leave the prefix in chord-wait
const chordWinners = new Map<string, string | null>()
for (const binding of contextBindings) {
if (binding.chord.length > testChord.length &&
chordPrefixMatches(testChord, binding)) {
chordWinners.set(chordToString(binding.chord), binding.action)
}
}
// Only enter chord-wait if at least one live (non-null) longer chord exists
let hasLongerChords = false
for (const action of chordWinners.values()) {
if (action !== null) { hasLongerChords = true; break }
}
组件通过两个钩子注册对操作的兴趣: useKeybinding
对于单个动作,并且 useKeybindings 获取行动地图。 两者共享相同的解析路径。
useKeybinding
// Usage in a component:
useKeybinding('app:toggleTodos', () => {
setShowTodos(prev => !prev)
}, { context: 'Global' })
// useKeybindings handles multiple at once:
useKeybindings({
'chat:submit': () => handleSubmit(),
'chat:cancel': () => handleCancel(),
}, { context: 'Chat' })
错误返回约定
处理程序可以返回 false 表示“未消耗——让事件进一步传播”。 这是由 ScrollKeybindingHandler:当内容完全适合屏幕时,滚动是无操作的,因此滚动处理程序返回
false 子组件可以使用相同的滚轮事件进行列表导航。
KeybindingContext
React 上下文对象 (KeybindingContext.tsx)是共享总线。 它包含:
type KeybindingContextValue = {
resolve: (input, key, activeContexts) => ChordResolveResult
setPendingChord: (pending | null) => void
getDisplayText: (action, context) => string | undefined
bindings: ParsedBinding[]
pendingChord: ParsedKeystroke[] | null
activeContexts: Set<KeybindingContextName>
registerActiveContext: (context) => void
unregisterActiveContext: (context) => void
registerHandler: (registration) => () => void // returns cleanup fn
invokeAction: (action) => boolean
}
组件调用 registerActiveContext 在安装和
unregisterActiveContext 卸载时,解析器始终准确地知道哪些上下文处于活动状态。 这 invokeAction 方法由
ChordInterceptor 在和弦完成后触发已注册的处理程序,无需经过第二次 useInput call.
用户定制存在于 ~/.claude/keybindings.json.
loadUserBindings.ts 通过以下方式处理加载、缓存和实时重新加载 chokidar.
文件格式
{
"$schema": "https://www.schemastore.org/claude-code-keybindings.json",
"$docs": "https://code.claude.com/docs/en/keybindings",
"bindings": [
{
"context": "Chat",
"bindings": {
"ctrl+y": "chat:submit", // remap submit
"enter": null, // unbind enter from submit
"ctrl+shift+p": "command:compact" // bind to slash command
}
}
]
}
合并策略:最后的胜利
用户绑定是串联的 after 默认数组。 解析器线性扫描绑定并始终接受最后一个匹配项 - 因此用户条目自然会取代相同键和上下文的默认值。
热重载管道
isKeybindingCustomizationEnabled() 函数检查 GrowthBook 标志 tengu_keybinding_customization_release。 当为 false 时,将完全跳过用户配置加载和文件监视 - 仅使用默认绑定。 在撰写本文时,该功能正在逐步推出。
同步加载与异步加载
存在两种变体,因为 React 的 useState 初始化程序在第一次渲染期间同步运行:
// Called from React useState initializer — must be sync
loadKeybindingsSync(): ParsedBinding[]
// Called by the chokidar watcher on file changes — async is fine
loadKeybindings(): Promise<KeybindingsLoadResult>
validate.ts 对用户配置运行多次传递并生成类型
KeybindingWarning 具有严重程度的对象('error' or
'warning')和可选的 suggestion text.
五种警告类型
| Type | 触发器示例 |
|---|---|
| parse_error | Missing context 田野,空旷 + 在关键字符串中 |
| duplicate | 同一键在一个上下文块中列出两次 |
| reserved | 正在尝试绑定 ctrl+c or ctrl+z |
| invalid_context | 未知的上下文名称,例如 "Input" |
| invalid_action | Malformed command: 命令绑定的字符串或错误上下文 |
原始 JSON 中的重复键检测
JSON.parse 当某个键出现两次时,默默地使用最后一个值。
checkDuplicateKeysInJson() 使用正则表达式解析原始字符串,以在重复项默默丢失之前捕获此字符串:
// Regex locates each "bindings": { ... } block, then finds all "key": pairs
// Only warns on the second occurrence — first is already silently overwritten
const bindingsBlockPattern =
/"bindings"\s*:\s*\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}/g
预留快捷键
reservedShortcuts.ts 列出了三类受保护的密钥:
Ctrl+C、Ctrl+D、Ctrl+M
在 Claude Code 中硬编码。 ctrl+m 与所有终端中的 Enter 相同(均发送 CR)。
ctrl+z, ctrl+\
Unix SIGTSTP 和 SIGQUIT — 在进程看到它们之前被内核拦截。
cmd+c/v/x/q/w/tab/space
在终端应用程序之前被 macOS 拦截的操作系统级快捷方式。 仅在 macOS 上显示。
ctrl+s 对于隐藏功能和评论 reservedShortcuts.ts
明确记录将其排除在保留列表之外的决定。
平台感知的显示字符串
根据平台的不同,相同的绑定显示有所不同。 parser.ts
exports keystrokeToDisplayString():
// macOS: shows "opt" for alt modifier
keystrokeToDisplayString(ks, 'macos') // → "opt+k"
// Linux/Windows: shows "alt"
keystrokeToDisplayString(ks, 'linux') // → "alt+k"
// getBindingDisplayText() is used by KeybindingContext for the help UI
getBindingDisplayText('chat:submit', 'Chat', bindings)
// → "Enter" (searches in reverse so user overrides show instead of defaults)
模板生成器
Running /keybindings 在 Claude Code 中创建一个启动器
~/.claude/keybindings.json 通过致电 generateKeybindingsTemplate()
from template.ts。 它:
- 开始于
DEFAULT_BINDINGS - 过滤掉
NON_REBINDABLE键,以便模板通过/doctor - 将结果包装在
{ "$schema", "$docs", "bindings": [] }envelope
要点
- 键绑定堆栈有五个不同的层:终端解码、配置解析、键匹配、和弦解析和 React 调度。
parse-keypress.ts处理三种终端键盘协议:传统 VT 序列、CSI u (kitty) 和 xtermmodifyOtherKeys — 每个协议都需要不同的解析逻辑。- 默认绑定被分成上下文块(
Global,Chat,Autocomplete等),因此相同的键在不同的 UI 状态下可能意味着不同的东西。 - 两个绑定是在运行时计算的 -
IMAGE_PASTE_KEYandMODE_CYCLE_KEY— 解决 Windows 终端 VT 模式限制。 - 和弦序列如
ctrl+x ctrl+k是一流的:解析器构建一个挂起状态,并且仅在不再可能和弦时触发(尊重最后胜利+空覆盖)。 - 用户覆盖使用简单的追加和最后获胜策略;
null作为值显式解除绑定键。 chokidarwatcheskeybindings.json具有 500 毫秒的写入稳定延迟,并在每次保存时替换绑定缓存。- 三类受保护的键:不可重新绑定(硬编码)、终端保留 (SIGTSTP/SIGQUIT) 和 macOS 操作系统级快捷方式 — 每个都标记有适当的严重性。
检查你的理解情况
meta 修饰符,特别是当匹配的键是 escape?keybindings.json 文件?ctrl+s to null 在他们的 keybindings.json 中。 当他们按下 Ctrl+S 时会发生什么?