Topic | 键位系统(Keybinding System)全拆¶
重要性:⭐⭐⭐⭐(TUI 交互核心) 出现位置:
src/keybindings/、src/hooks/useGlobalKeybindings.tsx、src/hooks/useCommandKeybindings.tsx关联:phase-02-repl.md 的键位层、phase-04-components.md 的 Dialog isCancelActive
1. 为什么 TUI 需要自研键位系统¶
Web 项目有 mousetrap / react-hotkeys / react-shortcuts 等成熟库。
Claude Code 不用这些库,因为 TUI 键位有 3 个 Web 没有的特殊需求:
| 需求 | Web 库能解决? | TUI 特殊点 |
|---|---|---|
| chord 组合(先 Ctrl+K 再 Ctrl+S) | 部分支持 | TUI 用户期望 vim/emacs 风格 |
| context-aware(输入框里 Esc 是删字符,列表里 Esc 是取消) | 通过 scope 支持 | 嵌套组件多,scope 复杂 |
| 可发现性(快捷键能查到提示文案) | 部分支持 | TUI 没有 hover 提示 |
跨平台(macOS ⌘C vs Linux ^C) |
部分支持 | TUI 用户对平台差异敏感 |
结论:自研。
2. 目录结构¶
src/keybindings/
├── KeybindingProviderSetup.tsx 全局 Provider
├── useShortcutDisplay.ts 快捷键显示 hook
├── shortcutFormat.ts 跨平台格式化
├── useKeybinding.ts 单个 keybinding hook(推测)
├── useKeybindings.ts 多个 keybindings hook
└── <其他辅助>
加上 hooks/ 目录的 3 个相关 hook:
- src/hooks/useGlobalKeybindings.tsx
- src/hooks/useCommandKeybindings.tsx
- src/hooks/useCancelRequest.ts
3. 三层架构¶
┌────────────────────────────────────────────────────────┐
│ Layer 3: Ink useInput (兜底) │
│ src/ink/hooks/use-input.ts │
│ 处理单字符快捷键(Esc、g、G、j、k) │
└────────────────────────────────────────────────────────┘
↓ 透传
┌────────────────────────────────────────────────────────┐
│ Layer 2: Command Keybindings (命令模式快捷键) │
│ src/hooks/useCommandKeybindings.tsx │
│ 仅在用户输入 `/` 后激活 │
└────────────────────────────────────────────────────────┘
↓ 透传
┌────────────────────────────────────────────────────────┐
│ Layer 1: Global Keybindings (全局快捷键) │
│ src/hooks/useGlobalKeybindings.tsx │
│ Ctrl+C 取消、Ctrl+L 清屏、Ctrl+O 全屏 │
└────────────────────────────────────────────────────────┘
↓ 透传
┌────────────────────────────────────────────────────────┐
│ Layer 0: Component 内部 useInput (组件级) │
│ PromptInput / FuzzyPicker / Tabs │
│ 比如 Esc 在输入框删字符、在 List 是取消 │
└────────────────────────────────────────────────────────┘
关键:每一层都可以消费事件(不传给下层)或透传(让下层处理)。
4. KeybindingProvider¶
// src/keybindings/KeybindingProviderSetup.tsx 推测
<KeybindingSetup>
<GlobalKeybindingHandlers />
<CommandKeybindingHandlers />
<CancelRequestHandler />
<BackgroundTaskNavigation />
{children}
</KeybindingSetup>
职责:
- 把所有快捷键注册到全局(React Context 树)
- 提供 useKeybinding() / useKeybindings() 钩子让组件订阅
- 维护"已注册的快捷键列表"(用于可发现性提示)
5. useGlobalKeybindings.tsx¶
推测结构(基于 import 和 use 模式)
export function GlobalKeybindingHandlers() {
// 1. 注册 Ctrl+C → 取消当前请求
useKeybinding({
keys: 'ctrl+c',
description: 'Cancel current request',
handler: useCallback(() => {
cancelCurrentRequest()
}, []),
})
// 2. 注册 Ctrl+D → 退出
useKeybinding({
keys: 'ctrl+d',
description: 'Exit Claude Code',
handler: useCallback(() => {
if (isInputEmpty()) {
exit()
} else {
// 转发到输入框作为 delete-forward
}
}, []),
})
// 3. 注册 Ctrl+L → 清屏
useKeybinding({
keys: 'ctrl+l',
description: 'Clear screen',
handler: clearScreen,
})
// 4. 注册 Ctrl+O → 全屏切换
useKeybinding({
keys: 'ctrl+o',
description: 'Toggle fullscreen',
handler: toggleFullscreen,
})
// 5. 注册 Ctrl+R → 反向搜索历史
useKeybinding({
keys: 'ctrl+r',
description: 'Reverse search history',
handler: openHistorySearch,
})
return null // 不渲染任何东西
}
关键观察:
- 返回 null —— Provider 不渲染 UI
- 用 useCallback 包裹 handler —— 避免每次重渲染重新注册
- 每个 binding 独立 useKeybinding —— 不是数组循环,方便 HMR 单个修改
6. chord 组合实现¶
// 推测的 chord 实现
useKeybinding({
keys: ['ctrl+k', 'ctrl+s'], // 先 Ctrl+K 再 Ctrl+S
description: 'Save transcript',
handler: saveTranscript,
})
实现机制(推测): 1. 维护一个 "chord buffer"(最近 1.5 秒内的按键) 2. 收到 Ctrl+K → 标记等待下一个 3. 1.5 秒内收到 Ctrl/S → 触发 4. 超时或收到其他键 → 重置
前端类比:和 VSCode 的 keybindings.json chord 模式一样。
7. context-aware 键位¶
7.1 Dialog 的 isCancelActive¶
// src/components/design-system/Dialog.tsx:14
type DialogProps = {
isCancelActive?: boolean // 默认 true
// ...
}
// 在 Dialog 内部
const isCancelActive = props.isCancelActive ?? true
if (isCancelActive) {
useKeybinding({
keys: 'escape',
handler: props.onCancel,
})
useKeybinding({
keys: 'ctrl+c',
handler: props.onCancel,
})
}
场景:
- Dialog 包含一个 <TextInput> 嵌入输入
- 用户在 TextInput 里按 Esc 想删字符
- Dialog 不应该拦截 Esc
解决:
- 当 TextInput 聚焦时,Dialog 设 isCancelActive={false}
- TextInput 自己的 useInput 接收 Esc 删字符
- 用户离开 TextInput(按 Tab)→ Dialog 重新激活 Esc 取消
前端类比:和 Web 的 e.stopPropagation() 等价,但显式声明而不是命令式。
7.2 PromptInput 的 vim/normal 模式¶
// src/components/PromptInput/PromptInput.tsx 推测
function PromptInput({ mode, onChange }) {
if (mode === 'vim') {
return <VimInput onChange={onChange} />
}
return <NormalInput onChange={onChange} />
}
vim 模式(src/vim/)独立实现,键位系统全替换:
- normal 模式:h/j/k/l 移动、d 删除、y 复制
- insert 模式:i 进入、Esc 退出
- visual 模式:v 进入、选择文本
8. 跨平台格式化¶
// src/keybindings/shortcutFormat.ts 推测
export function getShortcutDisplay(
keys: string,
platform: 'darwin' | 'linux' | 'win32'
): string {
if (platform === 'darwin') {
return keys
.replace('ctrl', '⌘')
.replace('alt', '⌥')
.replace('shift', '⇧')
}
return keys // Linux/Windows 用 'Ctrl+C' 文字
}
实际显示:
- macOS:⌘C ⇧⌘P
- Linux/Windows:Ctrl+C Shift+Ctrl+P
// src/keybindings/useShortcutDisplay.ts
const platform = usePlatform() // 'darwin' | 'linux' | 'win32'
const display = useCallback(
(keys: string) => getShortcutDisplay(keys, platform),
[platform]
)
9. 关键洞察¶
9.1 三层架构 vs React Router scope¶
Web 的 react-router 也有"scope"概念(嵌套路由)。Claude Code 的三层和它同形:
- Global ≈ 最外层路由
- Command ≈ 当前命令的子路由
- Component ≈ 路由内的具体组件
9.2 Provider pattern 让"全局快捷键"测试友好¶
useKeybinding 注册到 React Context,测试时可以包不同 Provider 模拟不同键位环境。
9.3 chord buffer 的状态机化¶
chord 实现本质是两状态有限状态机(idle → waiting-first → idle / waiting-second → triggered)。
观察 useKeybinding 的实现是否用 useState/useReducer 维护 buffer 是个学习点。
9.4 "可发现性" 是 TUI 的核心 UX¶
TUI 没有 hover,用户必须能查到所有快捷键。useShortcutDisplay + KeyboardShortcutHint 组件的组合实现了"任何 dialog 都展示可用快捷键"。
10. 阅读清单¶
- ✅
src/keybindings/KeybindingProviderSetup.tsx(通读) - ✅
src/hooks/useGlobalKeybindings.tsx(通读) - ✅
src/keybindings/shortcutFormat.ts(通读,跨平台逻辑) - ✅
src/components/design-system/Dialog.tsx:14-30(isCancelActive的注释) - 📌
src/hooks/useCommandKeybindings.tsx(命令模式) - 📌
src/hooks/useCancelRequest.ts(Ctrl+C 取消流程) - 📌
src/keybindings/useKeybinding.ts(单 binding 注册的实现)
11. 练习任务¶
- 列出所有全局快捷键 —— 跑
grep -rnE "useKeybinding\(" src/,分类 - 手写一个 chord binding —— 实现"先 Ctrl+K 再 Ctrl+S 保存"的 React hook
- 写 macOS / Linux 兼容测试 —— 给
shortcutFormat加测试 - 思考:如果让你把 TUI 的键位系统移植到 Web(用
mousetrap替换底层),哪些 API 会变?哪些不变?