Align behavior closer with kitty

This commit is contained in:
Daniel Imms
2026-01-09 06:53:12 -08:00
parent 2265fa2ade
commit 3fbaef06ef
5 changed files with 80 additions and 25 deletions
+3
View File
@@ -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 {
+2
View File
@@ -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;
+49 -2
View File
@@ -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');
});
});
});
});
+22 -23
View File
@@ -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 <keycode>[:<shifted>][:<base>] ; <modifiers>[:<event>] 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';
+4
View File
@@ -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`);
}
/**