fix(tui): add Kitty CSI-u printable decoding to Input component, closes #1857

This commit is contained in:
Mario Zechner
2026-03-06 00:33:58 +01:00
parent 863135d429
commit 9bcf06c056
6 changed files with 74 additions and 43 deletions

View File

@@ -1152,3 +1152,58 @@ export function parseKey(data: string): string | undefined {
return undefined;
}
// =============================================================================
// Kitty CSI-u Printable Decoding
// =============================================================================
const KITTY_CSI_U_REGEX = /^\x1b\[(\d+)(?::(\d*))?(?::(\d+))?(?:;(\d+))?(?::(\d+))?u$/;
const KITTY_PRINTABLE_ALLOWED_MODIFIERS = MODIFIERS.shift | LOCK_MASK;
/**
* Decode a Kitty CSI-u sequence into a printable character, if applicable.
*
* When Kitty keyboard protocol flag 1 (disambiguate) is active, terminals send
* CSI-u sequences for all keys, including plain printable characters. This
* function extracts the printable character from such sequences.
*
* Only accepts plain or Shift-modified keys. Rejects Ctrl, Alt, and unsupported
* modifier combinations (those are handled by keybinding matching instead).
* Prefers the shifted keycode when Shift is held and a shifted key is reported.
*
* @param data - Raw input data from terminal
* @returns The printable character, or undefined if not a printable CSI-u sequence
*/
export function decodeKittyPrintable(data: string): string | undefined {
const match = data.match(KITTY_CSI_U_REGEX);
if (!match) return undefined;
// CSI-u groups: <codepoint>[:<shifted>[:<base>]];<mod>[:<event>]u
const codepoint = Number.parseInt(match[1] ?? "", 10);
if (!Number.isFinite(codepoint)) return undefined;
const shiftedKey = match[2] && match[2].length > 0 ? Number.parseInt(match[2], 10) : undefined;
const modValue = match[4] ? Number.parseInt(match[4], 10) : 1;
// Modifiers are 1-indexed in CSI-u; normalize to our bitmask.
const modifier = Number.isFinite(modValue) ? modValue - 1 : 0;
// Only accept printable CSI-u input for plain or Shift-modified text keys.
// Reject unsupported modifier bits (e.g. Super/Meta) to avoid inserting
// characters from modifier-only terminal events.
if ((modifier & ~KITTY_PRINTABLE_ALLOWED_MODIFIERS) !== 0) return undefined;
if (modifier & (MODIFIERS.alt | MODIFIERS.ctrl)) return undefined;
// Prefer the shifted keycode when Shift is held.
let effectiveCodepoint = codepoint;
if (modifier & MODIFIERS.shift && typeof shiftedKey === "number") {
effectiveCodepoint = shiftedKey;
}
// Drop control characters or invalid codepoints.
if (!Number.isFinite(effectiveCodepoint) || effectiveCodepoint < 32) return undefined;
try {
return String.fromCodePoint(effectiveCodepoint);
} catch {
return undefined;
}
}