鍵盤解析: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。