From ffbc1f8f6e37d3a0f84a76d6ebecf7301676f7e5 Mon Sep 17 00:00:00 2001 From: Vasanthdev2004 Date: Thu, 2 Apr 2026 10:05:16 +0530 Subject: [PATCH] fix: support CSI-u printable input on Windows --- src/ink/parse-keypress.test.ts | 49 ++++++++++++++++++++++++++++++++++ src/ink/parse-keypress.ts | 28 ++++++++++++++++++- 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 src/ink/parse-keypress.test.ts diff --git a/src/ink/parse-keypress.test.ts b/src/ink/parse-keypress.test.ts new file mode 100644 index 00000000..23035286 --- /dev/null +++ b/src/ink/parse-keypress.test.ts @@ -0,0 +1,49 @@ +import { expect, test } from 'bun:test' + +import { + INITIAL_STATE, + parseMultipleKeypresses, + type ParsedKey, +} from './parse-keypress.ts' +import { InputEvent } from './events/input-event.ts' + +function parseInputEvent(sequence: string): InputEvent { + const [items] = parseMultipleKeypresses(INITIAL_STATE, sequence) + + expect(items).toHaveLength(1) + + const item = items[0] + expect(item?.kind).toBe('key') + + return new InputEvent(item as ParsedKey) +} + +test('treats CSI-u modifier 0 as unmodified printable input', () => { + const event = parseInputEvent('\x1b[47;0u') + + expect(event.input).toBe('/') + expect(event.key.ctrl).toBe(false) + expect(event.key.meta).toBe(false) + expect(event.key.shift).toBe(false) + expect(event.key.super).toBe(false) +}) + +test('preserves printable Unicode CSI-u input', () => { + const event = parseInputEvent('\x1b[231u') + + expect(event.input).toBe('ç') + expect(event.key.ctrl).toBe(false) + expect(event.key.meta).toBe(false) + expect(event.key.shift).toBe(false) + expect(event.key.super).toBe(false) +}) + +test('preserves printable Unicode CSI-u input with explicit modifier 0', () => { + const event = parseInputEvent('\x1b[231;0u') + + expect(event.input).toBe('ç') + expect(event.key.ctrl).toBe(false) + expect(event.key.meta).toBe(false) + expect(event.key.shift).toBe(false) + expect(event.key.super).toBe(false) +}) diff --git a/src/ink/parse-keypress.ts b/src/ink/parse-keypress.ts index a7e43adc..ac4162fd 100644 --- a/src/ink/parse-keypress.ts +++ b/src/ink/parse-keypress.ts @@ -468,7 +468,10 @@ function decodeModifier(modifier: number): { ctrl: boolean super: boolean } { - const m = modifier - 1 + // Some Windows VT stacks use 0 instead of 1 for an unmodified CSI-u key. + // Clamp to the protocol default so plain printable keys don't look like + // ctrl+meta+shift+super all at once. + const m = Math.max(modifier, 1) - 1 return { shift: !!(m & 1), meta: !!(m & 2), @@ -477,6 +480,14 @@ function decodeModifier(modifier: number): { } } +function isPrivateUseCodepoint(codepoint: number): boolean { + return ( + (codepoint >= 0xe000 && codepoint <= 0xf8ff) || + (codepoint >= 0xf0000 && codepoint <= 0xffffd) || + (codepoint >= 0x100000 && codepoint <= 0x10fffd) + ) +} + /** * Map keycode to key name for modifyOtherKeys/CSI u sequences. * Handles both ASCII keycodes and Kitty keyboard protocol functional keys. @@ -536,6 +547,21 @@ function keycodeToName(keycode: number): string | undefined { if (keycode >= 32 && keycode <= 126) { return String.fromCharCode(keycode).toLowerCase() } + + // CSI-u can carry printable Unicode codepoints directly on some + // Windows terminals and keyboard layouts. Keep kitty's private-use + // functional key range excluded so special keys still stay non-text. + if ( + keycode > 0x1f && + keycode !== 0x7f && + (keycode < 0x80 || keycode > 0x9f) && + keycode <= 0x10ffff && + (keycode < 0xd800 || keycode > 0xdfff) && + !isPrivateUseCodepoint(keycode) + ) { + return String.fromCodePoint(keycode) + } + return undefined } }