Skip to content

Tastatur-Parsing: kitty-Protokoll

源码版本v0.73.1

In @mariozechner/pi-tui übersetzen keys.ts (ca. 1400 Zeilen) und stdin-buffer.ts (411 Zeilen) den Byte-Stream aus dem Terminal-stdin in strukturierte key ids. Das läuft in zwei getrennten Schritten: StdinBuffer schneidet den Byte-Stream, der in Teilstücken ankommen kann, an vollständigen Escape-Sequenzen auseinander; parseKey erkennt die fertige Sequenz und wandelt sie in einen key id wie "ctrl+c" / "shift+enter" / "up" um. Das kitty keyboard protocol ist der Kern: es unterstützt key release/repeat, shifted-Varianten und baseLayoutKey für nicht-lateinische Layouts.

Zuständigkeiten

keys.ts + stdin-buffer.ts machen drei Dinge:

  1. Sequenz-Splitting: StdinBuffer.process sammelt stdin und schneidet an CSI/OSC/DCS/APC-Vollständigkeit; unvollständige Sequenzen bleiben im Buffer und warten auf den nächsten Aufruf. Siehe packages/tui/src/stdin-buffer.ts:251-312.
  2. key id-Parsing: parseKey(data) probiert zuerst kitty CSI-u, dann modifyOtherKeys, und fällt zurück auf legacy-Sequenzen. Siehe packages/tui/src/keys.ts:1251-1326.
  3. release/repeat-Erkennung: isKeyRelease/isKeyRepeat nutzt das kitty event-type-Feld (:2/:3); Komponenten entscheiden über wantsKeyRelease, ob sie release überhaupt bekommen wollen. Siehe packages/tui/src/keys.ts:505-577.

Designmotivation

Warum braucht man StdinBuffer? Weil die Grenzen der data-Ereignisse des stdin nicht mit den Grenzen der Escape-Sequenzen zusammenfallen. Eine SGR-Maussequenz \x1b[<35;20;5M kann in drei Teilen ankommen: \x1b, [<35, ;20;5M. Gibt man das direkt an parseKey, wird das erste \x1b als ESC interpretiert und der Rest [<35 als normales Zeichen eingefügt. StdinBuffer nutzt isCompleteSequence, um zu prüfen, ob der aktuelle Buffer eine vollständige Sequenz ist; falls nicht, wird der nächste process-Aufruf abgewartet. Ein 10ms-Timeout fängt verwaiste ESC ab, die sonst nie ein Ende finden und die Eingabe blockieren.

Warum hat kitty Vorrang? Weil legacy-Sequenzen begrenzt ausdrucksstark sind — ctrl+a und A sind in ASCII dasselbe Byte (0x01 vs 0x41), shift+enter hat in legacy gar keine Sequenz. kitty CSI-u kodiert explizit codepoint, modifier, event type, shifted-Version und base-layout key als \x1b[<codepoint>;<mod>:<event>u und kann jede Kombination unterscheiden. Nach setKittyProtocolActive(true) interpretiert parseKey \x1b\r als shift+enter (kitty mapping) statt als legacy alt+enter.

Wichtige Dateien

isKeyRelease entscheidet anhand der Suffixe :3u/:3~ über release-Ereignisse; Inhalt eines bracketed paste wird auch bei :3F nicht als release gewertet:

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 hat drei Fallback-Stufen: kitty CSI-u zuerst, dann modifyOtherKeys, dann legacy-Tabelle:

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 sammelt den Buffer; bracketed paste geht gesondert in den pasteBuffer, nicht-paste über 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;
}

Datenfluss

stdin → StdinBuffer → vollständige Sequenz → parseKey → key id → Komponenten-handleInput:

Randbedingungen und Fehler

  • Pseudo-release/release-Sequenzen innerhalb von bracketed paste: Eine Bluetooth-MAC 90:62:3F:A5 enthält :3F und darf beim Einfügen nicht als key release gewertet werden. isKeyRelease/isKeyRepeat prüfen am Anfang auf \x1b[200~ und geben direkt false zurück. Siehe packages/tui/src/keys.ts:528-534.
  • Verwaister ESC-Timeout: Ein einzelnes \x1b kann entweder ESC-Taste oder der Anfang einer Sequenz sein; StdinBuffer wartet 10ms auf Nachfolge-Daten, bevor es als ESC interpretiert wird. Siehe packages/tui/src/stdin-buffer.ts:259-262.
  • Mehrdeutigkeit von \x08 im Windows Terminal: \x08 ist im Windows Terminal ctrl+backspace, in anderen Terminals backspace; isWindowsTerminalSession() unterscheidet. Siehe packages/tui/src/keys.ts:1287-1288.
  • legacy alt+letter: \x1b<letter> wird bei aktivem kitty nicht als alt+letter interpretiert (kitty nutzt dafür CSI-u), sondern nur im legacy-Modus als alt+${char} aufgelöst.
  • Einzelnes Byte > 127 wird zu ESC + (byte-128): Kompatibel mit dem High-Byte-alt-Mapping des alten parseKeypress. Siehe packages/tui/src/stdin-buffer.ts:274-283.

Zusammenfassung

keys.ts + stdin-buffer.ts sind die Parsing-Schicht der Terminaleingabe: StdinBuffer schneidet vollständige Sequenzen aus, parseKey erkennt den key id, das kitty-Protokoll liefert die höherwertigen Informationen release/repeat/shifted, legacy-Sequenzen sind der Fallback. Der aufgelöste key id wird ans handleInput der TUI-Klasse übergeben und landet letztlich beim handleInput des Editor-Komponente oder der Komponentenbibliothek.