From 510629c8cb7dfecf41a679e5141d55039b296501 Mon Sep 17 00:00:00 2001 From: mastertyko <11311479+mastertyko@users.noreply.github.com> Date: Mon, 13 Apr 2026 12:51:12 +0200 Subject: [PATCH] fix(pi-tui): filter kitty keypad private-use input (#4026) --- .../src/components/__tests__/editor.test.ts | 18 +++++++++++ .../src/components/__tests__/input.test.ts | 18 +++++++++++ packages/pi-tui/src/keys.ts | 32 +++++++++++++++++++ 3 files changed, 68 insertions(+) diff --git a/packages/pi-tui/src/components/__tests__/editor.test.ts b/packages/pi-tui/src/components/__tests__/editor.test.ts index 057ed20da..91eb6257b 100644 --- a/packages/pi-tui/src/components/__tests__/editor.test.ts +++ b/packages/pi-tui/src/components/__tests__/editor.test.ts @@ -61,4 +61,22 @@ describe("Editor", () => { assert.ok(rendered.includes(CURSOR_MARKER)); }); + + it("maps kitty keypad digits to plain editor text", () => { + const editor = new Editor(new TUI(makeTerminal()), theme); + editor.focused = true; + + editor.handleInput("\x1b[57404;129u"); + + assert.equal(editor.getText(), "5"); + }); + + it("does not insert kitty keypad navigation private-use glyphs into the editor", () => { + const editor = new Editor(new TUI(makeTerminal()), theme); + editor.focused = true; + + editor.handleInput("\x1b[57419u"); + + assert.equal(editor.getText(), ""); + }); }); diff --git a/packages/pi-tui/src/components/__tests__/input.test.ts b/packages/pi-tui/src/components/__tests__/input.test.ts index 581c2e14f..7ea0fec46 100644 --- a/packages/pi-tui/src/components/__tests__/input.test.ts +++ b/packages/pi-tui/src/components/__tests__/input.test.ts @@ -43,4 +43,22 @@ describe("Input", () => { assert.ok(!line.includes("secret123"), "rendered line must not expose raw secret text"); assert.ok(line.includes("*********"), "rendered line should include masked characters"); }); + + it("maps kitty keypad digits to text instead of inserting private-use glyphs", () => { + const input = new Input(); + input.focused = true; + + input.handleInput("\x1b[57400;129u"); + + assert.equal(input.getValue(), "1"); + }); + + it("ignores kitty keypad navigation keys in text input", () => { + const input = new Input(); + input.focused = true; + + input.handleInput("\x1b[57417u"); + + assert.equal(input.getValue(), ""); + }); }); diff --git a/packages/pi-tui/src/keys.ts b/packages/pi-tui/src/keys.ts index eff21579c..952b04462 100644 --- a/packages/pi-tui/src/keys.ts +++ b/packages/pi-tui/src/keys.ts @@ -309,6 +309,28 @@ const CODEPOINTS = { kpEnter: 57414, // Numpad Enter (Kitty protocol) } as const; +const KITTY_PRIVATE_USE_RANGE = { start: 57344, end: 63743 } as const; + +const KITTY_KEYPAD_PRINTABLES = new Map([ + [57399, "0"], // KP_0 + [57400, "1"], // KP_1 + [57401, "2"], // KP_2 + [57402, "3"], // KP_3 + [57403, "4"], // KP_4 + [57404, "5"], // KP_5 + [57405, "6"], // KP_6 + [57406, "7"], // KP_7 + [57407, "8"], // KP_8 + [57408, "9"], // KP_9 + [57409, "."], // KP_DECIMAL + [57410, "/"], // KP_DIVIDE + [57411, "*"], // KP_MULTIPLY + [57412, "-"], // KP_SUBTRACT + [57413, "+"], // KP_ADD + [57415, "="], // KP_EQUAL + [57416, ","], // KP_SEPARATOR +]); + const ARROW_CODEPOINTS = { up: -1, down: -2, @@ -1168,6 +1190,16 @@ export function decodeKittyPrintable(data: string): string | undefined { // Drop control characters or invalid codepoints. if (!Number.isFinite(effectiveCodepoint) || effectiveCodepoint < 32) return undefined; + const keypadPrintable = KITTY_KEYPAD_PRINTABLES.get(effectiveCodepoint); + if (keypadPrintable !== undefined) return keypadPrintable; + + if ( + effectiveCodepoint >= KITTY_PRIVATE_USE_RANGE.start && + effectiveCodepoint <= KITTY_PRIVATE_USE_RANGE.end + ) { + return undefined; + } + try { return String.fromCodePoint(effectiveCodepoint); } catch {