Merge pull request #5636 from Tyriar/win32__kitty

Move kitty input mode into a class to avoid init cost
This commit is contained in:
Daniel Imms
2026-01-30 07:29:42 -08:00
committed by GitHub
3 changed files with 551 additions and 564 deletions
+12 -4
View File
@@ -5,7 +5,7 @@
import { IKeyboardService } from 'browser/services/Services';
import { evaluateKeyboardEvent } from 'common/input/Keyboard';
import { evaluateKeyboardEventKitty, KittyKeyboardEventType, KittyKeyboardFlags, shouldUseKittyProtocol } from 'common/input/KittyKeyboard';
import { KittyKeyboard, KittyKeyboardEventType, KittyKeyboardFlags } from 'common/input/KittyKeyboard';
import { Win32InputMode } from 'common/input/Win32InputMode';
import { isMac } from 'common/Platform';
import { ICoreService, IOptionsService } from 'common/services/Services';
@@ -15,6 +15,7 @@ export class KeyboardService implements IKeyboardService {
public serviceBrand: undefined;
private _win32InputMode: Win32InputMode | undefined;
private _kittyKeyboard: KittyKeyboard | undefined;
constructor(
@ICoreService private readonly _coreService: ICoreService,
@@ -29,6 +30,13 @@ export class KeyboardService implements IKeyboardService {
return this._win32InputMode;
}
private _getKittyKeyboard(): KittyKeyboard {
if (!this._kittyKeyboard) {
this._kittyKeyboard = new KittyKeyboard();
}
return this._kittyKeyboard;
}
public evaluateKeyDown(event: KeyboardEvent): IKeyboardResult {
// Win32 input mode takes priority (most raw)
if (this.useWin32InputMode) {
@@ -36,7 +44,7 @@ export class KeyboardService implements IKeyboardService {
}
const kittyFlags = this._coreService.kittyKeyboard.flags;
return this.useKitty
? evaluateKeyboardEventKitty(event, kittyFlags, event.repeat ? KittyKeyboardEventType.REPEAT : KittyKeyboardEventType.PRESS)
? this._getKittyKeyboard().evaluate(event, kittyFlags, event.repeat ? KittyKeyboardEventType.REPEAT : KittyKeyboardEventType.PRESS)
: evaluateKeyboardEvent(event, this._coreService.decPrivateModes.applicationCursorKeys, isMac, this._optionsService.rawOptions.macOptionIsMeta);
}
@@ -47,14 +55,14 @@ export class KeyboardService implements IKeyboardService {
}
const kittyFlags = this._coreService.kittyKeyboard.flags;
if (this.useKitty && (kittyFlags & KittyKeyboardFlags.REPORT_EVENT_TYPES)) {
return evaluateKeyboardEventKitty(event, kittyFlags, KittyKeyboardEventType.RELEASE);
return this._getKittyKeyboard().evaluate(event, kittyFlags, KittyKeyboardEventType.RELEASE);
}
return undefined;
}
public get useKitty(): boolean {
const kittyFlags = this._coreService.kittyKeyboard.flags;
return !!(this._optionsService.rawOptions.vtExtensions?.kittyKeyboard && shouldUseKittyProtocol(kittyFlags));
return !!(this._optionsService.rawOptions.vtExtensions?.kittyKeyboard && KittyKeyboard.shouldUseProtocol(kittyFlags));
}
public get useWin32InputMode(): boolean {
+133 -127
View File
@@ -1,7 +1,7 @@
import { assert } from 'chai';
import { evaluateKeyboardEventKitty, KittyKeyboardEventType, KittyKeyboardFlags, shouldUseKittyProtocol } from 'common/input/KittyKeyboard';
import { IKeyboardResult, IKeyboardEvent } from 'common/Types';
import { KittyKeyboard, KittyKeyboardEventType, KittyKeyboardFlags } from 'common/input/KittyKeyboard';
import { IKeyboardEvent } from 'common/Types';
function createEvent(partialEvent: Partial<IKeyboardEvent> = {}): IKeyboardEvent {
return {
@@ -17,71 +17,77 @@ function createEvent(partialEvent: Partial<IKeyboardEvent> = {}): IKeyboardEvent
}
describe('KittyKeyboard', () => {
describe('shouldUseKittyProtocol', () => {
let kitty: KittyKeyboard;
beforeEach(() => {
kitty = new KittyKeyboard();
});
describe('shouldUseProtocol', () => {
it('should return false when flags are 0', () => {
assert.strictEqual(shouldUseKittyProtocol(0), false);
assert.strictEqual(KittyKeyboard.shouldUseProtocol(0), false);
});
it('should return true when any flag is set', () => {
assert.strictEqual(shouldUseKittyProtocol(KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES), true);
assert.strictEqual(shouldUseKittyProtocol(KittyKeyboardFlags.REPORT_EVENT_TYPES), true);
assert.strictEqual(shouldUseKittyProtocol(0b11111), true);
assert.strictEqual(KittyKeyboard.shouldUseProtocol(KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES), true);
assert.strictEqual(KittyKeyboard.shouldUseProtocol(KittyKeyboardFlags.REPORT_EVENT_TYPES), true);
assert.strictEqual(KittyKeyboard.shouldUseProtocol(0b11111), true);
});
});
describe('evaluateKeyboardEventKitty', () => {
describe('evaluate', () => {
describe('modifier encoding (value = 1 + modifiers)', () => {
const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES;
it('shift+letter sends plain character in DISAMBIGUATE mode', () => {
// Kitty spec: DISAMBIGUATE only encodes keys ambiguous in legacy encoding
// Shift+a → "A" is not ambiguous, so send plain "A"
const result = evaluateKeyboardEventKitty(createEvent({ key: 'A', shiftKey: true }), flags);
const result = kitty.evaluate(createEvent({ key: 'A', shiftKey: true }), flags);
assert.strictEqual(result.key, 'A');
});
it('alt=3 (1+2) still uses CSI u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', altKey: true }), flags);
const result = kitty.evaluate(createEvent({ key: 'a', altKey: true }), flags);
assert.strictEqual(result.key, '\x1b[97;3u');
});
it('ctrl=5 (1+4)', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', ctrlKey: true }), flags);
const result = kitty.evaluate(createEvent({ key: 'a', ctrlKey: true }), flags);
assert.strictEqual(result.key, '\x1b[97;5u');
});
it('super/meta=9 (1+8)', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', metaKey: true }), flags);
const result = kitty.evaluate(createEvent({ key: 'a', metaKey: true }), flags);
assert.strictEqual(result.key, '\x1b[97;9u');
});
it('ctrl+shift=6 (1+4+1)', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', ctrlKey: true, shiftKey: true }), flags);
const result = kitty.evaluate(createEvent({ key: 'a', ctrlKey: true, shiftKey: true }), flags);
assert.strictEqual(result.key, '\x1b[97;6u');
});
it('ctrl+alt=7 (1+4+2)', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', ctrlKey: true, altKey: true }), flags);
const result = kitty.evaluate(createEvent({ key: 'a', ctrlKey: true, altKey: true }), flags);
assert.strictEqual(result.key, '\x1b[97;7u');
});
it('ctrl+alt+shift=8 (1+4+2+1)', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', ctrlKey: true, altKey: true, shiftKey: true }), flags);
const result = kitty.evaluate(createEvent({ key: 'a', ctrlKey: true, altKey: true, shiftKey: true }), flags);
assert.strictEqual(result.key, '\x1b[97;8u');
});
it('ctrl+super=13 (1+4+8)', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', ctrlKey: true, metaKey: true }), flags);
const result = kitty.evaluate(createEvent({ key: 'a', ctrlKey: true, metaKey: true }), flags);
assert.strictEqual(result.key, '\x1b[97;13u');
});
it('all four modifiers=16 (1+1+2+4+8)', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', shiftKey: true, altKey: true, ctrlKey: true, metaKey: true }), flags);
const result = kitty.evaluate(createEvent({ key: 'a', shiftKey: true, altKey: true, ctrlKey: true, metaKey: true }), flags);
assert.strictEqual(result.key, '\x1b[97;16u');
});
it('no modifiers omits modifier field', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Escape' }), flags);
const result = kitty.evaluate(createEvent({ key: 'Escape' }), flags);
assert.strictEqual(result.key, '\x1b[27u');
});
});
@@ -90,42 +96,42 @@ describe('KittyKeyboard', () => {
const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES;
it('Escape → CSI 27 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Escape' }), flags);
const result = kitty.evaluate(createEvent({ key: 'Escape' }), flags);
assert.strictEqual(result.key, '\x1b[27u');
});
it('Enter → CSI 13 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Enter' }), flags);
const result = kitty.evaluate(createEvent({ key: 'Enter' }), flags);
assert.strictEqual(result.key, '\x1b[13u');
});
it('Tab → CSI 9 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Tab' }), flags);
const result = kitty.evaluate(createEvent({ key: 'Tab' }), flags);
assert.strictEqual(result.key, '\x1b[9u');
});
it('Backspace → CSI 127 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Backspace' }), flags);
const result = kitty.evaluate(createEvent({ key: 'Backspace' }), flags);
assert.strictEqual(result.key, '\x1b[127u');
});
it('Space → CSI 32 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: ' ' }), flags);
const result = kitty.evaluate(createEvent({ key: ' ' }), flags);
assert.strictEqual(result.key, '\x1b[32u');
});
it('Shift+Tab → CSI 9;2 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Tab', shiftKey: true }), flags);
const result = kitty.evaluate(createEvent({ key: 'Tab', shiftKey: true }), flags);
assert.strictEqual(result.key, '\x1b[9;2u');
});
it('Ctrl+Enter → CSI 13;5 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Enter', ctrlKey: true }), flags);
const result = kitty.evaluate(createEvent({ key: 'Enter', ctrlKey: true }), flags);
assert.strictEqual(result.key, '\x1b[13;5u');
});
it('Alt+Escape → CSI 27;3 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Escape', altKey: true }), flags);
const result = kitty.evaluate(createEvent({ key: 'Escape', altKey: true }), flags);
assert.strictEqual(result.key, '\x1b[27;3u');
});
});
@@ -134,42 +140,42 @@ describe('KittyKeyboard', () => {
const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES;
it('Insert → CSI 2 ~', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Insert' }), flags);
const result = kitty.evaluate(createEvent({ key: 'Insert' }), flags);
assert.strictEqual(result.key, '\x1b[2~');
});
it('Delete → CSI 3 ~', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Delete' }), flags);
const result = kitty.evaluate(createEvent({ key: 'Delete' }), flags);
assert.strictEqual(result.key, '\x1b[3~');
});
it('PageUp → CSI 5 ~', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'PageUp' }), flags);
const result = kitty.evaluate(createEvent({ key: 'PageUp' }), flags);
assert.strictEqual(result.key, '\x1b[5~');
});
it('PageDown → CSI 6 ~', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'PageDown' }), flags);
const result = kitty.evaluate(createEvent({ key: 'PageDown' }), flags);
assert.strictEqual(result.key, '\x1b[6~');
});
it('Home → CSI H', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Home' }), flags);
const result = kitty.evaluate(createEvent({ key: 'Home' }), flags);
assert.strictEqual(result.key, '\x1b[H');
});
it('End → CSI F', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'End' }), flags);
const result = kitty.evaluate(createEvent({ key: 'End' }), flags);
assert.strictEqual(result.key, '\x1b[F');
});
it('Shift+PageUp → CSI 5;2 ~', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'PageUp', shiftKey: true }), flags);
const result = kitty.evaluate(createEvent({ key: 'PageUp', shiftKey: true }), flags);
assert.strictEqual(result.key, '\x1b[5;2~');
});
it('Ctrl+Home → CSI 1;5 H', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Home', ctrlKey: true }), flags);
const result = kitty.evaluate(createEvent({ key: 'Home', ctrlKey: true }), flags);
assert.strictEqual(result.key, '\x1b[1;5H');
});
});
@@ -178,37 +184,37 @@ describe('KittyKeyboard', () => {
const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES;
it('ArrowUp → CSI A', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'ArrowUp' }), flags);
const result = kitty.evaluate(createEvent({ key: 'ArrowUp' }), flags);
assert.strictEqual(result.key, '\x1b[A');
});
it('ArrowDown → CSI B', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'ArrowDown' }), flags);
const result = kitty.evaluate(createEvent({ key: 'ArrowDown' }), flags);
assert.strictEqual(result.key, '\x1b[B');
});
it('ArrowRight → CSI C', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'ArrowRight' }), flags);
const result = kitty.evaluate(createEvent({ key: 'ArrowRight' }), flags);
assert.strictEqual(result.key, '\x1b[C');
});
it('ArrowLeft → CSI D', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'ArrowLeft' }), flags);
const result = kitty.evaluate(createEvent({ key: 'ArrowLeft' }), flags);
assert.strictEqual(result.key, '\x1b[D');
});
it('Shift+ArrowUp → CSI 1;2 A', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'ArrowUp', shiftKey: true }), flags);
const result = kitty.evaluate(createEvent({ key: 'ArrowUp', shiftKey: true }), flags);
assert.strictEqual(result.key, '\x1b[1;2A');
});
it('Ctrl+ArrowLeft → CSI 1;5 D', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'ArrowLeft', ctrlKey: true }), flags);
const result = kitty.evaluate(createEvent({ key: 'ArrowLeft', ctrlKey: true }), flags);
assert.strictEqual(result.key, '\x1b[1;5D');
});
it('Ctrl+Shift+ArrowRight → CSI 1;6 C', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'ArrowRight', ctrlKey: true, shiftKey: true }), flags);
const result = kitty.evaluate(createEvent({ key: 'ArrowRight', ctrlKey: true, shiftKey: true }), flags);
assert.strictEqual(result.key, '\x1b[1;6C');
});
});
@@ -217,72 +223,72 @@ describe('KittyKeyboard', () => {
const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES;
it('F1 → CSI P (SS3 form)', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'F1' }), flags);
const result = kitty.evaluate(createEvent({ key: 'F1' }), flags);
assert.strictEqual(result.key, '\x1bOP');
});
it('F2 → CSI Q (SS3 form)', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'F2' }), flags);
const result = kitty.evaluate(createEvent({ key: 'F2' }), flags);
assert.strictEqual(result.key, '\x1bOQ');
});
it('F3 → CSI R (SS3 form)', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'F3' }), flags);
const result = kitty.evaluate(createEvent({ key: 'F3' }), flags);
assert.strictEqual(result.key, '\x1bOR');
});
it('F4 → CSI S (SS3 form)', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'F4' }), flags);
const result = kitty.evaluate(createEvent({ key: 'F4' }), flags);
assert.strictEqual(result.key, '\x1bOS');
});
it('F5 → CSI 15 ~', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'F5' }), flags);
const result = kitty.evaluate(createEvent({ key: 'F5' }), flags);
assert.strictEqual(result.key, '\x1b[15~');
});
it('F6 → CSI 17 ~', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'F6' }), flags);
const result = kitty.evaluate(createEvent({ key: 'F6' }), flags);
assert.strictEqual(result.key, '\x1b[17~');
});
it('F7 → CSI 18 ~', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'F7' }), flags);
const result = kitty.evaluate(createEvent({ key: 'F7' }), flags);
assert.strictEqual(result.key, '\x1b[18~');
});
it('F8 → CSI 19 ~', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'F8' }), flags);
const result = kitty.evaluate(createEvent({ key: 'F8' }), flags);
assert.strictEqual(result.key, '\x1b[19~');
});
it('F9 → CSI 20 ~', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'F9' }), flags);
const result = kitty.evaluate(createEvent({ key: 'F9' }), flags);
assert.strictEqual(result.key, '\x1b[20~');
});
it('F10 → CSI 21 ~', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'F10' }), flags);
const result = kitty.evaluate(createEvent({ key: 'F10' }), flags);
assert.strictEqual(result.key, '\x1b[21~');
});
it('F11 → CSI 23 ~', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'F11' }), flags);
const result = kitty.evaluate(createEvent({ key: 'F11' }), flags);
assert.strictEqual(result.key, '\x1b[23~');
});
it('F12 → CSI 24 ~', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'F12' }), flags);
const result = kitty.evaluate(createEvent({ key: 'F12' }), flags);
assert.strictEqual(result.key, '\x1b[24~');
});
it('Shift+F1 → CSI 1;2 P', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'F1', shiftKey: true }), flags);
const result = kitty.evaluate(createEvent({ key: 'F1', shiftKey: true }), flags);
assert.strictEqual(result.key, '\x1b[1;2P');
});
it('Ctrl+F5 → CSI 15;5 ~', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'F5', ctrlKey: true }), flags);
const result = kitty.evaluate(createEvent({ key: 'F5', ctrlKey: true }), flags);
assert.strictEqual(result.key, '\x1b[15;5~');
});
});
@@ -291,22 +297,22 @@ describe('KittyKeyboard', () => {
const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES;
it('F13 → CSI 57376 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'F13' }), flags);
const result = kitty.evaluate(createEvent({ key: 'F13' }), flags);
assert.strictEqual(result.key, '\x1b[57376u');
});
it('F14 → CSI 57377 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'F14' }), flags);
const result = kitty.evaluate(createEvent({ key: 'F14' }), flags);
assert.strictEqual(result.key, '\x1b[57377u');
});
it('F20 → CSI 57383 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'F20' }), flags);
const result = kitty.evaluate(createEvent({ key: 'F20' }), flags);
assert.strictEqual(result.key, '\x1b[57383u');
});
it('F24 → CSI 57387 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'F24' }), flags);
const result = kitty.evaluate(createEvent({ key: 'F24' }), flags);
assert.strictEqual(result.key, '\x1b[57387u');
});
});
@@ -315,57 +321,57 @@ describe('KittyKeyboard', () => {
const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES;
it('Numpad0 → CSI 57399 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: '0', code: 'Numpad0' }), flags);
const result = kitty.evaluate(createEvent({ key: '0', code: 'Numpad0' }), flags);
assert.strictEqual(result.key, '\x1b[57399u');
});
it('Numpad1 → CSI 57400 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: '1', code: 'Numpad1' }), flags);
const result = kitty.evaluate(createEvent({ key: '1', code: 'Numpad1' }), flags);
assert.strictEqual(result.key, '\x1b[57400u');
});
it('Numpad9 → CSI 57408 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: '9', code: 'Numpad9' }), flags);
const result = kitty.evaluate(createEvent({ key: '9', code: 'Numpad9' }), flags);
assert.strictEqual(result.key, '\x1b[57408u');
});
it('NumpadDecimal → CSI 57409 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: '.', code: 'NumpadDecimal' }), flags);
const result = kitty.evaluate(createEvent({ key: '.', code: 'NumpadDecimal' }), flags);
assert.strictEqual(result.key, '\x1b[57409u');
});
it('NumpadDivide → CSI 57410 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: '/', code: 'NumpadDivide' }), flags);
const result = kitty.evaluate(createEvent({ key: '/', code: 'NumpadDivide' }), flags);
assert.strictEqual(result.key, '\x1b[57410u');
});
it('NumpadMultiply → CSI 57411 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: '*', code: 'NumpadMultiply' }), flags);
const result = kitty.evaluate(createEvent({ key: '*', code: 'NumpadMultiply' }), flags);
assert.strictEqual(result.key, '\x1b[57411u');
});
it('NumpadSubtract → CSI 57412 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: '-', code: 'NumpadSubtract' }), flags);
const result = kitty.evaluate(createEvent({ key: '-', code: 'NumpadSubtract' }), flags);
assert.strictEqual(result.key, '\x1b[57412u');
});
it('NumpadAdd → CSI 57413 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: '+', code: 'NumpadAdd' }), flags);
const result = kitty.evaluate(createEvent({ key: '+', code: 'NumpadAdd' }), flags);
assert.strictEqual(result.key, '\x1b[57413u');
});
it('NumpadEnter → CSI 57414 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Enter', code: 'NumpadEnter' }), flags);
const result = kitty.evaluate(createEvent({ key: 'Enter', code: 'NumpadEnter' }), flags);
assert.strictEqual(result.key, '\x1b[57414u');
});
it('NumpadEqual → CSI 57415 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: '=', code: 'NumpadEqual' }), flags);
const result = kitty.evaluate(createEvent({ key: '=', code: 'NumpadEqual' }), flags);
assert.strictEqual(result.key, '\x1b[57415u');
});
it('Ctrl+Numpad5 → CSI 57404;5 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: '5', code: 'Numpad5', ctrlKey: true }), flags);
const result = kitty.evaluate(createEvent({ key: '5', code: 'Numpad5', ctrlKey: true }), flags);
assert.strictEqual(result.key, '\x1b[57404;5u');
});
});
@@ -374,57 +380,57 @@ describe('KittyKeyboard', () => {
const flags = KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES;
it('Left Shift → CSI 57441 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Shift', code: 'ShiftLeft', shiftKey: true }), flags);
const result = kitty.evaluate(createEvent({ key: 'Shift', code: 'ShiftLeft', shiftKey: true }), flags);
assert.strictEqual(result.key, '\x1b[57441;2u');
});
it('Right Shift → CSI 57447 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Shift', code: 'ShiftRight', shiftKey: true }), flags);
const result = kitty.evaluate(createEvent({ key: 'Shift', code: 'ShiftRight', shiftKey: true }), flags);
assert.strictEqual(result.key, '\x1b[57447;2u');
});
it('Left Control → CSI 57442 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Control', code: 'ControlLeft', ctrlKey: true }), flags);
const result = kitty.evaluate(createEvent({ key: 'Control', code: 'ControlLeft', ctrlKey: true }), flags);
assert.strictEqual(result.key, '\x1b[57442;5u');
});
it('Right Control → CSI 57448 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Control', code: 'ControlRight', ctrlKey: true }), flags);
const result = kitty.evaluate(createEvent({ key: 'Control', code: 'ControlRight', ctrlKey: true }), flags);
assert.strictEqual(result.key, '\x1b[57448;5u');
});
it('Left Alt → CSI 57443 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Alt', code: 'AltLeft', altKey: true }), flags);
const result = kitty.evaluate(createEvent({ key: 'Alt', code: 'AltLeft', altKey: true }), flags);
assert.strictEqual(result.key, '\x1b[57443;3u');
});
it('Right Alt → CSI 57449 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Alt', code: 'AltRight', altKey: true }), flags);
const result = kitty.evaluate(createEvent({ key: 'Alt', code: 'AltRight', altKey: true }), flags);
assert.strictEqual(result.key, '\x1b[57449;3u');
});
it('Left Meta/Super → CSI 57444 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Meta', code: 'MetaLeft', metaKey: true }), flags);
const result = kitty.evaluate(createEvent({ key: 'Meta', code: 'MetaLeft', metaKey: true }), flags);
assert.strictEqual(result.key, '\x1b[57444;9u');
});
it('Right Meta/Super → CSI 57450 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Meta', code: 'MetaRight', metaKey: true }), flags);
const result = kitty.evaluate(createEvent({ key: 'Meta', code: 'MetaRight', metaKey: true }), flags);
assert.strictEqual(result.key, '\x1b[57450;9u');
});
it('CapsLock → CSI 57358 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'CapsLock', code: 'CapsLock' }), flags);
const result = kitty.evaluate(createEvent({ key: 'CapsLock', code: 'CapsLock' }), flags);
assert.strictEqual(result.key, '\x1b[57358u');
});
it('NumLock → CSI 57360 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'NumLock', code: 'NumLock' }), flags);
const result = kitty.evaluate(createEvent({ key: 'NumLock', code: 'NumLock' }), flags);
assert.strictEqual(result.key, '\x1b[57360u');
});
it('ScrollLock → CSI 57359 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'ScrollLock', code: 'ScrollLock' }), flags);
const result = kitty.evaluate(createEvent({ key: 'ScrollLock', code: 'ScrollLock' }), flags);
assert.strictEqual(result.key, '\x1b[57359u');
});
});
@@ -433,42 +439,42 @@ describe('KittyKeyboard', () => {
const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES | KittyKeyboardFlags.REPORT_EVENT_TYPES;
it('press event (default, no suffix)', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'a' }), flags, KittyKeyboardEventType.PRESS);
const result = kitty.evaluate(createEvent({ key: 'a' }), flags, KittyKeyboardEventType.PRESS);
assert.strictEqual(result.key, '\x1b[97u');
});
it('press event explicit :1 when modifiers present', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', ctrlKey: true }), flags, KittyKeyboardEventType.PRESS);
const result = kitty.evaluate(createEvent({ key: 'a', ctrlKey: true }), flags, KittyKeyboardEventType.PRESS);
assert.strictEqual(result.key, '\x1b[97;5u');
});
it('repeat event → :2 suffix', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'a' }), flags, KittyKeyboardEventType.REPEAT);
const result = kitty.evaluate(createEvent({ key: 'a' }), flags, KittyKeyboardEventType.REPEAT);
assert.strictEqual(result.key, '\x1b[97;1:2u');
});
it('release event → :3 suffix', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'a' }), flags, KittyKeyboardEventType.RELEASE);
const result = kitty.evaluate(createEvent({ key: 'a' }), flags, KittyKeyboardEventType.RELEASE);
assert.strictEqual(result.key, '\x1b[97;1:3u');
});
it('release with modifier → mod:3', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', ctrlKey: true }), flags, KittyKeyboardEventType.RELEASE);
const result = kitty.evaluate(createEvent({ key: 'a', ctrlKey: true }), flags, KittyKeyboardEventType.RELEASE);
assert.strictEqual(result.key, '\x1b[97;5:3u');
});
it('repeat with modifier → mod:2', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', shiftKey: true, altKey: true }), flags, KittyKeyboardEventType.REPEAT);
const result = kitty.evaluate(createEvent({ key: 'a', shiftKey: true, altKey: true }), flags, KittyKeyboardEventType.REPEAT);
assert.strictEqual(result.key, '\x1b[97;4:2u');
});
it('functional key release → CSI code;1:3 ~', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Delete' }), flags, KittyKeyboardEventType.RELEASE);
const result = kitty.evaluate(createEvent({ key: 'Delete' }), flags, KittyKeyboardEventType.RELEASE);
assert.strictEqual(result.key, '\x1b[3;1:3~');
});
it('modifier key release includes its own bit cleared', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Shift', code: 'ShiftLeft', shiftKey: false }), flags, KittyKeyboardEventType.RELEASE);
const result = kitty.evaluate(createEvent({ key: 'Shift', code: 'ShiftLeft', shiftKey: false }), flags, KittyKeyboardEventType.RELEASE);
assert.strictEqual(result.key, '\x1b[57441;1:3u');
});
});
@@ -477,34 +483,34 @@ describe('KittyKeyboard', () => {
const flags = KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES;
it('lowercase letter → CSI codepoint u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'a' }), flags);
const result = kitty.evaluate(createEvent({ key: 'a' }), flags);
assert.strictEqual(result.key, '\x1b[97u');
});
it('uppercase letter uses lowercase codepoint', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'A', shiftKey: true }), flags);
const result = kitty.evaluate(createEvent({ key: 'A', shiftKey: true }), flags);
assert.strictEqual(result.key, '\x1b[97;2u');
});
it('digit → CSI codepoint u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: '5' }), flags);
const result = kitty.evaluate(createEvent({ key: '5' }), flags);
assert.strictEqual(result.key, '\x1b[53u');
});
it('punctuation → CSI codepoint u', () => {
assert.strictEqual(evaluateKeyboardEventKitty(createEvent({ key: '.' }), flags).key, '\x1b[46u');
assert.strictEqual(evaluateKeyboardEventKitty(createEvent({ key: ',' }), flags).key, '\x1b[44u');
assert.strictEqual(evaluateKeyboardEventKitty(createEvent({ key: ';' }), flags).key, '\x1b[59u');
assert.strictEqual(evaluateKeyboardEventKitty(createEvent({ key: '/' }), flags).key, '\x1b[47u');
assert.strictEqual(kitty.evaluate(createEvent({ key: '.' }), flags).key, '\x1b[46u');
assert.strictEqual(kitty.evaluate(createEvent({ key: ',' }), flags).key, '\x1b[44u');
assert.strictEqual(kitty.evaluate(createEvent({ key: ';' }), flags).key, '\x1b[59u');
assert.strictEqual(kitty.evaluate(createEvent({ key: '/' }), flags).key, '\x1b[47u');
});
it('brackets → CSI codepoint u', () => {
assert.strictEqual(evaluateKeyboardEventKitty(createEvent({ key: '[' }), flags).key, '\x1b[91u');
assert.strictEqual(evaluateKeyboardEventKitty(createEvent({ key: ']' }), flags).key, '\x1b[93u');
assert.strictEqual(kitty.evaluate(createEvent({ key: '[' }), flags).key, '\x1b[91u');
assert.strictEqual(kitty.evaluate(createEvent({ key: ']' }), flags).key, '\x1b[93u');
});
it('space → CSI 32 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: ' ' }), flags);
const result = kitty.evaluate(createEvent({ key: ' ' }), flags);
assert.strictEqual(result.key, '\x1b[32u');
});
});
@@ -513,38 +519,38 @@ describe('KittyKeyboard', () => {
const flags = KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES | KittyKeyboardFlags.REPORT_ASSOCIATED_TEXT;
it('regular key includes text codepoint', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'a' }), flags);
const result = kitty.evaluate(createEvent({ key: 'a' }), flags);
assert.strictEqual(result.key, '\x1b[97;;97u');
});
it('shifted key includes shifted text', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'A', shiftKey: true }), flags);
const result = kitty.evaluate(createEvent({ key: 'A', shiftKey: true }), flags);
assert.strictEqual(result.key, '\x1b[97;2;65u');
});
it('Ctrl+key omits text (control code)', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', ctrlKey: true }), flags);
const result = kitty.evaluate(createEvent({ key: 'a', ctrlKey: true }), flags);
assert.strictEqual(result.key, '\x1b[97;5u');
});
it('functional key has no text', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Escape' }), flags);
const result = kitty.evaluate(createEvent({ key: 'Escape' }), flags);
assert.strictEqual(result.key, '\x1b[27u');
});
it('release event has no text', () => {
const flagsWithEvents = flags | KittyKeyboardFlags.REPORT_EVENT_TYPES;
const result = evaluateKeyboardEventKitty(createEvent({ key: 'a' }), flagsWithEvents, KittyKeyboardEventType.RELEASE);
const result = kitty.evaluate(createEvent({ key: 'a' }), flagsWithEvents, KittyKeyboardEventType.RELEASE);
assert.strictEqual(result.key, '\x1b[97;1:3u');
});
it('digit with text', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: '5' }), flags);
const result = kitty.evaluate(createEvent({ key: '5' }), flags);
assert.strictEqual(result.key, '\x1b[53;;53u');
});
it('Shift+digit shows shifted symbol', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: '%', shiftKey: true, code: 'Digit5' }), flags);
const result = kitty.evaluate(createEvent({ key: '%', shiftKey: true, code: 'Digit5' }), flags);
assert.strictEqual(result.key, '\x1b[53;2;37u');
});
});
@@ -553,22 +559,22 @@ describe('KittyKeyboard', () => {
const flags = KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES | KittyKeyboardFlags.REPORT_ALTERNATE_KEYS;
it('Shift+a includes shifted key → CSI 97:65 ; 2 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'A', shiftKey: true, code: 'KeyA' }), flags);
const result = kitty.evaluate(createEvent({ key: 'A', shiftKey: true, code: 'KeyA' }), flags);
assert.strictEqual(result.key, '\x1b[97:65;2u');
});
it('unshifted key has no alternate', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', code: 'KeyA' }), flags);
const result = kitty.evaluate(createEvent({ key: 'a', code: 'KeyA' }), flags);
assert.strictEqual(result.key, '\x1b[97u');
});
it('Shift+5 includes shifted key → CSI 53:37 ; 2 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: '%', shiftKey: true, code: 'Digit5' }), flags);
const result = kitty.evaluate(createEvent({ key: '%', shiftKey: true, code: 'Digit5' }), flags);
assert.strictEqual(result.key, '\x1b[53:37;2u');
});
it('functional keys have no shifted alternate', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Escape', shiftKey: true }), flags);
const result = kitty.evaluate(createEvent({ key: 'Escape', shiftKey: true }), flags);
assert.strictEqual(result.key, '\x1b[27;2u');
});
});
@@ -577,13 +583,13 @@ describe('KittyKeyboard', () => {
const flags = KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES | KittyKeyboardFlags.REPORT_ALTERNATE_KEYS | KittyKeyboardFlags.REPORT_ASSOCIATED_TEXT;
it('Shift+a → CSI 97:65 ; 2 ; 65 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'A', shiftKey: true, code: 'KeyA' }), flags);
const result = kitty.evaluate(createEvent({ key: 'A', shiftKey: true, code: 'KeyA' }), flags);
assert.strictEqual(result.key, '\x1b[97:65;2;65u');
});
it('Shift+a release → CSI 97:65 ; 2:3 u (no text)', () => {
const flagsWithEvents = flags | KittyKeyboardFlags.REPORT_EVENT_TYPES;
const result = evaluateKeyboardEventKitty(createEvent({ key: 'A', shiftKey: true, code: 'KeyA' }), flagsWithEvents, KittyKeyboardEventType.RELEASE);
const result = kitty.evaluate(createEvent({ key: 'A', shiftKey: true, code: 'KeyA' }), flagsWithEvents, KittyKeyboardEventType.RELEASE);
assert.strictEqual(result.key, '\x1b[97:65;2:3u');
});
});
@@ -592,7 +598,7 @@ describe('KittyKeyboard', () => {
const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES;
it('should not generate key sequence for release events', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'a' }), flags, KittyKeyboardEventType.RELEASE);
const result = kitty.evaluate(createEvent({ key: 'a' }), flags, KittyKeyboardEventType.RELEASE);
assert.strictEqual(result.key, undefined);
});
});
@@ -602,37 +608,37 @@ describe('KittyKeyboard', () => {
it('shift+letter sends plain character in DISAMBIGUATE mode', () => {
// Shift+A produces printable "A", not ambiguous, so send plain character
const result = evaluateKeyboardEventKitty(createEvent({ key: 'A', shiftKey: true }), flags);
const result = kitty.evaluate(createEvent({ key: 'A', shiftKey: true }), flags);
assert.strictEqual(result.key, 'A');
});
it('ctrl+shift+a sends lowercase codepoint 97', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'A', ctrlKey: true, shiftKey: true }), flags);
const result = kitty.evaluate(createEvent({ key: 'A', ctrlKey: true, shiftKey: true }), flags);
assert.strictEqual(result.key, '\x1b[97;6u');
});
it('Dead key produces no output', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Dead' }), flags);
const result = kitty.evaluate(createEvent({ key: 'Dead' }), flags);
assert.strictEqual(result.key, undefined);
});
it('Unidentified key produces no output', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Unidentified' }), flags);
const result = kitty.evaluate(createEvent({ key: 'Unidentified' }), flags);
assert.strictEqual(result.key, undefined);
});
it('PrintScreen → CSI 57361 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'PrintScreen' }), flags);
const result = kitty.evaluate(createEvent({ key: 'PrintScreen' }), flags);
assert.strictEqual(result.key, '\x1b[57361u');
});
it('Pause → CSI 57362 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Pause' }), flags);
const result = kitty.evaluate(createEvent({ key: 'Pause' }), flags);
assert.strictEqual(result.key, '\x1b[57362u');
});
it('ContextMenu → CSI 57363 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'ContextMenu' }), flags);
const result = kitty.evaluate(createEvent({ key: 'ContextMenu' }), flags);
assert.strictEqual(result.key, '\x1b[57363u');
});
});
@@ -641,37 +647,37 @@ describe('KittyKeyboard', () => {
const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES;
it('MediaPlayPause → CSI 57430 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'MediaPlayPause' }), flags);
const result = kitty.evaluate(createEvent({ key: 'MediaPlayPause' }), flags);
assert.strictEqual(result.key, '\x1b[57430u');
});
it('MediaStop → CSI 57432 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'MediaStop' }), flags);
const result = kitty.evaluate(createEvent({ key: 'MediaStop' }), flags);
assert.strictEqual(result.key, '\x1b[57432u');
});
it('MediaTrackNext → CSI 57435 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'MediaTrackNext' }), flags);
const result = kitty.evaluate(createEvent({ key: 'MediaTrackNext' }), flags);
assert.strictEqual(result.key, '\x1b[57435u');
});
it('MediaTrackPrevious → CSI 57436 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'MediaTrackPrevious' }), flags);
const result = kitty.evaluate(createEvent({ key: 'MediaTrackPrevious' }), flags);
assert.strictEqual(result.key, '\x1b[57436u');
});
it('AudioVolumeDown → CSI 57438 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'AudioVolumeDown' }), flags);
const result = kitty.evaluate(createEvent({ key: 'AudioVolumeDown' }), flags);
assert.strictEqual(result.key, '\x1b[57438u');
});
it('AudioVolumeUp → CSI 57439 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'AudioVolumeUp' }), flags);
const result = kitty.evaluate(createEvent({ key: 'AudioVolumeUp' }), flags);
assert.strictEqual(result.key, '\x1b[57439u');
});
it('AudioVolumeMute → CSI 57440 u', () => {
const result = evaluateKeyboardEventKitty(createEvent({ key: 'AudioVolumeMute' }), flags);
const result = kitty.evaluate(createEvent({ key: 'AudioVolumeMute' }), flags);
assert.strictEqual(result.key, '\x1b[57440u');
});
});
+406 -433
View File
@@ -51,468 +51,441 @@ export const enum KittyKeyboardModifiers {
}
/**
* Functional key codes for Kitty protocol.
* Keys that don't produce text have specific unicode codepoint mappings.
* Kitty keyboard protocol handler class.
* Encapsulates all key code mappings and encoding logic.
*/
const FUNCTIONAL_KEY_CODES: { [key: string]: number } = {
'Escape': 27,
'Enter': 13,
'Tab': 9,
'Backspace': 127,
'CapsLock': 57358,
'ScrollLock': 57359,
'NumLock': 57360,
'PrintScreen': 57361,
'Pause': 57362,
'ContextMenu': 57363,
// F13-F35 (F1-F12 use legacy encoding)
'F13': 57376,
'F14': 57377,
'F15': 57378,
'F16': 57379,
'F17': 57380,
'F18': 57381,
'F19': 57382,
'F20': 57383,
'F21': 57384,
'F22': 57385,
'F23': 57386,
'F24': 57387,
'F25': 57388,
// Keypad keys
'KP_0': 57399,
'KP_1': 57400,
'KP_2': 57401,
'KP_3': 57402,
'KP_4': 57403,
'KP_5': 57404,
'KP_6': 57405,
'KP_7': 57406,
'KP_8': 57407,
'KP_9': 57408,
'KP_Decimal': 57409,
'KP_Divide': 57410,
'KP_Multiply': 57411,
'KP_Subtract': 57412,
'KP_Add': 57413,
'KP_Enter': 57414,
'KP_Equal': 57415,
// Modifier keys
'ShiftLeft': 57441,
'ShiftRight': 57447,
'ControlLeft': 57442,
'ControlRight': 57448,
'AltLeft': 57443,
'AltRight': 57449,
'MetaLeft': 57444,
'MetaRight': 57450,
// Media keys
'MediaPlayPause': 57430,
'MediaStop': 57432,
'MediaTrackNext': 57435,
'MediaTrackPrevious': 57436,
'AudioVolumeDown': 57438,
'AudioVolumeUp': 57439,
'AudioVolumeMute': 57440
};
/**
* Keys that use CSI ~ encoding with a number parameter.
*/
const CSI_TILDE_KEYS: { [key: string]: number } = {
'Insert': 2,
'Delete': 3,
'PageUp': 5,
'PageDown': 6,
'F5': 15,
'F6': 17,
'F7': 18,
'F8': 19,
'F9': 20,
'F10': 21,
'F11': 23,
'F12': 24
};
/**
* Keys that use CSI letter encoding (arrows, Home, End).
*/
const CSI_LETTER_KEYS: { [key: string]: string } = {
'ArrowUp': 'A',
'ArrowDown': 'B',
'ArrowRight': 'C',
'ArrowLeft': 'D',
'Home': 'H',
'End': 'F'
};
/**
* Function keys F1-F4 use SS3 encoding without modifiers.
*/
const SS3_FUNCTION_KEYS: { [key: string]: string } = {
'F1': 'P',
'F2': 'Q',
'F3': 'R',
'F4': 'S'
};
/**
* Map browser key codes to Kitty numpad codes.
*/
function getNumpadKeyCode(ev: IKeyboardEvent): number | undefined {
// Detect numpad via code property
if (ev.code.startsWith('Numpad')) {
const suffix = ev.code.slice(6);
if (suffix >= '0' && suffix <= '9') {
return 57399 + parseInt(suffix, 10);
}
switch (suffix) {
case 'Decimal': return 57409;
case 'Divide': return 57410;
case 'Multiply': return 57411;
case 'Subtract': return 57412;
case 'Add': return 57413;
case 'Enter': return 57414;
case 'Equal': return 57415;
}
}
return undefined;
}
/**
* Get modifier key code from code property.
*/
function getModifierKeyCode(ev: IKeyboardEvent): number | undefined {
switch (ev.code) {
case 'ShiftLeft': return 57441;
case 'ShiftRight': return 57447;
case 'ControlLeft': return 57442;
case 'ControlRight': return 57448;
case 'AltLeft': return 57443;
case 'AltRight': return 57449;
case 'MetaLeft': return 57444;
case 'MetaRight': return 57450;
}
return undefined;
}
/**
* Encode modifiers for Kitty protocol.
* Returns 1 + modifier bits, or 0 if no modifiers.
*/
function encodeModifiers(ev: IKeyboardEvent): number {
let mods = 0;
if (ev.shiftKey) mods |= KittyKeyboardModifiers.SHIFT;
if (ev.altKey) mods |= KittyKeyboardModifiers.ALT;
if (ev.ctrlKey) mods |= KittyKeyboardModifiers.CTRL;
if (ev.metaKey) mods |= KittyKeyboardModifiers.SUPER;
return mods > 0 ? mods + 1 : 0;
}
/**
* Get the unicode key code for a keyboard event.
* Returns the lowercase codepoint for letters.
* For shifted keys, uses the code property to get the base key.
*/
function getKeyCode(ev: IKeyboardEvent): number | undefined {
// Check for numpad first
const numpadCode = getNumpadKeyCode(ev);
if (numpadCode !== undefined) {
return numpadCode;
}
// Check for modifier keys
const modifierCode = getModifierKeyCode(ev);
if (modifierCode !== undefined) {
return modifierCode;
}
// Check functional keys
const funcCode = FUNCTIONAL_KEY_CODES[ev.key];
if (funcCode !== undefined) {
return funcCode;
}
// For shifted keys, use code property to get base key
if (ev.shiftKey && ev.code) {
// Handle Digit0-Digit9
if (ev.code.startsWith('Digit') && ev.code.length === 6) {
const digit = ev.code.charAt(5);
if (digit >= '0' && digit <= '9') {
return digit.charCodeAt(0);
}
}
// Handle KeyA-KeyZ
if (ev.code.startsWith('Key') && ev.code.length === 4) {
const letter = ev.code.charAt(3).toLowerCase();
return letter.charCodeAt(0);
}
}
// For regular keys, use the key character's codepoint
// Always use lowercase for letters (per spec)
if (ev.key.length === 1) {
const code = ev.key.codePointAt(0)!;
// Convert uppercase A-Z to lowercase a-z
if (code >= 65 && code <= 90) {
return code + 32;
}
return code;
}
return undefined;
}
/**
* Check if a key is a modifier key.
*/
function isModifierKey(ev: IKeyboardEvent): boolean {
return ev.key === 'Shift' || ev.key === 'Control' || ev.key === 'Alt' || ev.key === 'Meta';
}
/**
* Evaluate a keyboard event using Kitty keyboard protocol.
*
* @param ev The keyboard event.
* @param flags The active Kitty keyboard enhancement flags.
* @param eventType The event type (press, repeat, release).
* @returns The keyboard result with the encoded key sequence.
*/
export function evaluateKeyboardEventKitty(
ev: IKeyboardEvent,
flags: number,
eventType: KittyKeyboardEventType = KittyKeyboardEventType.PRESS
): IKeyboardResult {
const result: IKeyboardResult = {
type: KeyboardResultType.SEND_KEY,
cancel: false,
key: undefined
export class KittyKeyboard {
/**
* Functional key codes for Kitty protocol.
* Keys that don't produce text have specific unicode codepoint mappings.
*/
private readonly _functionalKeyCodes: { [key: string]: number } = {
'Escape': 27,
'Enter': 13,
'Tab': 9,
'Backspace': 127,
'CapsLock': 57358,
'ScrollLock': 57359,
'NumLock': 57360,
'PrintScreen': 57361,
'Pause': 57362,
'ContextMenu': 57363,
// F13-F35 (F1-F12 use legacy encoding)
'F13': 57376,
'F14': 57377,
'F15': 57378,
'F16': 57379,
'F17': 57380,
'F18': 57381,
'F19': 57382,
'F20': 57383,
'F21': 57384,
'F22': 57385,
'F23': 57386,
'F24': 57387,
'F25': 57388,
// Keypad keys
'KP_0': 57399,
'KP_1': 57400,
'KP_2': 57401,
'KP_3': 57402,
'KP_4': 57403,
'KP_5': 57404,
'KP_6': 57405,
'KP_7': 57406,
'KP_8': 57407,
'KP_9': 57408,
'KP_Decimal': 57409,
'KP_Divide': 57410,
'KP_Multiply': 57411,
'KP_Subtract': 57412,
'KP_Add': 57413,
'KP_Enter': 57414,
'KP_Equal': 57415,
// Modifier keys
'ShiftLeft': 57441,
'ShiftRight': 57447,
'ControlLeft': 57442,
'ControlRight': 57448,
'AltLeft': 57443,
'AltRight': 57449,
'MetaLeft': 57444,
'MetaRight': 57450,
// Media keys
'MediaPlayPause': 57430,
'MediaStop': 57432,
'MediaTrackNext': 57435,
'MediaTrackPrevious': 57436,
'AudioVolumeDown': 57438,
'AudioVolumeUp': 57439,
'AudioVolumeMute': 57440
};
const modifiers = encodeModifiers(ev);
const isMod = isModifierKey(ev);
const reportEventTypes = !!(flags & KittyKeyboardFlags.REPORT_EVENT_TYPES);
/**
* Keys that use CSI ~ encoding with a number parameter.
*/
private readonly _csiTildeKeys: { [key: string]: number } = {
'Insert': 2,
'Delete': 3,
'PageUp': 5,
'PageDown': 6,
'F5': 15,
'F6': 17,
'F7': 18,
'F8': 19,
'F9': 20,
'F10': 21,
'F11': 23,
'F12': 24
};
// Don't report release events unless flag is set
if (!reportEventTypes && eventType === KittyKeyboardEventType.RELEASE) {
return result;
}
/**
* Keys that use CSI letter encoding (arrows, Home, End).
*/
private readonly _csiLetterKeys: { [key: string]: string } = {
'ArrowUp': 'A',
'ArrowDown': 'B',
'ArrowRight': 'C',
'ArrowLeft': 'D',
'Home': 'H',
'End': 'F'
};
// Modifier-only keys require REPORT_ALL_KEYS_AS_ESCAPE_CODES or REPORT_EVENT_TYPES
if (isMod && !(flags & KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES) && !reportEventTypes) {
return result;
}
/**
* Function keys F1-F4 use SS3 encoding without modifiers.
*/
private readonly _ss3FunctionKeys: { [key: string]: string } = {
'F1': 'P',
'F2': 'Q',
'F3': 'R',
'F4': 'S'
};
// Check for CSI letter keys (arrows, Home, End)
const csiLetter = CSI_LETTER_KEYS[ev.key];
if (csiLetter) {
result.key = buildCsiLetterSequence(csiLetter, modifiers, eventType, reportEventTypes);
result.cancel = true;
return result;
}
// Check for SS3/CSI function keys (F1-F4)
const ss3Letter = SS3_FUNCTION_KEYS[ev.key];
if (ss3Letter) {
result.key = buildSs3Sequence(ss3Letter, modifiers, eventType, reportEventTypes);
result.cancel = true;
return result;
}
// Check for CSI ~ keys (Insert, Delete, PageUp/Down, F5-F12)
const tildeCode = CSI_TILDE_KEYS[ev.key];
if (tildeCode !== undefined) {
result.key = buildCsiTildeSequence(tildeCode, modifiers, eventType, reportEventTypes);
result.cancel = true;
return result;
}
// Get the key code for CSI u encoding
const keyCode = getKeyCode(ev);
if (keyCode === undefined) {
return result;
}
const isFunc = FUNCTIONAL_KEY_CODES[ev.key] !== undefined || getNumpadKeyCode(ev) !== undefined;
// Determine if we should use CSI u encoding
let useCsiU = false;
if (flags & KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES) {
useCsiU = true;
} else if (reportEventTypes) {
useCsiU = true;
} else if (flags & KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES) {
// Modifier-only keys already handled above
// Use CSI u for keys that would be ambiguous in legacy encoding
if (keyCode === 27 || keyCode === 127 || keyCode === 13 || keyCode === 9 || keyCode === 32) {
// Escape, Backspace, Enter, Tab, Space
useCsiU = true;
} else if (isFunc) {
useCsiU = true;
} else if (modifiers > 0) {
// Shift-only + printable character (e.g., Shift+a → "A") should NOT use CSI u
// per Kitty spec: DISAMBIGUATE only encodes keys ambiguous in legacy encoding
if (ev.shiftKey && !ev.ctrlKey && !ev.altKey && !ev.metaKey && ev.key.length === 1) {
useCsiU = false;
} else {
useCsiU = true;
/**
* Map browser key codes to Kitty numpad codes.
*/
private _getNumpadKeyCode(ev: IKeyboardEvent): number | undefined {
if (ev.code.startsWith('Numpad')) {
const suffix = ev.code.slice(6);
if (suffix >= '0' && suffix <= '9') {
return 57399 + parseInt(suffix, 10);
}
switch (suffix) {
case 'Decimal': return 57409;
case 'Divide': return 57410;
case 'Multiply': return 57411;
case 'Subtract': return 57412;
case 'Add': return 57413;
case 'Enter': return 57414;
case 'Equal': return 57415;
}
}
return undefined;
}
if (useCsiU) {
result.key = buildCsiUSequence(ev, keyCode, modifiers, eventType, flags, isFunc, isMod);
result.cancel = true;
} else {
// Legacy-compatible encoding for text keys without modifiers
if (ev.key.length === 1 && !ev.ctrlKey && !ev.altKey && !ev.metaKey) {
result.key = ev.key;
/**
* Get modifier key code from code property.
*/
private _getModifierKeyCode(ev: IKeyboardEvent): number | undefined {
switch (ev.code) {
case 'ShiftLeft': return 57441;
case 'ShiftRight': return 57447;
case 'ControlLeft': return 57442;
case 'ControlRight': return 57448;
case 'AltLeft': return 57443;
case 'AltRight': return 57449;
case 'MetaLeft': return 57444;
case 'MetaRight': return 57450;
}
return undefined;
}
return result;
}
/**
* Encode modifiers for Kitty protocol.
* Returns 1 + modifier bits, or 0 if no modifiers.
*/
private _encodeModifiers(ev: IKeyboardEvent): number {
let mods = 0;
if (ev.shiftKey) mods |= KittyKeyboardModifiers.SHIFT;
if (ev.altKey) mods |= KittyKeyboardModifiers.ALT;
if (ev.ctrlKey) mods |= KittyKeyboardModifiers.CTRL;
if (ev.metaKey) mods |= KittyKeyboardModifiers.SUPER;
return mods > 0 ? mods + 1 : 0;
}
/**
* Build CSI letter sequence for arrow keys, Home, End.
* Format: CSI [1;mod] letter
*/
function buildCsiLetterSequence(
letter: string,
modifiers: number,
eventType: KittyKeyboardEventType,
reportEventTypes: boolean
): string {
const needsEventType = reportEventTypes && eventType !== KittyKeyboardEventType.PRESS;
if (modifiers > 0 || needsEventType) {
let seq = C0.ESC + '[1;' + (modifiers > 0 ? modifiers : '1');
if (needsEventType) {
seq += ':' + eventType;
/**
* Get the unicode key code for a keyboard event.
* Returns the lowercase codepoint for letters.
* For shifted keys, uses the code property to get the base key.
*/
private _getKeyCode(ev: IKeyboardEvent): number | undefined {
const numpadCode = this._getNumpadKeyCode(ev);
if (numpadCode !== undefined) {
return numpadCode;
}
seq += letter;
const modifierCode = this._getModifierKeyCode(ev);
if (modifierCode !== undefined) {
return modifierCode;
}
const funcCode = this._functionalKeyCodes[ev.key];
if (funcCode !== undefined) {
return funcCode;
}
if (ev.shiftKey && ev.code) {
if (ev.code.startsWith('Digit') && ev.code.length === 6) {
const digit = ev.code.charAt(5);
if (digit >= '0' && digit <= '9') {
return digit.charCodeAt(0);
}
}
if (ev.code.startsWith('Key') && ev.code.length === 4) {
const letter = ev.code.charAt(3).toLowerCase();
return letter.charCodeAt(0);
}
}
if (ev.key.length === 1) {
const code = ev.key.codePointAt(0)!;
if (code >= 65 && code <= 90) {
return code + 32;
}
return code;
}
return undefined;
}
/**
* Check if a key is a modifier key.
*/
private _isModifierKey(ev: IKeyboardEvent): boolean {
return ev.key === 'Shift' || ev.key === 'Control' || ev.key === 'Alt' || ev.key === 'Meta';
}
/**
* Build CSI letter sequence for arrow keys, Home, End.
* Format: CSI [1;mod] letter
*/
private _buildCsiLetterSequence(
letter: string,
modifiers: number,
eventType: KittyKeyboardEventType,
reportEventTypes: boolean
): string {
const needsEventType = reportEventTypes && eventType !== KittyKeyboardEventType.PRESS;
if (modifiers > 0 || needsEventType) {
let seq = C0.ESC + '[1;' + (modifiers > 0 ? modifiers : '1');
if (needsEventType) {
seq += ':' + eventType;
}
seq += letter;
return seq;
}
return C0.ESC + '[' + letter;
}
/**
* Build SS3 sequence for F1-F4.
* Without modifiers: SS3 letter
* With modifiers: CSI 1;mod letter
*/
private _buildSs3Sequence(
letter: string,
modifiers: number,
eventType: KittyKeyboardEventType,
reportEventTypes: boolean
): string {
const needsEventType = reportEventTypes && eventType !== KittyKeyboardEventType.PRESS;
if (modifiers > 0 || needsEventType) {
let seq = C0.ESC + '[1;' + (modifiers > 0 ? modifiers : '1');
if (needsEventType) {
seq += ':' + eventType;
}
seq += letter;
return seq;
}
return C0.ESC + 'O' + letter;
}
/**
* Build CSI ~ sequence for Insert, Delete, PageUp/Down, F5-F12.
* Format: CSI number [;mod[:event]] ~
*/
private _buildCsiTildeSequence(
number: number,
modifiers: number,
eventType: KittyKeyboardEventType,
reportEventTypes: boolean
): string {
const needsEventType = reportEventTypes && eventType !== KittyKeyboardEventType.PRESS;
let seq = C0.ESC + '[' + number;
if (modifiers > 0 || needsEventType) {
seq += ';' + (modifiers > 0 ? modifiers : '1');
if (needsEventType) {
seq += ':' + eventType;
}
}
seq += '~';
return seq;
}
return C0.ESC + '[' + letter;
}
/**
* Build SS3 sequence for F1-F4.
* Without modifiers: SS3 letter
* With modifiers: CSI 1;mod letter
*/
function buildSs3Sequence(
letter: string,
modifiers: number,
eventType: KittyKeyboardEventType,
reportEventTypes: boolean
): string {
const needsEventType = reportEventTypes && eventType !== KittyKeyboardEventType.PRESS;
/**
* Build CSI u sequence.
* Format: CSI keycode[:shifted[:base]] [;mod[:event][;text]] u
*/
private _buildCsiUSequence(
ev: IKeyboardEvent,
keyCode: number,
modifiers: number,
eventType: KittyKeyboardEventType,
flags: number,
isFunc: boolean,
isMod: boolean
): string {
const reportEventTypes = !!(flags & KittyKeyboardFlags.REPORT_EVENT_TYPES);
const reportAlternateKeys = !!(flags & KittyKeyboardFlags.REPORT_ALTERNATE_KEYS);
if (modifiers > 0 || needsEventType) {
let seq = C0.ESC + '[1;' + (modifiers > 0 ? modifiers : '1');
if (needsEventType) {
seq += ':' + eventType;
let seq = C0.ESC + '[' + keyCode;
let shiftedKey: number | undefined;
if (reportAlternateKeys && ev.shiftKey && ev.key.length === 1 && !isFunc && !isMod) {
shiftedKey = ev.key.codePointAt(0);
seq += ':' + shiftedKey;
}
seq += letter;
const reportAssociatedText = !!(flags & KittyKeyboardFlags.REPORT_ASSOCIATED_TEXT) &&
eventType !== KittyKeyboardEventType.RELEASE &&
ev.key.length === 1 &&
!isFunc &&
!isMod &&
!ev.ctrlKey;
const textCode = reportAssociatedText ? ev.key.codePointAt(0) : undefined;
const needsEventType = reportEventTypes &&
eventType !== KittyKeyboardEventType.PRESS &&
(eventType === KittyKeyboardEventType.RELEASE || textCode === undefined);
if (modifiers > 0 || needsEventType || textCode !== undefined) {
seq += ';';
if (modifiers > 0) {
seq += modifiers;
} else if (needsEventType) {
seq += '1';
}
if (needsEventType) {
seq += ':' + eventType;
}
}
if (textCode !== undefined) {
seq += ';' + textCode;
}
seq += 'u';
return seq;
}
return C0.ESC + 'O' + letter;
}
/**
* Build CSI ~ sequence for Insert, Delete, PageUp/Down, F5-F12.
* Format: CSI number [;mod[:event]] ~
*/
function buildCsiTildeSequence(
number: number,
modifiers: number,
eventType: KittyKeyboardEventType,
reportEventTypes: boolean
): string {
const needsEventType = reportEventTypes && eventType !== KittyKeyboardEventType.PRESS;
/**
* Evaluate a keyboard event using Kitty keyboard protocol.
*
* @param ev The keyboard event.
* @param flags The active Kitty keyboard enhancement flags.
* @param eventType The event type (press, repeat, release).
* @returns The keyboard result with the encoded key sequence.
*/
public evaluate(
ev: IKeyboardEvent,
flags: number,
eventType: KittyKeyboardEventType = KittyKeyboardEventType.PRESS
): IKeyboardResult {
const result: IKeyboardResult = {
type: KeyboardResultType.SEND_KEY,
cancel: false,
key: undefined
};
let seq = C0.ESC + '[' + number;
if (modifiers > 0 || needsEventType) {
seq += ';' + (modifiers > 0 ? modifiers : '1');
if (needsEventType) {
seq += ':' + eventType;
const modifiers = this._encodeModifiers(ev);
const isMod = this._isModifierKey(ev);
const reportEventTypes = !!(flags & KittyKeyboardFlags.REPORT_EVENT_TYPES);
if (!reportEventTypes && eventType === KittyKeyboardEventType.RELEASE) {
return result;
}
}
seq += '~';
return seq;
}
/**
* Build CSI u sequence.
* Format: CSI keycode[:shifted[:base]] [;mod[:event][;text]] u
*/
function buildCsiUSequence(
ev: IKeyboardEvent,
keyCode: number,
modifiers: number,
eventType: KittyKeyboardEventType,
flags: number,
isFunc: boolean,
isMod: boolean
): string {
const reportEventTypes = !!(flags & KittyKeyboardFlags.REPORT_EVENT_TYPES);
const reportAlternateKeys = !!(flags & KittyKeyboardFlags.REPORT_ALTERNATE_KEYS);
let seq = C0.ESC + '[' + keyCode;
// Add shifted key alternate if REPORT_ALTERNATE_KEYS is set and shift is pressed
// Only for text-producing keys (not functional or modifier keys)
let shiftedKey: number | undefined;
if (reportAlternateKeys && ev.shiftKey && ev.key.length === 1 && !isFunc && !isMod) {
shiftedKey = ev.key.codePointAt(0);
seq += ':' + shiftedKey;
}
// Check if we need associated text (press and repeat events, not release)
// Only for text-producing keys (not functional or modifier keys)
// Also don't include text when ctrl is pressed (produces control code)
const reportAssociatedText = !!(flags & KittyKeyboardFlags.REPORT_ASSOCIATED_TEXT) &&
eventType !== KittyKeyboardEventType.RELEASE &&
ev.key.length === 1 &&
!isFunc &&
!isMod &&
!ev.ctrlKey;
const textCode = reportAssociatedText ? ev.key.codePointAt(0) : undefined;
// Determine if we need event type suffix
// For repeat: only include :2 when there's no text (text implies it's still useful input)
// For release: always include :3
const needsEventType = reportEventTypes &&
eventType !== KittyKeyboardEventType.PRESS &&
(eventType === KittyKeyboardEventType.RELEASE || textCode === undefined);
if (modifiers > 0 || needsEventType || textCode !== undefined) {
seq += ';';
if (modifiers > 0) {
seq += modifiers;
} else if (needsEventType) {
seq += '1';
if (isMod && !(flags & KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES) && !reportEventTypes) {
return result;
}
if (needsEventType) {
seq += ':' + eventType;
const csiLetter = this._csiLetterKeys[ev.key];
if (csiLetter) {
result.key = this._buildCsiLetterSequence(csiLetter, modifiers, eventType, reportEventTypes);
result.cancel = true;
return result;
}
const ss3Letter = this._ss3FunctionKeys[ev.key];
if (ss3Letter) {
result.key = this._buildSs3Sequence(ss3Letter, modifiers, eventType, reportEventTypes);
result.cancel = true;
return result;
}
const tildeCode = this._csiTildeKeys[ev.key];
if (tildeCode !== undefined) {
result.key = this._buildCsiTildeSequence(tildeCode, modifiers, eventType, reportEventTypes);
result.cancel = true;
return result;
}
const keyCode = this._getKeyCode(ev);
if (keyCode === undefined) {
return result;
}
const isFunc = this._functionalKeyCodes[ev.key] !== undefined || this._getNumpadKeyCode(ev) !== undefined;
let useCsiU = false;
if (flags & KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES) {
useCsiU = true;
} else if (reportEventTypes) {
useCsiU = true;
} else if (flags & KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES) {
if (keyCode === 27 || keyCode === 127 || keyCode === 13 || keyCode === 9 || keyCode === 32) {
useCsiU = true;
} else if (isFunc) {
useCsiU = true;
} else if (modifiers > 0) {
if (ev.shiftKey && !ev.ctrlKey && !ev.altKey && !ev.metaKey && ev.key.length === 1) {
useCsiU = false;
} else {
useCsiU = true;
}
}
}
if (useCsiU) {
result.key = this._buildCsiUSequence(ev, keyCode, modifiers, eventType, flags, isFunc, isMod);
result.cancel = true;
} else {
if (ev.key.length === 1 && !ev.ctrlKey && !ev.altKey && !ev.metaKey) {
result.key = ev.key;
}
}
return result;
}
// Add associated text if requested
if (textCode !== undefined) {
seq += ';' + textCode;
/**
* Check if Kitty protocol should be used based on flags.
*/
public static shouldUseProtocol(flags: number): boolean {
return flags > 0;
}
seq += 'u';
return seq;
}
/**
* Check if a keyboard event should be handled by Kitty protocol.
* Returns true if Kitty flags are active and the event should use Kitty encoding.
*/
export function shouldUseKittyProtocol(flags: number): boolean {
return flags > 0;
}