Parseo de teclado: protocolo kitty
keys.ts (unas 1400 líneas) y stdin-buffer.ts (411 líneas) de @mariozechner/pi-tui traducen el byte stream de stdin de la terminal a un key id estructurado. Las dos cosas se separan: StdinBuffer corta el byte stream (que puede llegar fragmentado) por secuencias de escape completas, y parseKey reconoce la secuencia cortada como un key id tipo "ctrl+c" / "shift+enter" / "up". El kitty keyboard protocol es el núcleo: soporta release/repeat, versiones shifted y baseLayoutKey para layouts no latinos.
Responsabilidades
keys.ts + stdin-buffer.ts hacen tres cosas:
- Segmentación de secuencias:
StdinBuffer.processacumula stdin y corta por integridad CSI/OSC/DCS/APC; lo incompleto se queda en buffer para la próxima vez. Verpackages/tui/src/stdin-buffer.ts:251-312. - Parseo de key id:
parseKey(data)prioriza kitty CSI-u, luego modifyOtherKeys, y por último fallback a secuencias legacy. Verpackages/tui/src/keys.ts:1251-1326. - Detección release/repeat:
isKeyRelease/isKeyRepeatusan el campo event type de kitty (:2/:3) para detectar; los componentes deciden si lo reciben víawantsKeyRelease. Verpackages/tui/src/keys.ts:505-577.
Motivación de diseño
¿Por qué hace falta StdinBuffer? Porque los bordes del evento data de stdin no coinciden con los bordes de las secuencias de escape. Una secuencia SGR de mouse \x1b[<35;20;5M puede llegar en tres partes: \x1b, [<35, ;20;5M. Si se pasa directamente a parseKey, el primer \x1b se reconoce como ESC y el resto [<35 se inserta como caracteres. StdinBuffer usa isCompleteSequence para decidir si el buffer actual es una secuencia completa; si no, espera al siguiente process. Un timeout de 10ms evita que un ESC huérfano (sin continuación nunca) bloquee la entrada para siempre.
¿Por qué priorizar el protocolo kitty? Porque las secuencias legacy son limitadas: ctrl+a y A son el mismo byte en ASCII (0x01 vs 0x41); shift+enter no tiene secuencia legacy. kitty CSI-u codifica explícitamente codepoint, modifier, event type, versión shifted y base layout key, y distingue cualquier combinación. Tras setKittyProtocolActive(true), parseKey interpreta \x1b\r como shift+enter (mapeo kitty) en vez del alt+enter legacy.
Archivos clave
packages/tui/src/keys.ts:25-40— Estado global_kittyProtocolActive;ProcessTerminallo activa consetKittyProtocolActive(true)al detectar soporte kitty.packages/tui/src/keys.ts:505-585— TipoKeyEventType+isKeyRelease/isKeyRepeat/parseEventType.packages/tui/src/keys.ts:587-695—parseKittySequenceparsea\x1b[<cp>:<shifted>:<base>;<mod>:<event>uy devuelveParsedKittySequence.packages/tui/src/keys.ts:1332-1398—KITTY_CSI_U_REGEXydecodeKittyPrintable: con flag 1 activo, reconstruye la secuencia CSI-u a un char imprimible.packages/tui/src/keys.ts:1251-1326—parseKeyentrada principal: kitty → modifyOtherKeys → legacy, tres niveles de fallback.packages/tui/src/stdin-buffer.ts:29-150—isCompleteSequence+isCompleteCsi/Osc/Dcs/ApcSequence, validación de integridad.packages/tui/src/stdin-buffer.ts:192-250—extractCompleteSequencescorta secuencias completas del buffer y dejaremainderpara la próxima.packages/tui/src/stdin-buffer.ts:251-340—class StdinBuffer:process(data), ruta de bracketed paste, timeout 10ms.
isKeyRelease juzga release por sufijos :3u/:3~ etc.; el contenido de bracketed paste aunque incluya :3F no se trata como 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 tres niveles de fallback: kitty CSI-u primero, si no modifyOtherKeys, y al final tabla 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 acumula el buffer; bracketed paste va aparte a pasteBuffer, lo demás pasa por 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;
}Flujo de datos
stdin → StdinBuffer → secuencia completa → parseKey → key id → handleInput del componente:
Límites y fallos
- Secuencias de release/repeat falsas dentro de bracketed paste: una MAC Bluetooth
90:62:3F:A5contiene:3Fy al pegar no debería tratarse como release.isKeyRelease/isKeyRepeatcomprueban al inicio\x1b[200~y devuelven false. Verpackages/tui/src/keys.ts:528-534. - ESC huérfano con timeout: un
\x1bsolo puede ser la tecla ESC o el inicio de una secuencia;StdinBufferespera 10ms y si no llega nada más lo trata como ESC. Verpackages/tui/src/stdin-buffer.ts:259-262. - Ambigüedad de
\x08en Windows Terminal:\x08esctrl+backspaceen Windows Terminal ybackspaceen otras;isWindowsTerminalSession()lo distingue. Verpackages/tui/src/keys.ts:1287-1288. - alt+letter legacy:
\x1b<letter>con kitty activo no se interpreta comoalt+letter(kitty lo expresa con CSI-u), sólo se va poralt+${char}en modo legacy. - Byte suelto > 127 a ESC + (byte-128): compatible con el mapeo alt de high byte del viejo
parseKeypress. Verpackages/tui/src/stdin-buffer.ts:274-283.
Resumen
keys.ts + stdin-buffer.ts son la capa de parseo de entrada de terminal: StdinBuffer corta secuencias completas, parseKey reconoce el key id, el protocolo kitty aporta información rica (release/repeat/shifted), y las secuencias legacy son fallback. El key id se pasa al handleInput de la clase TUI, que al final enruta al handleInput del editor o de la biblioteca de componentes.