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

@ -13,6 +13,7 @@
- Fixed Antigravity reliability: endpoint cascade on 403/404, added autopush sandbox fallback, removed extra fingerprint headers ([#1830](https://github.com/badlogic/pi-mono/issues/1830)). - Fixed Antigravity reliability: endpoint cascade on 403/404, added autopush sandbox fallback, removed extra fingerprint headers ([#1830](https://github.com/badlogic/pi-mono/issues/1830)).
- Fixed `@mariozechner/pi-ai/oauth` extension imports in published installs by resolving the subpath directly from built `dist` files instead of package-root wrapper shims ([#1856](https://github.com/badlogic/pi-mono/issues/1856)). - Fixed `@mariozechner/pi-ai/oauth` extension imports in published installs by resolving the subpath directly from built `dist` files instead of package-root wrapper shims ([#1856](https://github.com/badlogic/pi-mono/issues/1856)).
- Fixed Gemini 3 multi-turn tool use losing structured context by using `skip_thought_signature_validator` sentinel for unsigned function calls instead of text fallback ([#1829](https://github.com/badlogic/pi-mono/issues/1829)). - Fixed Gemini 3 multi-turn tool use losing structured context by using `skip_thought_signature_validator` sentinel for unsigned function calls instead of text fallback ([#1829](https://github.com/badlogic/pi-mono/issues/1829)).
- Fixed model selector filter not accepting typed characters in VS Code 1.110+ due to missing Kitty CSI-u printable decoding in the `Input` component ([#1857](https://github.com/badlogic/pi-mono/issues/1857))
- Fixed editor/footer visibility drift during terminal resize by forcing full redraws when terminal width or height changes ([#1844](https://github.com/badlogic/pi-mono/pull/1844) by [@ghoulr](https://github.com/ghoulr)). - Fixed editor/footer visibility drift during terminal resize by forcing full redraws when terminal width or height changes ([#1844](https://github.com/badlogic/pi-mono/pull/1844) by [@ghoulr](https://github.com/ghoulr)).
- Fixed footer width truncation for wide Unicode text (session name, model, provider) to prevent TUI crashes from rendered lines exceeding terminal width ([#1833](https://github.com/badlogic/pi-mono/issues/1833)). - Fixed footer width truncation for wide Unicode text (session name, model, provider) to prevent TUI crashes from rendered lines exceeding terminal width ([#1833](https://github.com/badlogic/pi-mono/issues/1833)).
- Fixed Windows write preview background artifacts by normalizing CRLF content (`\r\n`) to LF for display rendering in tool output previews ([#1854](https://github.com/badlogic/pi-mono/issues/1854)). - Fixed Windows write preview background artifacts by normalizing CRLF content (`\r\n`) to LF for display rendering in tool output previews ([#1854](https://github.com/badlogic/pi-mono/issues/1854)).

View file

@ -2,8 +2,13 @@
## [Unreleased] ## [Unreleased]
### Added
- Exported `decodeKittyPrintable()` from `keys.ts` for decoding Kitty CSI-u sequences into printable characters
### Fixed ### Fixed
- Fixed `Input` component not accepting typed characters when Kitty keyboard protocol is active (e.g., VS Code 1.110+), causing model selector filter to ignore keystrokes ([#1857](https://github.com/badlogic/pi-mono/issues/1857))
- Fixed editor/footer visibility drift during terminal resize by forcing full redraws when terminal width or height changes ([#1844](https://github.com/badlogic/pi-mono/pull/1844) by [@ghoulr](https://github.com/ghoulr)). - Fixed editor/footer visibility drift during terminal resize by forcing full redraws when terminal width or height changes ([#1844](https://github.com/badlogic/pi-mono/pull/1844) by [@ghoulr](https://github.com/ghoulr)).
## [0.56.1] - 2026-03-05 ## [0.56.1] - 2026-03-05

View file

@ -1,6 +1,6 @@
import type { AutocompleteProvider, CombinedAutocompleteProvider } from "../autocomplete.js"; import type { AutocompleteProvider, CombinedAutocompleteProvider } from "../autocomplete.js";
import { getEditorKeybindings } from "../keybindings.js"; import { getEditorKeybindings } from "../keybindings.js";
import { matchesKey } from "../keys.js"; import { decodeKittyPrintable, matchesKey } from "../keys.js";
import { KillRing } from "../kill-ring.js"; import { KillRing } from "../kill-ring.js";
import { type Component, CURSOR_MARKER, type Focusable, type TUI } from "../tui.js"; import { type Component, CURSOR_MARKER, type Focusable, type TUI } from "../tui.js";
import { UndoStack } from "../undo-stack.js"; import { UndoStack } from "../undo-stack.js";
@ -92,48 +92,6 @@ export function wordWrapLine(line: string, maxWidth: number): TextChunk[] {
} }
// Kitty CSI-u sequences for printable keys, including optional shifted/base codepoints. // Kitty CSI-u sequences for printable keys, including optional shifted/base codepoints.
const KITTY_CSI_U_REGEX = /^\x1b\[(\d+)(?::(\d*))?(?::(\d+))?(?:;(\d+))?(?::(\d+))?u$/;
const KITTY_MOD_SHIFT = 1;
const KITTY_MOD_ALT = 2;
const KITTY_MOD_CTRL = 4;
const KITTY_LOCK_MASK = 64 + 128; // Caps Lock + Num Lock
const KITTY_ALLOWED_MODIFIERS = KITTY_MOD_SHIFT | KITTY_LOCK_MASK;
// Decode a printable CSI-u sequence, preferring the shifted key when present.
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>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_ALLOWED_MODIFIERS) !== 0) return undefined;
if (modifier & (KITTY_MOD_ALT | KITTY_MOD_CTRL)) return undefined;
// Prefer the shifted keycode when Shift is held.
let effectiveCodepoint = codepoint;
if (modifier & KITTY_MOD_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;
}
}
interface EditorState { interface EditorState {
lines: string[]; lines: string[];
cursorLine: number; cursorLine: number;

View file

@ -1,4 +1,5 @@
import { getEditorKeybindings } from "../keybindings.js"; import { getEditorKeybindings } from "../keybindings.js";
import { decodeKittyPrintable } from "../keys.js";
import { KillRing } from "../kill-ring.js"; import { KillRing } from "../kill-ring.js";
import { type Component, CURSOR_MARKER, type Focusable } from "../tui.js"; import { type Component, CURSOR_MARKER, type Focusable } from "../tui.js";
import { UndoStack } from "../undo-stack.js"; import { UndoStack } from "../undo-stack.js";
@ -187,6 +188,16 @@ export class Input implements Component, Focusable {
return; return;
} }
// Kitty CSI-u printable character (e.g. \x1b[97u for 'a').
// Terminals with Kitty protocol flag 1 (disambiguate) send CSI-u for all keys,
// including plain printable characters. Decode before the control-char check
// since CSI-u sequences contain \x1b which would be rejected.
const kittyPrintable = decodeKittyPrintable(data);
if (kittyPrintable !== undefined) {
this.insertCharacter(kittyPrintable);
return;
}
// Regular character input - accept printable characters including Unicode, // Regular character input - accept printable characters including Unicode,
// but reject control characters (C0: 0x00-0x1F, DEL: 0x7F, C1: 0x80-0x9F) // but reject control characters (C0: 0x00-0x1F, DEL: 0x7F, C1: 0x80-0x9F)
const hasControlChars = [...data].some((ch) => { const hasControlChars = [...data].some((ch) => {

View file

@ -35,6 +35,7 @@ export {
} from "./keybindings.js"; } from "./keybindings.js";
// Keyboard input handling // Keyboard input handling
export { export {
decodeKittyPrintable,
isKeyRelease, isKeyRelease,
isKeyRepeat, isKeyRepeat,
isKittyProtocolActive, isKittyProtocolActive,

View file

@ -1152,3 +1152,58 @@ export function parseKey(data: string): string | undefined {
return 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;
}
}