fix(tui): remove Kitty protocol query timeout

The 100ms timeout was causing Kitty protocol detection to fail when the
terminal response was delayed (e.g., due to event loop blocking during
startup). This resulted in shift+enter not working in some scenarios.

Changes:
- Remove timeout-based Kitty detection, process input immediately
- Detect Kitty response in stdinBuffer output (handles split data)
- Add modifyOtherKeys fallback for terminals without Kitty support
  (matches xterm format \x1b[27;modifier;keycode~)
This commit is contained in:
Mario Zechner 2026-01-11 22:51:30 +01:00
parent f1e225d9e7
commit c07126c0fd
2 changed files with 48 additions and 73 deletions

View file

@ -437,6 +437,21 @@ function matchesKittySequence(data: string, expectedCodepoint: number, expectedM
return parsed.codepoint === expectedCodepoint && actualMod === expectedMod;
}
/**
* Match xterm modifyOtherKeys format: CSI 27 ; modifiers ; keycode ~
* This is used by terminals when Kitty protocol is not enabled.
* Modifier values are 1-indexed: 2=shift, 3=alt, 5=ctrl, etc.
*/
function matchesModifyOtherKeys(data: string, expectedKeycode: number, expectedModifier: number): boolean {
const match = data.match(/^\x1b\[27;(\d+);(\d+)~$/);
if (!match) return false;
const modValue = parseInt(match[1]!, 10);
const keycode = parseInt(match[2]!, 10);
// Convert from 1-indexed xterm format to our 0-indexed format
const actualMod = modValue - 1;
return keycode === expectedKeycode && actualMod === expectedModifier;
}
// =============================================================================
// Generic Key Matching
// =============================================================================
@ -515,6 +530,10 @@ export function matchesKey(data: string, keyId: KeyId): boolean {
) {
return true;
}
// xterm modifyOtherKeys format (fallback when Kitty protocol not enabled)
if (matchesModifyOtherKeys(data, CODEPOINTS.enter, MODIFIERS.shift)) {
return true;
}
// When Kitty protocol is active, legacy sequences are custom terminal mappings
// \x1b\r = Kitty's "map shift+enter send_text all \e\r"
// \n = Ghostty's "keybind = shift+enter=text:\n"
@ -531,6 +550,10 @@ export function matchesKey(data: string, keyId: KeyId): boolean {
) {
return true;
}
// xterm modifyOtherKeys format (fallback when Kitty protocol not enabled)
if (matchesModifyOtherKeys(data, CODEPOINTS.enter, MODIFIERS.alt)) {
return true;
}
// \x1b\r is alt+enter only in legacy mode (no Kitty protocol)
// When Kitty protocol is active, alt+enter comes as CSI u sequence
if (!_kittyProtocolActive) {

View file

@ -85,13 +85,34 @@ export class ProcessTerminal implements Terminal {
/**
* Set up StdinBuffer to split batched input into individual sequences.
* This ensures components receive single events, making matchesKey/isKeyRelease work correctly.
* Note: Does NOT register the stdin handler - that's done after the Kitty protocol query.
*
* Also watches for Kitty protocol response and enables it when detected.
* This is done here (after stdinBuffer parsing) rather than on raw stdin
* to handle the case where the response arrives split across multiple events.
*/
private setupStdinBuffer(): void {
this.stdinBuffer = new StdinBuffer({ timeout: 10 });
// Kitty protocol response pattern: \x1b[?<flags>u
const kittyResponsePattern = /^\x1b\[\?(\d+)u$/;
// Forward individual sequences to the input handler
this.stdinBuffer.on("data", (sequence) => {
// Check for Kitty protocol response (only if not already enabled)
if (!this._kittyProtocolActive) {
const match = sequence.match(kittyResponsePattern);
if (match) {
this._kittyProtocolActive = true;
setKittyProtocolActive(true);
// Enable Kitty keyboard protocol (push flags)
// Flag 1 = disambiguate escape codes
// Flag 2 = report event types (press/repeat/release)
process.stdout.write("\x1b[>3u");
return; // Don't forward protocol response to TUI
}
}
if (this.inputHandler) {
this.inputHandler(sequence);
}
@ -105,7 +126,6 @@ export class ProcessTerminal implements Terminal {
});
// Handler that pipes stdin data through the buffer
// Registration happens after Kitty protocol query completes
this.stdinDataHandler = (data: string) => {
this.stdinBuffer!.process(data);
};
@ -117,81 +137,13 @@ export class ProcessTerminal implements Terminal {
* Sends CSI ? u to query current flags. If terminal responds with CSI ? <flags> u,
* it supports the protocol and we enable it with CSI > 1 u.
*
* Non-supporting terminals won't respond, so we use a timeout.
* The response is detected in setupStdinBuffer's data handler, which properly
* handles the case where the response arrives split across multiple stdin events.
*/
private queryAndEnableKittyProtocol(): void {
const QUERY_TIMEOUT_MS = 100;
let resolved = false;
let buffer = "";
// Kitty protocol response pattern: \x1b[?<flags>u
const kittyResponsePattern = /\x1b\[\?(\d+)u/;
const queryHandler = (data: string) => {
if (resolved) {
// Query phase done, forward to StdinBuffer
if (this.stdinBuffer) {
this.stdinBuffer.process(data);
}
return;
}
buffer += data;
// Check if we have a Kitty protocol response
const match = buffer.match(kittyResponsePattern);
if (match) {
resolved = true;
this._kittyProtocolActive = true;
setKittyProtocolActive(true);
// Enable Kitty keyboard protocol (push flags)
// Flag 1 = disambiguate escape codes
// Flag 2 = report event types (press/repeat/release)
process.stdout.write("\x1b[>3u");
// Remove the response from buffer, forward any remaining input through StdinBuffer
const remaining = buffer.replace(kittyResponsePattern, "");
if (remaining && this.stdinBuffer) {
this.stdinBuffer.process(remaining);
}
// Replace query handler with StdinBuffer handler
process.stdin.removeListener("data", queryHandler);
if (this.stdinDataHandler) {
process.stdin.on("data", this.stdinDataHandler);
}
}
};
// Set up StdinBuffer before query (it will receive input after query completes)
this.setupStdinBuffer();
// Temporarily intercept input for the query (before StdinBuffer)
process.stdin.on("data", queryHandler);
// Send query
process.stdin.on("data", this.stdinDataHandler!);
process.stdout.write("\x1b[?u");
// Timeout: if no response, terminal doesn't support Kitty protocol
setTimeout(() => {
if (!resolved) {
resolved = true;
this._kittyProtocolActive = false;
setKittyProtocolActive(false);
// Forward any buffered input that wasn't a Kitty response through StdinBuffer
if (buffer && this.stdinBuffer) {
this.stdinBuffer.process(buffer);
}
// Replace query handler with StdinBuffer handler
process.stdin.removeListener("data", queryHandler);
if (this.stdinDataHandler) {
process.stdin.on("data", this.stdinDataHandler);
}
}
}, QUERY_TIMEOUT_MS);
}
stop(): void {