Tastatur-Parsing: kitty-Protokoll
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:
- Sequenz-Splitting:
StdinBuffer.processsammelt stdin und schneidet an CSI/OSC/DCS/APC-Vollständigkeit; unvollständige Sequenzen bleiben im Buffer und warten auf den nächsten Aufruf. Siehepackages/tui/src/stdin-buffer.ts:251-312. - key id-Parsing:
parseKey(data)probiert zuerst kitty CSI-u, dann modifyOtherKeys, und fällt zurück auf legacy-Sequenzen. Siehepackages/tui/src/keys.ts:1251-1326. - release/repeat-Erkennung:
isKeyRelease/isKeyRepeatnutzt das kitty event-type-Feld (:2/:3); Komponenten entscheiden überwantsKeyRelease, ob sie release überhaupt bekommen wollen. Siehepackages/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
packages/tui/src/keys.ts:25-40— globaler_kittyProtocolActive-Zustand;ProcessTerminalruftsetKittyProtocolActive(true)auf, wenn kitty-Unterstützung erkannt wird.packages/tui/src/keys.ts:505-585—KeyEventType-Typ +isKeyRelease/isKeyRepeat/parseEventType.packages/tui/src/keys.ts:587-695—parseKittySequenceparst\x1b[<cp>:<shifted>:<base>;<mod>:<event>uund liefertParsedKittySequence.packages/tui/src/keys.ts:1332-1398—KITTY_CSI_U_REGEXunddecodeKittyPrintable: Wenn Flag 1 aktiv ist, wird die CSI-u-Sequenz in ein printable char zurückübersetzt.packages/tui/src/keys.ts:1251-1326—parseKey-Haupteinstieg: kitty → modifyOtherKeys → legacy, dreistufiger Fallback.packages/tui/src/stdin-buffer.ts:29-150—isCompleteSequence+isCompleteCsi/Osc/Dcs/ApcSequence-Vollständigkeitsprüfung.packages/tui/src/stdin-buffer.ts:192-250—extractCompleteSequencesschneidet vollständige Sequenzen aus dem Buffer und lässt einenremainderfür den nächsten Aufruf.packages/tui/src/stdin-buffer.ts:251-340—class StdinBuffer:process(data), bracketed-paste-Pfad, 10ms-Timeout.
isKeyRelease entscheidet anhand der Suffixe :3u/:3~ über release-Ereignisse; Inhalt eines bracketed paste wird auch bei :3F nicht als release gewertet:
// 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:
// 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:
// 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:A5enthält:3Fund darf beim Einfügen nicht als key release gewertet werden.isKeyRelease/isKeyRepeatprüfen am Anfang auf\x1b[200~und geben direkt false zurück. Siehepackages/tui/src/keys.ts:528-534. - Verwaister ESC-Timeout: Ein einzelnes
\x1bkann entweder ESC-Taste oder der Anfang einer Sequenz sein;StdinBufferwartet 10ms auf Nachfolge-Daten, bevor es als ESC interpretiert wird. Siehepackages/tui/src/stdin-buffer.ts:259-262. - Mehrdeutigkeit von
\x08im Windows Terminal:\x08ist im Windows Terminalctrl+backspace, in anderen Terminalsbackspace;isWindowsTerminalSession()unterscheidet. Siehepackages/tui/src/keys.ts:1287-1288. - legacy alt+letter:
\x1b<letter>wird bei aktivem kitty nicht alsalt+letterinterpretiert (kitty nutzt dafür CSI-u), sondern nur im legacy-Modus alsalt+${char}aufgelöst. - Einzelnes Byte > 127 wird zu ESC + (byte-128): Kompatibel mit dem High-Byte-alt-Mapping des alten
parseKeypress. Siehepackages/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.