Skip to content

Parsing clavier : protocole kitty

源码版本v0.73.1

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 :

  1. Découpage des séquences : StdinBuffer.process accumule stdin, découpe selon la complétude CSI/OSC/DCS/APC, et conserve en buffer ce qui est incomplet jusqu'au prochain appel. Voir packages/tui/src/stdin-buffer.ts:251-312.
  2. Parsing du key id : parseKey(data) privilégie kitty CSI-u, puis modifyOtherKeys, puis replie sur les séquences legacy. Voir packages/tui/src/keys.ts:1251-1326.
  3. Détection release/repeat : isKeyRelease/isKeyRepeat utilisent le champ event type de kitty (:2/:3) ; les composants décident de les recevoir ou non via wantsKeyRelease. Voir packages/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

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 :

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 replie sur trois niveaux : kitty CSI-u d'abord, puis modifyOtherKeys en cas d'échec, puis table 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 accumule le buffer ; le bracketed paste passe par pasteBuffer séparément, le reste passe par 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;
}

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:A5 contient :3F, il ne faut pas la prendre pour un key release pendant un collage. isKeyRelease/isKeyRepeat vérifient le préfixe \x1b[200~ en tête et retournent false. Voir packages/tui/src/keys.ts:528-534.
  • Timeout d'ESC orphelin : un \x1b isolé peut être soit la touche ESC, soit le début d'une séquence ; StdinBuffer attend 10 ms sans suite avant de le traiter comme ESC. Voir packages/tui/src/stdin-buffer.ts:259-262.
  • Ambiguïté du \x08 sous Windows Terminal : \x08 est ctrl+backspace sous Windows Terminal, backspace ailleurs ; isWindowsTerminalSession() fait la distinction. Voir packages/tui/src/keys.ts:1287-1288.
  • alt+letter legacy : \x1b<letter> n'est pas interprété comme alt+letter quand kitty est actif (kitty l'exprime via CSI-u) ; c'est seulement en mode legacy qu'on produit alt+${char}.
  • Byte unique > 127 converti en ESC + (byte-128) : pour rester compatible avec le mapping alt des hauts bytes de l'ancien parseKeypress. Voir packages/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.