Skip to content

キーボード解析:kitty プロトコル

源码版本v0.73.1

@mariozechner/pi-tuikeys.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 は三つの仕事をする:

  1. シーケンス分割:StdinBuffer.process は stdin を蓄積し、CSI/OSC/DCS/APC の完全性で切り分ける。不完全なものは buffer に残して次回を待つ。packages/tui/src/stdin-buffer.ts:251-312 参照。
  2. key id 解析:parseKey(data) は kitty CSI-u を優先し、次に modifyOtherKeys、最後に legacy シーケンスにフォールバックする。packages/tui/src/keys.ts:1251-1326 参照。
  3. 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 は通常文字として挿入されてしまう。StdinBufferisCompleteSequence で現在の buffer が完全なシーケンスかを判定し、不完全なら次の process を待つ。10ms のタイムアウトで、末尾が永遠に来ない孤立 ESC が入力を塞ぐのを防ぐ。

なぜ kitty プロトコルを優先するのか?legacy シーケンスは表現力が限界だからだ——ctrl+aA は 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\rshift+enter(kitty mapping)として解釈し、legacy の alt+enter とはしない。

主要ファイル

isKeyRelease はサフィックス :3u/:3~ などで release イベントを判定する。bracketed paste 内容中に :3F が含まれていても release とみなさない:

typescript
// 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 表を引く:

typescript
// 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 に流す:

typescript
// 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 シーケンス:Bluetooth 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、他のターミナルでは backspaceisWindowsTerminalSession() で区別する。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 シーケンスはフォールバックとして機能する。解析された key id は TUI クラスhandleInput に渡され、最終的に エディタコンポーネントコンポーネントライブラリhandleInput にルーティングされる。