键盘解析:kitty 协议
@mariozechner/pi-tui 的 keys.ts(约 1400 行)和 stdin-buffer.ts(411 行)负责把终端 stdin 字节流翻译成结构化的 key id。两件事分开做:StdinBuffer 把可能分片到达的字节流按完整转义序列切开,parseKey 把切好的序列识别成 "ctrl+c" / "shift+enter" / "up" 这样的 key id。kitty keyboard protocol 是核心:支持 key release/repeat、shifted 版本、非拉丁布局的 baseLayoutKey。
职责
keys.ts + stdin-buffer.ts 做三件事:
- 序列切分:
StdinBuffer.process累积 stdin,按 CSI/OSC/DCS/APC 完整性切分,不完整的留 buffer 等下次。见packages/tui/src/stdin-buffer.ts:251-312。 - key id 解析:
parseKey(data)优先 kitty CSI-u,然后 modifyOtherKeys,再回退 legacy 序列。见packages/tui/src/keys.ts:1251-1326。 - release/repeat 检测:
isKeyRelease/isKeyRepeat用 kitty event type 字段(:2/:3)判断,组件通过wantsKeyRelease决定是否接收。见packages/tui/src/keys.ts:505-577。
设计动机
为什么需要 StdinBuffer?因为 stdin 的 data 事件边界和转义序列边界不一致。鼠标 SGR 序列 \x1b[<35;20;5M 可能分三次到达:\x1b、[<35、;20;5M。如果直接喂给 parseKey,第一个 \x1b 会被识别成 ESC,后面的 [<35 当成普通字符插入。StdinBuffer 用 isCompleteSequence 判断当前 buffer 是不是完整序列,不完整就等下一次 process。10ms 超时兜底防止永远等不到结尾的孤儿 ESC 卡死输入。
为什么优先 kitty 协议?因为 legacy 序列表达力有限——ctrl+a 和 A 在 ASCII 里是同一个字节(0x01 vs 0x41),shift+enter 在 legacy 下根本没序列。kitty CSI-u 用 \x1b[<codepoint>;<mod>:<event>u 显式编码 codepoint、modifier、event type、shifted 版本、base layout key,能区分任意组合。setKittyProtocolActive(true) 后 parseKey 会把 \x1b\r 解读成 shift+enter(kitty mapping)而非 legacy 的 alt+enter。
关键文件
packages/tui/src/keys.ts:25-40— 全局_kittyProtocolActive状态,ProcessTerminal检测到 kitty 支持后调setKittyProtocolActive(true)。packages/tui/src/keys.ts:505-585—KeyEventType类型 +isKeyRelease/isKeyRepeat/parseEventType。packages/tui/src/keys.ts:587-695—parseKittySequence解析\x1b[<cp>:<shifted>:<base>;<mod>:<event>u,返回ParsedKittySequence。packages/tui/src/keys.ts:1332-1398—KITTY_CSI_U_REGEX与decodeKittyPrintable:flag 1 active 时把 CSI-u 序列还原成 printable char。packages/tui/src/keys.ts:1251-1326—parseKey主入口:kitty → modifyOtherKeys → legacy 三级回退。packages/tui/src/stdin-buffer.ts:29-150—isCompleteSequence+isCompleteCsi/Osc/Dcs/ApcSequence完整性判断。packages/tui/src/stdin-buffer.ts:192-250—extractCompleteSequences从 buffer 切出完整序列,剩remainder留下次。packages/tui/src/stdin-buffer.ts:251-340—class StdinBuffer:process(data)、bracketed paste 路径、10ms 超时。
isKeyRelease 用后缀 :3u/:3~ 等判断 release 事件,bracketed paste 内容即使含 :3F 也不当 release:
// packages/tui/src/keys.ts:527-551
export function isKeyRelease(data: string): boolean {
if (data.includes("\x1b[200~")) {
return false;
}
if (
data.includes(":3u") ||
data.includes(":3~") ||
data.includes(":3A") ||
// ...
data.includes(":3F")
) {
return true;
}
return false;
}parseKey 三级回退:kitty CSI-u 优先,失败再试 modifyOtherKeys,最后查 legacy 表:
// packages/tui/src/keys.ts:1251-1271
export function parseKey(data: string): string | undefined {
const kitty = parseKittySequence(data);
if (kitty) {
return formatParsedKey(kitty.codepoint, kitty.modifier, kitty.baseLayoutKey);
}
const modifyOtherKeys = parseModifyOtherKeysSequence(data);
if (modifyOtherKeys) {
return formatParsedKey(modifyOtherKeys.codepoint, modifyOtherKeys.modifier);
}
// Mode-aware legacy sequences
if (_kittyProtocolActive) {
if (data === "\x1b\r" || data === "\n") return "shift+enter";
}StdinBuffer.process 累积 buffer,bracketed paste 单独走 pasteBuffer,非 paste 走 extractCompleteSequences:
// packages/tui/src/stdin-buffer.ts:290-312
this.buffer += str;
if (this.pasteMode) {
this.pasteBuffer += this.buffer;
this.buffer = "";
const endIndex = this.pasteBuffer.indexOf(BRACKETED_PASTE_END);
if (endIndex !== -1) {
const pastedContent = this.pasteBuffer.slice(0, endIndex);
// ...
this.emit("paste", pastedContent);
}
return;
}数据流
stdin → StdinBuffer → 完整序列 → parseKey → key id → 组件 handleInput:
边界与失败
- bracketed paste 内的伪 release/release 序列:蓝牙 MAC
90:62:3F:A5含:3F,粘贴时不能被当成 key release。isKeyRelease/isKeyRepeat开头检查\x1b[200~直接返回 false。见packages/tui/src/keys.ts:528-534。 - 孤儿 ESC 超时:单独一个
\x1b既可能是 ESC 键,也可能是序列开头,StdinBuffer等 10ms 没下文才当 ESC。见packages/tui/src/stdin-buffer.ts:259-262。 - Windows Terminal 的
\x08歧义:\x08在 Windows Terminal 是ctrl+backspace,其他终端是backspace,isWindowsTerminalSession()区分。见packages/tui/src/keys.ts:1287-1288。 - legacy alt+letter:
\x1b<letter>在 kitty active 时不解读成alt+letter(因为 kitty 用 CSI-u 表达),只有在 legacy 模式下才走alt+${char}。 - 单字节 > 127 转成 ESC + (byte-128):兼容老
parseKeypress的高字节 alt 映射。见packages/tui/src/stdin-buffer.ts:274-283。
小结
keys.ts + stdin-buffer.ts 是终端输入的解析层:StdinBuffer 切完整序列,parseKey 识别 key id,kitty 协议提供 release/repeat/shifted 等高维信息,legacy 序列做 fallback。解析出来的 key id 喂给 TUI 类 的 handleInput,最终路由到 编辑器组件 或 组件库 的 handleInput。