Parsing clavier : protocole kitty
keys.ts (environ 1400 lignes) et stdin-buffer.ts (411 lignes) de @mariozechner/pi-tui traduisent le flux d'octets stdin du terminal en key id structuré. Deux choses séparées : StdinBuffer découpe le flux d'octets — qui peut arriver en fragments — en séquences d'échappement complètes, et parseKey reconnaît les séquences ainsi découpées en key id comme "ctrl+c" / "shift+enter" / "up". Le kitty keyboard protocol est au cœur du dispositif : il supporte key release/repeat, versions shifted, et baseLayoutKey pour les dispositions non latines.
Responsabilités
keys.ts + stdin-buffer.ts font trois choses :
- Découpage des séquences :
StdinBuffer.processaccumule stdin, découpe selon la complétude CSI/OSC/DCS/APC, et conserve en buffer ce qui est incomplet jusqu'au prochain appel. Voirpackages/tui/src/stdin-buffer.ts:251-312. - Parsing du key id :
parseKey(data)privilégie kitty CSI-u, puis modifyOtherKeys, puis replie sur les séquences legacy. Voirpackages/tui/src/keys.ts:1251-1326. - Détection release/repeat :
isKeyRelease/isKeyRepeatutilisent le champ event type de kitty (:2/:3) ; les composants décident de les recevoir ou non viawantsKeyRelease. Voirpackages/tui/src/keys.ts:505-577.
Motifs de conception
Pourquoi un StdinBuffer ? Parce que les bornes des événements data de stdin ne coïncident pas avec les bornes des séquences d'échappement. Une séquence SGR de souris \x1b[<35;20;5M peut arriver en trois fois : \x1b, [<35, ;20;5M. Si on la passe telle quelle à parseKey, le premier \x1b est reconnu comme ESC, et le [<35 qui suit est inséré comme caractères ordinaires. StdinBuffer utilise isCompleteSequence pour déterminer si le buffer courant est une séquence complète, sinon il attend le prochain process. Un timeout de 10 ms sert de filet de sécurité pour éviter qu'un ESC orphelin, qui n'aura jamais de suite, ne bloque indéfiniment l'entrée.
Pourquoi privilégier le protocole kitty ? Parce que les séquences legacy sont peu expressives — ctrl+a et A partagent le même byte en ASCII (0x01 vs 0x41), et shift+enter n'a pas de séquence legacy du tout. kitty CSI-u encode explicitement \x1b[<codepoint>;<mod>:<event>u : codepoint, modifier, event type, version shifted, base layout key — toute combinaison est distinguishable. Après setKittyProtocolActive(true), parseKey interprète \x1b\r comme shift+enter (mapping kitty) plutôt que comme alt+enter en legacy.
Fichiers clés
packages/tui/src/keys.ts:25-40— état global_kittyProtocolActive;ProcessTerminalappellesetKittyProtocolActive(true)après avoir détecté le support kitty.packages/tui/src/keys.ts:505-585— typeKeyEventType+isKeyRelease/isKeyRepeat/parseEventType.packages/tui/src/keys.ts:587-695—parseKittySequenceparse\x1b[<cp>:<shifted>:<base>;<mod>:<event>uet renvoieParsedKittySequence.packages/tui/src/keys.ts:1332-1398—KITTY_CSI_U_REGEXetdecodeKittyPrintable: quand flag 1 active, restitue la séquence CSI-u en printable char.packages/tui/src/keys.ts:1251-1326— entrée principaleparseKey: repli kitty → modifyOtherKeys → legacy sur trois niveaux.packages/tui/src/stdin-buffer.ts:29-150—isCompleteSequence+isCompleteCsi/Osc/Dcs/ApcSequence, jugement de complétude.packages/tui/src/stdin-buffer.ts:192-250—extractCompleteSequencesextrait du buffer les séquences complètes, garderemainderpour la prochaine fois.packages/tui/src/stdin-buffer.ts:251-340—class StdinBuffer:process(data), chemin bracketed paste, timeout 10 ms.
isKeyRelease juge un événement release via les suffixes :3u/:3~ etc. ; le contenu d'un bracketed paste, même s'il contient :3F, n'est pas interprété comme 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 replie sur trois niveaux : kitty CSI-u d'abord, puis modifyOtherKeys en cas d'échec, puis table 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 accumule le buffer ; le bracketed paste passe par pasteBuffer séparément, le reste passe par 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;
}Flux de données
stdin → StdinBuffer → séquence complète → parseKey → key id → handleInput du composant :
Limites et cas d'échec
- Fausse séquence release/repeat dans un bracketed paste : une adresse MAC Bluetooth
90:62:3F:A5contient:3F, il ne faut pas la prendre pour un key release pendant un collage.isKeyRelease/isKeyRepeatvérifient le préfixe\x1b[200~en tête et retournent false. Voirpackages/tui/src/keys.ts:528-534. - Timeout d'ESC orphelin : un
\x1bisolé peut être soit la touche ESC, soit le début d'une séquence ;StdinBufferattend 10 ms sans suite avant de le traiter comme ESC. Voirpackages/tui/src/stdin-buffer.ts:259-262. - Ambiguïté du
\x08sous Windows Terminal :\x08estctrl+backspacesous Windows Terminal,backspaceailleurs ;isWindowsTerminalSession()fait la distinction. Voirpackages/tui/src/keys.ts:1287-1288. - alt+letter legacy :
\x1b<letter>n'est pas interprété commealt+letterquand kitty est actif (kitty l'exprime via CSI-u) ; c'est seulement en mode legacy qu'on produitalt+${char}. - Byte unique > 127 converti en ESC + (byte-128) : pour rester compatible avec le mapping alt des hauts bytes de l'ancien
parseKeypress. Voirpackages/tui/src/stdin-buffer.ts:274-283.
Synthèse
keys.ts + stdin-buffer.ts constituent la couche de parsing de l'entrée terminal : StdinBuffer découpe les séquences complètes, parseKey reconnaît le key id, le protocole kitty fournit les informations riches (release/repeat/shifted), et les séquences legacy servent de repli. Le key id obtenu est ensuite passé au handleInput de la classe TUI, qui route finalement vers le handleInput du composant éditeur ou de la bibliothèque de composants.