fix(tui): ignore unsupported Kitty CSI-u modifiers closes #1807

This commit is contained in:
Mario Zechner 2026-03-04 18:14:29 +01:00
parent 8e157412a6
commit 49749407fa
5 changed files with 25 additions and 1 deletions

View file

@ -5,6 +5,7 @@
### Fixed
- Fixed TUI width calculation for regional indicator symbols (e.g. partial flag sequences like `🇨` during streaming) to prevent wrap drift and stale character artifacts in differential rendering.
- Fixed Kitty CSI-u handling to ignore unsupported modifiers so modifier-only events do not insert stray printable characters ([#1807](https://github.com/badlogic/pi-mono/issues/1807))
## [0.55.4] - 2026-03-02

View file

@ -96,6 +96,8 @@ const KITTY_CSI_U_REGEX = /^\x1b\[(\d+)(?::(\d*))?(?::(\d+))?(?:;(\d+))?(?::(\d+
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 {
@ -111,7 +113,10 @@ function decodeKittyPrintable(data: string): string | undefined {
// Modifiers are 1-indexed in CSI-u; normalize to our bitmask.
const modifier = Number.isFinite(modValue) ? modValue - 1 : 0;
// Ignore CSI-u sequences used for Alt/Ctrl shortcuts.
// 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.

View file

@ -1048,6 +1048,8 @@ export function parseKey(data: string): string | undefined {
const { codepoint, baseLayoutKey, modifier } = kitty;
const mods: string[] = [];
const effectiveMod = modifier & ~LOCK_MASK;
const supportedModifierMask = MODIFIERS.shift | MODIFIERS.ctrl | MODIFIERS.alt;
if ((effectiveMod & ~supportedModifierMask) !== 0) return undefined;
if (effectiveMod & MODIFIERS.shift) mods.push("shift");
if (effectiveMod & MODIFIERS.ctrl) mods.push("ctrl");
if (effectiveMod & MODIFIERS.alt) mods.push("alt");

View file

@ -361,6 +361,16 @@ describe("Editor component", () => {
});
});
describe("Kitty CSI-u handling", () => {
it("ignores printable CSI-u sequences with unsupported modifiers", () => {
const editor = new Editor(createTestTUI(), defaultEditorTheme);
editor.handleInput("\x1b[99;9u");
assert.strictEqual(editor.getText(), "");
});
});
describe("Unicode text editing behavior", () => {
it("inserts mixed ASCII, umlauts, and emojis as literal text", () => {
const editor = new Editor(createTestTUI(), defaultEditorTheme);

View file

@ -294,6 +294,12 @@ describe("parseKey", () => {
assert.strictEqual(parseKey(latinCtrlC), "ctrl+c");
setKittyProtocolActive(false);
});
it("should ignore Kitty CSI-u with unsupported modifiers", () => {
setKittyProtocolActive(true);
assert.strictEqual(parseKey("\x1b[99;9u"), undefined);
setKittyProtocolActive(false);
});
});
describe("Legacy key parsing", () => {