From 3fbaef06efbb392701db693deeecaa699bf71679 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 9 Jan 2026 06:53:12 -0800 Subject: [PATCH] Align behavior closer with kitty --- src/browser/CoreBrowserTerminal.ts | 3 ++ src/common/InputHandler.ts | 2 + src/common/input/KittyKeyboard.test.ts | 51 +++++++++++++++++++++++++- src/common/input/KittyKeyboard.ts | 45 +++++++++++------------ src/headless/Terminal.ts | 4 ++ 5 files changed, 80 insertions(+), 25 deletions(-) diff --git a/src/browser/CoreBrowserTerminal.ts b/src/browser/CoreBrowserTerminal.ts index 32bd1b13..a2db348e 100644 --- a/src/browser/CoreBrowserTerminal.ts +++ b/src/browser/CoreBrowserTerminal.ts @@ -610,6 +610,9 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { // Listen for mouse events and translate // them into terminal mouse protocols. this.bindMouse(); + + // Emit kitty keyboard protocol support notification (31 = all flags supported) + this.coreService.triggerDataEvent(`${C0.ESC}[>31u`); } private _createRenderer(): IRenderer { diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index a4fa8a4d..ade124c2 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -2012,6 +2012,8 @@ export class InputHandler extends Disposable implements IInputHandler { this._coreService.isCursorInitialized = true; this._onRequestRefreshRows.fire(undefined); this._onRequestSyncScrollBar.fire(); + // Emit kitty keyboard protocol support notification + this._coreService.triggerDataEvent(`${C0.ESC}[>31u`); break; case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste) this._coreService.decPrivateModes.bracketedPasteMode = true; diff --git a/src/common/input/KittyKeyboard.test.ts b/src/common/input/KittyKeyboard.test.ts index ff9f346d..981c47fd 100644 --- a/src/common/input/KittyKeyboard.test.ts +++ b/src/common/input/KittyKeyboard.test.ts @@ -98,12 +98,12 @@ describe('KittyKeyboard', () => { it('should include event type for repeat events', () => { const result = evaluateKeyboardEventKitty(createEvent({ key: 'a' }), flags, KittyKeyboardEventType.REPEAT); - assert.strictEqual(result.key, '\x1b[97;:2u'); + assert.strictEqual(result.key, '\x1b[97;1:2u'); }); it('should include event type for release events', () => { const result = evaluateKeyboardEventKitty(createEvent({ key: 'a' }), flags, KittyKeyboardEventType.RELEASE); - assert.strictEqual(result.key, '\x1b[97;:3u'); + assert.strictEqual(result.key, '\x1b[97;1:3u'); }); it('should include modifiers and event type', () => { @@ -157,6 +157,12 @@ describe('KittyKeyboard', () => { const result = evaluateKeyboardEventKitty(createEvent({ key: 'Control', code: 'ControlRight', ctrlKey: true }), flags); assert.strictEqual(result.key, '\x1b[57448;5u'); }); + + it('should not report modifier-only keys without REPORT_EVENT_TYPES', () => { + const flagsNoEventTypes = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES; + const result = evaluateKeyboardEventKitty(createEvent({ key: 'Shift', code: 'ShiftLeft', shiftKey: true }), flagsNoEventTypes); + assert.strictEqual(result.key, undefined); + }); }); describe('release events without REPORT_EVENT_TYPES', () => { @@ -167,5 +173,46 @@ describe('KittyKeyboard', () => { assert.strictEqual(result.key, undefined); }); }); + + describe('with REPORT_ASSOCIATED_TEXT flag', () => { + const flags = KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES | KittyKeyboardFlags.REPORT_ASSOCIATED_TEXT; + + it('should include text codepoint for regular keys', () => { + const result = evaluateKeyboardEventKitty(createEvent({ key: 'a' }), flags); + assert.strictEqual(result.key, '\x1b[97;;97u'); + }); + + it('should include text codepoint even when same as keycode', () => { + const result = evaluateKeyboardEventKitty(createEvent({ key: 'b' }), flags); + assert.strictEqual(result.key, '\x1b[98;;98u'); + }); + + it('should include modifier and text codepoint together', () => { + const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', shiftKey: true }), flags); + assert.strictEqual(result.key, '\x1b[97;2;97u'); + }); + + it('should include text on repeat events', () => { + const flagsWithEvents = flags | KittyKeyboardFlags.REPORT_EVENT_TYPES; + const result = evaluateKeyboardEventKitty(createEvent({ key: 'a' }), flagsWithEvents, KittyKeyboardEventType.REPEAT); + assert.strictEqual(result.key, '\x1b[97;;97u'); + }); + + it('should not include text on release events', () => { + const flagsWithEvents = flags | KittyKeyboardFlags.REPORT_EVENT_TYPES; + const result = evaluateKeyboardEventKitty(createEvent({ key: 'a' }), flagsWithEvents, KittyKeyboardEventType.RELEASE); + assert.strictEqual(result.key, '\x1b[97;1:3u'); + }); + + it('should not include text for functional keys', () => { + const result = evaluateKeyboardEventKitty(createEvent({ key: 'Escape' }), flags); + assert.strictEqual(result.key, '\x1b[27u'); + }); + + it('should not include text for modifier keys', () => { + const result = evaluateKeyboardEventKitty(createEvent({ key: 'Shift', code: 'ShiftLeft', shiftKey: true }), flags); + assert.strictEqual(result.key, '\x1b[57441;2u'); + }); + }); }); }); diff --git a/src/common/input/KittyKeyboard.ts b/src/common/input/KittyKeyboard.ts index eb649006..0cb4d459 100644 --- a/src/common/input/KittyKeyboard.ts +++ b/src/common/input/KittyKeyboard.ts @@ -268,16 +268,12 @@ export function evaluateKeyboardEventKitty( // When reporting event types, use CSI u for all keys useCsiU = true; } else if (flags & KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES) { - // Use CSI u for: - // - Modifier-only keys when reporting event types - // - Keys that would be ambiguous in legacy encoding - // - Escape key - // - Backspace - // - Tab (when shifted) - // - Enter - if (isMod && (flags & KittyKeyboardFlags.REPORT_EVENT_TYPES)) { - useCsiU = true; - } else if (keyCode === 27 || keyCode === 127 || keyCode === 13) { + // Modifier-only keys are never reported without REPORT_EVENT_TYPES + if (isMod) { + return result; + } + // Use CSI u for keys that would be ambiguous in legacy encoding + if (keyCode === 27 || keyCode === 127 || keyCode === 13) { // Escape, Backspace, Enter useCsiU = true; } else if (keyCode === 9 && ev.shiftKey) { @@ -303,27 +299,30 @@ export function evaluateKeyboardEventKitty( // Format: CSI [:][:] ; [:] u let seq = C0.ESC + '[' + keyCode; - // Add modifiers and event type - if (modifiers > 0 || (reportEventTypes && eventType !== KittyKeyboardEventType.PRESS)) { + // Check if we need associated text (press and repeat events, not release) + const reportAssociatedText = !!(flags & KittyKeyboardFlags.REPORT_ASSOCIATED_TEXT) && + eventType !== KittyKeyboardEventType.RELEASE && ev.key.length === 1 && !isFunc && !isMod; + const textCode = reportAssociatedText ? ev.key.codePointAt(0) : undefined; + + // When text is present, don't include event type marker (even for repeat) + // Release events always need event type marker + const needsEventType = reportEventTypes && eventType !== KittyKeyboardEventType.PRESS && textCode === undefined; + if (modifiers > 0 || needsEventType || textCode !== undefined) { seq += ';'; + // Use 1 as base when event type needed but no modifiers (kitty format: 1 + modifier_bits) if (modifiers > 0) { seq += modifiers; + } else if (needsEventType) { + seq += '1'; } - if (reportEventTypes && eventType !== KittyKeyboardEventType.PRESS) { + if (needsEventType) { seq += ':' + eventType; } } - // Add associated text if requested and available - if ((flags & KittyKeyboardFlags.REPORT_ASSOCIATED_TEXT) && ev.key.length === 1 && !isFunc && !isMod) { - const textCode = ev.key.codePointAt(0); - if (textCode !== undefined && textCode !== keyCode) { - // Append text as ; text u - if (!seq.includes(';')) { - seq += ';'; - } - seq += ';' + textCode; - } + // Add associated text if requested + if (textCode !== undefined) { + seq += ';' + textCode; } seq += 'u'; diff --git a/src/headless/Terminal.ts b/src/headless/Terminal.ts index 1425699e..41aaf4b6 100644 --- a/src/headless/Terminal.ts +++ b/src/headless/Terminal.ts @@ -24,6 +24,7 @@ import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { IBuffer } from 'common/buffer/Types'; import { CoreTerminal } from 'common/CoreTerminal'; +import { C0 } from 'common/data/EscapeSequences'; import { IMarker, ITerminalOptions } from 'common/Types'; import { Emitter, Event } from 'vs/base/common/event'; @@ -54,6 +55,9 @@ export class Terminal extends CoreTerminal { this._register(Event.forward(this._inputHandler.onA11yChar, this._onA11yCharEmitter)); this._register(Event.forward(this._inputHandler.onA11yTab, this._onA11yTabEmitter)); this._register(Event.forward(Event.map(this._inputHandler.onRequestRefreshRows, e => ({ start: e?.start ?? 0, end: e?.end ?? this.rows - 1 })), this._onRender)); + + // Emit kitty keyboard protocol support notification (31 = all flags supported) + this.coreService.triggerDataEvent(`${C0.ESC}[>31u`); } /**