diff --git a/addons/addon-attach/src/AttachAddon.ts b/addons/addon-attach/src/AttachAddon.ts index 37a10d17..9463dc36 100644 --- a/addons/addon-attach/src/AttachAddon.ts +++ b/addons/addon-attach/src/AttachAddon.ts @@ -12,7 +12,7 @@ interface IAttachOptions { bidirectional?: boolean; } -export class AttachAddon implements ITerminalAddon , IAttachApi { +export class AttachAddon implements ITerminalAddon, IAttachApi { private _socket: WebSocket; private _bidirectional: boolean; private _disposables: IDisposable[] = []; diff --git a/addons/addon-fit/src/FitAddon.ts b/addons/addon-fit/src/FitAddon.ts index 23004c1c..3b06fb73 100644 --- a/addons/addon-fit/src/FitAddon.ts +++ b/addons/addon-fit/src/FitAddon.ts @@ -33,7 +33,7 @@ function _getComputedStyle(el: HTMLElement): CSSStyleDeclaration { return getWindow(el).getComputedStyle(el, null); } -export class FitAddon implements ITerminalAddon , IFitApi { +export class FitAddon implements ITerminalAddon, IFitApi { private _terminal: Terminal | undefined; public activate(terminal: Terminal): void { diff --git a/addons/addon-image/src/ImageAddon.ts b/addons/addon-image/src/ImageAddon.ts index 6c7fe5ab..d18b59a0 100644 --- a/addons/addon-image/src/ImageAddon.ts +++ b/addons/addon-image/src/ImageAddon.ts @@ -48,7 +48,7 @@ const enum GaStatus { } -export class ImageAddon implements ITerminalAddon , IImageApi { +export class ImageAddon implements ITerminalAddon, IImageApi { private _opts: IImageAddonOptions; private _defaultOpts: IImageAddonOptions; private _storage: ImageStorage | undefined; diff --git a/addons/addon-ligatures/src/LigaturesAddon.ts b/addons/addon-ligatures/src/LigaturesAddon.ts index 463b0d26..5bba67e8 100644 --- a/addons/addon-ligatures/src/LigaturesAddon.ts +++ b/addons/addon-ligatures/src/LigaturesAddon.ts @@ -13,7 +13,7 @@ export interface ITerminalAddon { dispose(): void; } -export class LigaturesAddon implements ITerminalAddon , ILigaturesApi { +export class LigaturesAddon implements ITerminalAddon, ILigaturesApi { private readonly _fallbackLigatures: string[]; private readonly _fontFeatureSettings?: string; diff --git a/addons/addon-serialize/src/SerializeAddon.ts b/addons/addon-serialize/src/SerializeAddon.ts index 4b06ae70..deb3e2cc 100644 --- a/addons/addon-serialize/src/SerializeAddon.ts +++ b/addons/addon-serialize/src/SerializeAddon.ts @@ -457,7 +457,7 @@ class StringSerializeHandler extends BaseSerializeHandler { } } -export class SerializeAddon implements ITerminalAddon , ISerializeApi { +export class SerializeAddon implements ITerminalAddon, ISerializeApi { private _terminal: Terminal | undefined; public activate(terminal: Terminal): void { diff --git a/addons/addon-unicode-graphemes/src/UnicodeGraphemesAddon.ts b/addons/addon-unicode-graphemes/src/UnicodeGraphemesAddon.ts index e40f2076..574a36cc 100644 --- a/addons/addon-unicode-graphemes/src/UnicodeGraphemesAddon.ts +++ b/addons/addon-unicode-graphemes/src/UnicodeGraphemesAddon.ts @@ -9,7 +9,7 @@ import type { Terminal, ITerminalAddon, IUnicodeHandling } from '@xterm/xterm'; import type { UnicodeGraphemesAddon as IUnicodeGraphemesApi } from '@xterm/addon-unicode-graphemes'; import { UnicodeGraphemeProvider } from './UnicodeGraphemeProvider'; -export class UnicodeGraphemesAddon implements ITerminalAddon , IUnicodeGraphemesApi { +export class UnicodeGraphemesAddon implements ITerminalAddon, IUnicodeGraphemesApi { private _provider15Graphemes?: UnicodeGraphemeProvider; private _provider15?: UnicodeGraphemeProvider; private _unicode?: IUnicodeHandling; diff --git a/addons/addon-unicode11/src/Unicode11Addon.ts b/addons/addon-unicode11/src/Unicode11Addon.ts index 301145ee..897f4f40 100644 --- a/addons/addon-unicode11/src/Unicode11Addon.ts +++ b/addons/addon-unicode11/src/Unicode11Addon.ts @@ -9,7 +9,7 @@ import type { Terminal, ITerminalAddon } from '@xterm/xterm'; import type { Unicode11Addon as IUnicode11Api } from '@xterm/addon-unicode11'; import { UnicodeV11 } from './UnicodeV11'; -export class Unicode11Addon implements ITerminalAddon , IUnicode11Api { +export class Unicode11Addon implements ITerminalAddon, IUnicode11Api { public activate(terminal: Terminal): void { terminal.unicode.register(new UnicodeV11()); } diff --git a/addons/addon-web-links/src/WebLinksAddon.ts b/addons/addon-web-links/src/WebLinksAddon.ts index 56491e85..3328a3c3 100644 --- a/addons/addon-web-links/src/WebLinksAddon.ts +++ b/addons/addon-web-links/src/WebLinksAddon.ts @@ -35,7 +35,7 @@ function handleLink(event: MouseEvent, uri: string): void { } } -export class WebLinksAddon implements ITerminalAddon , IWebLinksApi { +export class WebLinksAddon implements ITerminalAddon, IWebLinksApi { private _terminal: Terminal | undefined; private _linkProvider: IDisposable | undefined; diff --git a/addons/addon-webgl/src/WebglAddon.ts b/addons/addon-webgl/src/WebglAddon.ts index 7dc3ed35..f21fb704 100644 --- a/addons/addon-webgl/src/WebglAddon.ts +++ b/addons/addon-webgl/src/WebglAddon.ts @@ -9,13 +9,12 @@ import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRender import { ITerminal } from 'browser/Types'; import { Disposable, toDisposable } from 'common/Lifecycle'; import { getSafariVersion, isSafari } from 'common/Platform'; -import { ICoreService, IDecorationService, ILogService, IOptionsService } from 'common/services/Services'; +import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; import { IWebGL2RenderingContext } from './Types'; import { WebglRenderer } from './WebglRenderer'; -import { setTraceLogger } from 'common/services/LogService'; import { Emitter, EventUtils } from 'common/Event'; -export class WebglAddon extends Disposable implements ITerminalAddon , IWebglApi { +export class WebglAddon extends Disposable implements ITerminalAddon, IWebglApi { private _terminal?: Terminal; private _renderer?: WebglRenderer; @@ -66,13 +65,8 @@ export class WebglAddon extends Disposable implements ITerminalAddon , IWebglApi const charSizeService: ICharSizeService = unsafeCore._charSizeService; const coreBrowserService: ICoreBrowserService = unsafeCore._coreBrowserService; const decorationService: IDecorationService = unsafeCore._decorationService; - const logService: ILogService = unsafeCore._logService; const themeService: IThemeService = unsafeCore._themeService; - // Set trace logger just in case it hasn't been yet which could happen when the addon is - // bundled separately to the core module - setTraceLogger(logService); - this._renderer = this._register(new WebglRenderer( terminal, characterJoinerService, diff --git a/eslint.config.mjs b/eslint.config.mjs index d630c9dc..3e62a3af 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -35,6 +35,7 @@ export default tseslint.config( } }, rules: { + '@stylistic/comma-spacing': ['warn', { before: false, after: true }], '@stylistic/indent': ['warn', 2], '@stylistic/semi': ['warn', 'always'], '@stylistic/quotes': ['warn', 'single', { allowTemplateLiterals: true }], diff --git a/package-lock.json b/package-lock.json index 4c218b70..f20b41a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,7 +41,7 @@ "jsdom": "^27.3.0", "mocha": "^10.1.0", "mustache": "^4.2.0", - "node-pty": "1.1.0-beta19", + "node-pty": "^1.2.0-beta.9", "nyc": "^17.1.0", "source-map-loader": "^3.0.0", "source-map-support": "^0.5.20", @@ -6016,11 +6016,12 @@ } }, "node_modules/node-pty": { - "version": "1.1.0-beta19", - "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0-beta19.tgz", - "integrity": "sha512-/p4Zu56EYDdXjjaLWzrIlFyrBnND11LQGP0/L6GEVGURfCNkAlHc3Twg/2I4NPxghimHXgvDlwp7Z2GtvDIh8A==", + "version": "1.2.0-beta.9", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.2.0-beta.9.tgz", + "integrity": "sha512-XR3x/4OQZ7DlceespOQ99kk9jZHwmO78RuEIZtGsSPdsDi11s79VcK04aqcaK3x09Y6ORJM/KBz/oQ/p+WGRLg==", "dev": true, "hasInstallScript": true, + "license": "MIT", "dependencies": { "node-addon-api": "^7.1.0" } diff --git a/package.json b/package.json index 9b54946c..c319f0b3 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ "jsdom": "^27.3.0", "mocha": "^10.1.0", "mustache": "^4.2.0", - "node-pty": "1.1.0-beta19", + "node-pty": "^1.2.0-beta.9", "nyc": "^17.1.0", "source-map-loader": "^3.0.0", "source-map-support": "^0.5.20", diff --git a/src/browser/renderer/shared/RendererUtils.test.ts b/src/browser/renderer/shared/RendererUtils.test.ts index a050e8a9..4512cb6d 100644 --- a/src/browser/renderer/shared/RendererUtils.test.ts +++ b/src/browser/renderer/shared/RendererUtils.test.ts @@ -28,7 +28,7 @@ describe('RendererUtils', () => { line = 2; variantOffset = 0; cells = [cellWidth, cellWidth, doubleCellWidth, doubleCellWidth]; - result = [3, 2, 0 ,2]; + result = [3, 2, 0, 2]; for (let index = 0; index < cells.length; index++) { const cell = cells[index]; variantOffset = computeNextVariantOffset(cell, line, variantOffset); diff --git a/src/browser/services/KeyboardService.ts b/src/browser/services/KeyboardService.ts index 478a262e..d9e6c1ee 100644 --- a/src/browser/services/KeyboardService.ts +++ b/src/browser/services/KeyboardService.ts @@ -5,8 +5,8 @@ import { IKeyboardService } from 'browser/services/Services'; import { evaluateKeyboardEvent } from 'common/input/Keyboard'; -import { evaluateKeyboardEventKitty, KittyKeyboardEventType, KittyKeyboardFlags, shouldUseKittyProtocol } from 'common/input/KittyKeyboard'; -import { evaluateKeyboardEventWin32 } from 'common/input/Win32InputMode'; +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'; import { IKeyboardResult } from 'common/Types'; @@ -14,38 +14,55 @@ import { IKeyboardResult } from 'common/Types'; export class KeyboardService implements IKeyboardService { public serviceBrand: undefined; + private _win32InputMode: Win32InputMode | undefined; + private _kittyKeyboard: KittyKeyboard | undefined; + constructor( @ICoreService private readonly _coreService: ICoreService, @IOptionsService private readonly _optionsService: IOptionsService ) { } + private _getWin32InputMode(): Win32InputMode { + if (!this._win32InputMode) { + this._win32InputMode = new Win32InputMode(); + } + 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) { - return evaluateKeyboardEventWin32(event, true); + return this._getWin32InputMode().evaluateKeyboardEvent(event, true); } 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); } public evaluateKeyUp(event: KeyboardEvent): IKeyboardResult | undefined { // Win32 input mode sends key up events if (this.useWin32InputMode) { - return evaluateKeyboardEventWin32(event, false); + return this._getWin32InputMode().evaluateKeyboardEvent(event, false); } 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 { diff --git a/src/common/Platform.ts b/src/common/Platform.ts index ec34acde..cc13c1af 100644 --- a/src/common/Platform.ts +++ b/src/common/Platform.ts @@ -39,8 +39,6 @@ export function getSafariVersion(): number { // and ISO third level shifts. // http://stackoverflow.com/q/19877924/577598 export const isMac = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'].includes(platform); -export const isIpad = platform === 'iPad'; -export const isIphone = platform === 'iPhone'; export const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].includes(platform); export const isLinux = platform.indexOf('Linux') >= 0; // Note that when this is true, isLinux will also be true. diff --git a/src/common/TypedArrayUtils.test.ts b/src/common/TypedArrayUtils.test.ts deleted file mode 100644 index 6390854b..00000000 --- a/src/common/TypedArrayUtils.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) 2018 The xterm.js authors. All rights reserved. - * @license MIT - */ -import { assert } from 'chai'; -import { concat } from 'common/TypedArrayUtils'; - -type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray | Int8Array | Int16Array | Int32Array | Float32Array | Float64Array; - -function deepEquals(a: TypedArray, b: TypedArray): void { - assert.equal(a.length, b.length); - for (let i = 0; i < a.length; ++i) { - assert.equal(a[i], b[i]); - } -} - -describe('typed array convenience functions', () => { - it('concat', () => { - const a = new Uint8Array([1, 2, 3, 4, 5]); - const b = new Uint8Array([6, 7, 8, 9, 0]); - const merged = concat(a, b); - deepEquals(merged, new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 0])); - }); -}); diff --git a/src/common/TypedArrayUtils.ts b/src/common/TypedArrayUtils.ts deleted file mode 100644 index f3bacd50..00000000 --- a/src/common/TypedArrayUtils.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2018 The xterm.js authors. All rights reserved. - * @license MIT - */ - -export type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray | Int8Array | Int16Array | Int32Array | Float32Array | Float64Array; - -/** - * Concat two typed arrays `a` and `b`. - * Returns a new typed array. - */ -export function concat(a: T, b: T): T { - const result = new (a.constructor as any)(a.length + b.length); - result.set(a); - result.set(b, a.length); - return result; -} diff --git a/src/common/input/KittyKeyboard.test.ts b/src/common/input/KittyKeyboard.test.ts index 706eec15..af28a5b2 100644 --- a/src/common/input/KittyKeyboard.test.ts +++ b/src/common/input/KittyKeyboard.test.ts @@ -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 { return { @@ -17,71 +17,77 @@ function createEvent(partialEvent: Partial = {}): 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'); }); }); diff --git a/src/common/input/KittyKeyboard.ts b/src/common/input/KittyKeyboard.ts index f2d7388b..c9db3dad 100644 --- a/src/common/input/KittyKeyboard.ts +++ b/src/common/input/KittyKeyboard.ts @@ -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; } diff --git a/src/common/input/Win32InputMode.test.ts b/src/common/input/Win32InputMode.test.ts index e5dc98b4..d0f33359 100644 --- a/src/common/input/Win32InputMode.test.ts +++ b/src/common/input/Win32InputMode.test.ts @@ -4,7 +4,7 @@ */ import { assert } from 'chai'; -import { evaluateKeyboardEventWin32, Win32ControlKeyState } from 'common/input/Win32InputMode'; +import { Win32InputMode, Win32ControlKeyState } from 'common/input/Win32InputMode'; import { IKeyboardEvent, KeyboardResultType } from 'common/Types'; type EventOpts = Partial; @@ -18,18 +18,20 @@ const parse = (seq: string) => { return m ? { vk: +m[1], sc: +m[2], uc: +m[3], kd: +m[4], cs: +m[5], rc: +m[6] } : null; }; +const win32 = new Win32InputMode(); + const test = (opts: EventOpts, isDown: boolean, check: (p: ReturnType) => void) => { - const result = evaluateKeyboardEventWin32(ev(opts), isDown); + const result = win32.evaluateKeyboardEvent(ev(opts), isDown); const parsed = parse(result.key!); assert.ok(parsed); check(parsed); }; describe('Win32InputMode', () => { - describe('evaluateKeyboardEventWin32', () => { + describe('evaluateKeyboardEvent', () => { describe('basic key encoding', () => { it('letter key press', () => { - const result = evaluateKeyboardEventWin32(ev({ code: 'KeyA', key: 'a', keyCode: 65 }), true); + const result = win32.evaluateKeyboardEvent(ev({ code: 'KeyA', key: 'a', keyCode: 65 }), true); assert.strictEqual(result.type, KeyboardResultType.SEND_KEY); assert.strictEqual(result.cancel, true); const p = parse(result.key!); @@ -135,7 +137,7 @@ describe('Win32InputMode', () => { describe('sequence format', () => { it('valid CSI format', () => { - const result = evaluateKeyboardEventWin32(ev({ code: 'KeyA', key: 'a', keyCode: 65 }), true); + const result = win32.evaluateKeyboardEvent(ev({ code: 'KeyA', key: 'a', keyCode: 65 }), true); assert.ok(result.key?.startsWith('\x1b[') && result.key.endsWith('_')); assert.strictEqual(result.key?.slice(2, -1).split(';').length, 6); }); diff --git a/src/common/input/Win32InputMode.ts b/src/common/input/Win32InputMode.ts index 6818bc9f..72907836 100644 --- a/src/common/input/Win32InputMode.ts +++ b/src/common/input/Win32InputMode.ts @@ -33,274 +33,265 @@ export const enum Win32ControlKeyState { } /** - * Mapping from browser KeyboardEvent.code to Win32 virtual key codes. - * Based on https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes + * Win32 input mode handler. Lookup tables are only initialized when this class + * is instantiated, reducing bundle size for environments that don't use this mode. */ -const CODE_TO_VK: { [code: string]: number } = { - // Letters - 'KeyA': 0x41, 'KeyB': 0x42, 'KeyC': 0x43, 'KeyD': 0x44, 'KeyE': 0x45, - 'KeyF': 0x46, 'KeyG': 0x47, 'KeyH': 0x48, 'KeyI': 0x49, 'KeyJ': 0x4A, - 'KeyK': 0x4B, 'KeyL': 0x4C, 'KeyM': 0x4D, 'KeyN': 0x4E, 'KeyO': 0x4F, - 'KeyP': 0x50, 'KeyQ': 0x51, 'KeyR': 0x52, 'KeyS': 0x53, 'KeyT': 0x54, - 'KeyU': 0x55, 'KeyV': 0x56, 'KeyW': 0x57, 'KeyX': 0x58, 'KeyY': 0x59, - 'KeyZ': 0x5A, +export class Win32InputMode { + /** + * Mapping from browser KeyboardEvent.code to Win32 virtual key codes. + * Based on https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes + */ + private readonly _codeToVk: { [code: string]: number } = { + // Letters + 'KeyA': 0x41, 'KeyB': 0x42, 'KeyC': 0x43, 'KeyD': 0x44, 'KeyE': 0x45, + 'KeyF': 0x46, 'KeyG': 0x47, 'KeyH': 0x48, 'KeyI': 0x49, 'KeyJ': 0x4A, + 'KeyK': 0x4B, 'KeyL': 0x4C, 'KeyM': 0x4D, 'KeyN': 0x4E, 'KeyO': 0x4F, + 'KeyP': 0x50, 'KeyQ': 0x51, 'KeyR': 0x52, 'KeyS': 0x53, 'KeyT': 0x54, + 'KeyU': 0x55, 'KeyV': 0x56, 'KeyW': 0x57, 'KeyX': 0x58, 'KeyY': 0x59, + 'KeyZ': 0x5A, - // Digits - 'Digit0': 0x30, 'Digit1': 0x31, 'Digit2': 0x32, 'Digit3': 0x33, 'Digit4': 0x34, - 'Digit5': 0x35, 'Digit6': 0x36, 'Digit7': 0x37, 'Digit8': 0x38, 'Digit9': 0x39, + // Digits + 'Digit0': 0x30, 'Digit1': 0x31, 'Digit2': 0x32, 'Digit3': 0x33, 'Digit4': 0x34, + 'Digit5': 0x35, 'Digit6': 0x36, 'Digit7': 0x37, 'Digit8': 0x38, 'Digit9': 0x39, - // Function keys - 'F1': 0x70, 'F2': 0x71, 'F3': 0x72, 'F4': 0x73, 'F5': 0x74, 'F6': 0x75, - 'F7': 0x76, 'F8': 0x77, 'F9': 0x78, 'F10': 0x79, 'F11': 0x7A, 'F12': 0x7B, - 'F13': 0x7C, 'F14': 0x7D, 'F15': 0x7E, 'F16': 0x7F, 'F17': 0x80, 'F18': 0x81, - 'F19': 0x82, 'F20': 0x83, 'F21': 0x84, 'F22': 0x85, 'F23': 0x86, 'F24': 0x87, + // Function keys + 'F1': 0x70, 'F2': 0x71, 'F3': 0x72, 'F4': 0x73, 'F5': 0x74, 'F6': 0x75, + 'F7': 0x76, 'F8': 0x77, 'F9': 0x78, 'F10': 0x79, 'F11': 0x7A, 'F12': 0x7B, + 'F13': 0x7C, 'F14': 0x7D, 'F15': 0x7E, 'F16': 0x7F, 'F17': 0x80, 'F18': 0x81, + 'F19': 0x82, 'F20': 0x83, 'F21': 0x84, 'F22': 0x85, 'F23': 0x86, 'F24': 0x87, - // Numpad - 'Numpad0': 0x60, 'Numpad1': 0x61, 'Numpad2': 0x62, 'Numpad3': 0x63, 'Numpad4': 0x64, - 'Numpad5': 0x65, 'Numpad6': 0x66, 'Numpad7': 0x67, 'Numpad8': 0x68, 'Numpad9': 0x69, - 'NumpadMultiply': 0x6A, 'NumpadAdd': 0x6B, 'NumpadSeparator': 0x6C, - 'NumpadSubtract': 0x6D, 'NumpadDecimal': 0x6E, 'NumpadDivide': 0x6F, - 'NumpadEnter': 0x0D, // Same as Enter but with ENHANCED_KEY flag - 'NumLock': 0x90, + // Numpad + 'Numpad0': 0x60, 'Numpad1': 0x61, 'Numpad2': 0x62, 'Numpad3': 0x63, 'Numpad4': 0x64, + 'Numpad5': 0x65, 'Numpad6': 0x66, 'Numpad7': 0x67, 'Numpad8': 0x68, 'Numpad9': 0x69, + 'NumpadMultiply': 0x6A, 'NumpadAdd': 0x6B, 'NumpadSeparator': 0x6C, + 'NumpadSubtract': 0x6D, 'NumpadDecimal': 0x6E, 'NumpadDivide': 0x6F, + 'NumpadEnter': 0x0D, // Same as Enter but with ENHANCED_KEY flag + 'NumLock': 0x90, - // Navigation - 'ArrowUp': 0x26, 'ArrowDown': 0x28, 'ArrowLeft': 0x25, 'ArrowRight': 0x27, - 'Home': 0x24, 'End': 0x23, 'PageUp': 0x21, 'PageDown': 0x22, - 'Insert': 0x2D, 'Delete': 0x2E, + // Navigation + 'ArrowUp': 0x26, 'ArrowDown': 0x28, 'ArrowLeft': 0x25, 'ArrowRight': 0x27, + 'Home': 0x24, 'End': 0x23, 'PageUp': 0x21, 'PageDown': 0x22, + 'Insert': 0x2D, 'Delete': 0x2E, - // Modifiers - 'ShiftLeft': 0x10, 'ShiftRight': 0x10, - 'ControlLeft': 0x11, 'ControlRight': 0x11, - 'AltLeft': 0x12, 'AltRight': 0x12, - 'MetaLeft': 0x5B, 'MetaRight': 0x5C, - 'CapsLock': 0x14, 'ScrollLock': 0x91, + // Modifiers + 'ShiftLeft': 0x10, 'ShiftRight': 0x10, + 'ControlLeft': 0x11, 'ControlRight': 0x11, + 'AltLeft': 0x12, 'AltRight': 0x12, + 'MetaLeft': 0x5B, 'MetaRight': 0x5C, + 'CapsLock': 0x14, 'ScrollLock': 0x91, - // Special keys - 'Escape': 0x1B, 'Enter': 0x0D, 'Tab': 0x09, 'Space': 0x20, - 'Backspace': 0x08, 'Pause': 0x13, 'ContextMenu': 0x5D, 'PrintScreen': 0x2C, + // Special keys + 'Escape': 0x1B, 'Enter': 0x0D, 'Tab': 0x09, 'Space': 0x20, + 'Backspace': 0x08, 'Pause': 0x13, 'ContextMenu': 0x5D, 'PrintScreen': 0x2C, - // OEM keys (US keyboard layout) - 'Semicolon': 0xBA, // ;: - 'Equal': 0xBB, // =+ - 'Comma': 0xBC, // ,< - 'Minus': 0xBD, // -_ - 'Period': 0xBE, // .> - 'Slash': 0xBF, // /? - 'Backquote': 0xC0, // `~ - 'BracketLeft': 0xDB, // [{ - 'Backslash': 0xDC, // \| - 'BracketRight': 0xDD, // ]} - 'Quote': 0xDE, // '" - 'IntlBackslash': 0xE2, // Non-US backslash -}; - -/** - * Mapping from browser KeyboardEvent.code to approximate Win32 scan codes. - * Note: Scan codes can vary by keyboard layout. These are approximations - * based on standard US keyboard layout. - */ -const CODE_TO_SCANCODE: { [code: string]: number } = { - // Letters (row by row) - 'KeyQ': 0x10, 'KeyW': 0x11, 'KeyE': 0x12, 'KeyR': 0x13, 'KeyT': 0x14, - 'KeyY': 0x15, 'KeyU': 0x16, 'KeyI': 0x17, 'KeyO': 0x18, 'KeyP': 0x19, - 'KeyA': 0x1E, 'KeyS': 0x1F, 'KeyD': 0x20, 'KeyF': 0x21, 'KeyG': 0x22, - 'KeyH': 0x23, 'KeyJ': 0x24, 'KeyK': 0x25, 'KeyL': 0x26, - 'KeyZ': 0x2C, 'KeyX': 0x2D, 'KeyC': 0x2E, 'KeyV': 0x2F, 'KeyB': 0x30, - 'KeyN': 0x31, 'KeyM': 0x32, - - // Digits - 'Digit1': 0x02, 'Digit2': 0x03, 'Digit3': 0x04, 'Digit4': 0x05, 'Digit5': 0x06, - 'Digit6': 0x07, 'Digit7': 0x08, 'Digit8': 0x09, 'Digit9': 0x0A, 'Digit0': 0x0B, - - // Function keys - 'F1': 0x3B, 'F2': 0x3C, 'F3': 0x3D, 'F4': 0x3E, 'F5': 0x3F, 'F6': 0x40, - 'F7': 0x41, 'F8': 0x42, 'F9': 0x43, 'F10': 0x44, 'F11': 0x57, 'F12': 0x58, - - // Numpad - 'Numpad0': 0x52, 'Numpad1': 0x4F, 'Numpad2': 0x50, 'Numpad3': 0x51, 'Numpad4': 0x4B, - 'Numpad5': 0x4C, 'Numpad6': 0x4D, 'Numpad7': 0x47, 'Numpad8': 0x48, 'Numpad9': 0x49, - 'NumpadMultiply': 0x37, 'NumpadAdd': 0x4E, 'NumpadSubtract': 0x4A, - 'NumpadDecimal': 0x53, 'NumpadDivide': 0x35, 'NumpadEnter': 0x1C, - 'NumLock': 0x45, - - // Navigation (extended keys) - 'ArrowUp': 0x48, 'ArrowDown': 0x50, 'ArrowLeft': 0x4B, 'ArrowRight': 0x4D, - 'Home': 0x47, 'End': 0x4F, 'PageUp': 0x49, 'PageDown': 0x51, - 'Insert': 0x52, 'Delete': 0x53, - - // Modifiers - 'ShiftLeft': 0x2A, 'ShiftRight': 0x36, - 'ControlLeft': 0x1D, 'ControlRight': 0x1D, - 'AltLeft': 0x38, 'AltRight': 0x38, - 'CapsLock': 0x3A, 'ScrollLock': 0x46, - - // Special keys - 'Escape': 0x01, 'Enter': 0x1C, 'Tab': 0x0F, 'Space': 0x39, - 'Backspace': 0x0E, 'Pause': 0x45, - - // OEM keys - 'Semicolon': 0x27, 'Equal': 0x0D, 'Comma': 0x33, 'Minus': 0x0C, - 'Period': 0x34, 'Slash': 0x35, 'Backquote': 0x29, - 'BracketLeft': 0x1A, 'Backslash': 0x2B, 'BracketRight': 0x1B, 'Quote': 0x28, -}; - -/** - * Codes that represent enhanced keys (extended keyboard keys). - */ -const ENHANCED_KEY_CODES = new Set([ - 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', - 'Home', 'End', 'PageUp', 'PageDown', 'Insert', 'Delete', - 'NumpadEnter', 'NumpadDivide', - 'ControlRight', 'AltRight', - 'PrintScreen', 'Pause', 'ContextMenu', - 'MetaLeft', 'MetaRight', -]); - -/** - * Mapping of special keys (ev.key values) to their Unicode control character codes. - * These keys have multi-character ev.key strings but produce control characters. - * @see https://docs.microsoft.com/en-us/windows/console/key-event-record-str - */ -const KEY_TO_CONTROL_CHAR: { [key: string]: number } = { - 'Enter': 0x0D, // Carriage return - 'Backspace': 0x08, // Backspace - 'Tab': 0x09, // Horizontal tab - 'Escape': 0x1B, // Escape -}; - -/** - * Get the Win32 virtual key code for a keyboard event. - */ -function getVirtualKeyCode(ev: IKeyboardEvent): number { - // Try code-based lookup first - const vk = CODE_TO_VK[ev.code]; - if (vk !== undefined) { - return vk; - } - - // Fall back to keyCode for unmapped keys - // Note: keyCode is deprecated but provides reasonable fallback - return ev.keyCode || 0; -} - -/** - * Get the Win32 scan code for a keyboard event. - * Returns 0 if unknown (scan codes vary by hardware). - */ -function getScanCode(ev: IKeyboardEvent): number { - return CODE_TO_SCANCODE[ev.code] || 0; -} - -/** - * Get the unicode character for a keyboard event. - * Returns 0 for non-character keys. - */ -function getUnicodeChar(ev: IKeyboardEvent): number { - // Handle special keys that produce control characters - // Ctrl modifies some of these: Ctrl+Enter=LF, Ctrl+Backspace=DEL - if (ev.ctrlKey && !ev.altKey && !ev.metaKey) { - if (ev.key === 'Enter') { - return 0x0A; // Line feed (Ctrl+Enter) - } - if (ev.key === 'Backspace') { - return 0x7F; // DEL (Ctrl+Backspace) - } - } - - // Check for special keys that always produce control characters - const controlChar = KEY_TO_CONTROL_CHAR[ev.key]; - if (controlChar !== undefined) { - return controlChar; - } - - // Only single-character keys produce unicode output - if (ev.key.length === 1) { - const codePoint = ev.key.codePointAt(0) || 0; - - // Handle Ctrl+letter combinations - these produce control characters (0x01-0x1A) - if (ev.ctrlKey && !ev.altKey && !ev.metaKey) { - // Convert A-Z or a-z to control character (Ctrl+A = 0x01, Ctrl+C = 0x03, etc.) - if (codePoint >= 0x41 && codePoint <= 0x5A) { // A-Z - return codePoint - 0x40; - } - if (codePoint >= 0x61 && codePoint <= 0x7A) { // a-z - return codePoint - 0x60; - } - } - - return codePoint; - } - return 0; -} - -/** - * Get the Win32 control key state flags. - */ -function getControlKeyState(ev: IKeyboardEvent): number { - let state = 0; - - if (ev.shiftKey) { - state |= Win32ControlKeyState.SHIFT_PRESSED; - } - - // Note: We can't distinguish left/right for ctrl/alt in standard browser events, - // so we use the generic pressed flags. The right-side flags are used when - // we can detect them (e.g., via code property). - if (ev.ctrlKey) { - if (ev.code === 'ControlRight') { - state |= Win32ControlKeyState.RIGHT_CTRL_PRESSED; - } else { - state |= Win32ControlKeyState.LEFT_CTRL_PRESSED; - } - } - - if (ev.altKey) { - if (ev.code === 'AltRight') { - state |= Win32ControlKeyState.RIGHT_ALT_PRESSED; - } else { - state |= Win32ControlKeyState.LEFT_ALT_PRESSED; - } - } - - // Check for enhanced key - if (ENHANCED_KEY_CODES.has(ev.code)) { - state |= Win32ControlKeyState.ENHANCED_KEY; - } - - // Note: CapsLock, NumLock, ScrollLock states are not reliably available - // in standard browser keyboard events. We could potentially detect them - // via getModifierState() but this may not be available in all environments. - - return state; -} - -/** - * Evaluate a keyboard event using Win32 input mode. - * - * @param ev The keyboard event. - * @param isKeyDown Whether this is a keydown (true) or keyup (false) event. - * @returns The keyboard result with the encoded key sequence. - */ -export function evaluateKeyboardEventWin32( - ev: IKeyboardEvent, - isKeyDown: boolean -): IKeyboardResult { - const result: IKeyboardResult = { - type: KeyboardResultType.SEND_KEY, - cancel: false, - key: undefined + // OEM keys (US keyboard layout) + 'Semicolon': 0xBA, // ;: + 'Equal': 0xBB, // =+ + 'Comma': 0xBC, // ,< + 'Minus': 0xBD, // -_ + 'Period': 0xBE, // .> + 'Slash': 0xBF, // /? + 'Backquote': 0xC0, // `~ + 'BracketLeft': 0xDB, // [{ + 'Backslash': 0xDC, // \| + 'BracketRight': 0xDD, // ]} + 'Quote': 0xDE, // '" + 'IntlBackslash': 0xE2 // Non-US backslash }; - const vk = getVirtualKeyCode(ev); - const sc = getScanCode(ev); - const uc = getUnicodeChar(ev); - const kd = isKeyDown ? 1 : 0; - const cs = getControlKeyState(ev); - const rc = 1; // Repeat count, always 1 for now + /** + * Mapping from browser KeyboardEvent.code to approximate Win32 scan codes. + * Note: Scan codes can vary by keyboard layout. These are approximations + * based on standard US keyboard layout. + */ + private readonly _codeToScancode: { [code: string]: number } = { + // Letters (row by row) + 'KeyQ': 0x10, 'KeyW': 0x11, 'KeyE': 0x12, 'KeyR': 0x13, 'KeyT': 0x14, + 'KeyY': 0x15, 'KeyU': 0x16, 'KeyI': 0x17, 'KeyO': 0x18, 'KeyP': 0x19, + 'KeyA': 0x1E, 'KeyS': 0x1F, 'KeyD': 0x20, 'KeyF': 0x21, 'KeyG': 0x22, + 'KeyH': 0x23, 'KeyJ': 0x24, 'KeyK': 0x25, 'KeyL': 0x26, + 'KeyZ': 0x2C, 'KeyX': 0x2D, 'KeyC': 0x2E, 'KeyV': 0x2F, 'KeyB': 0x30, + 'KeyN': 0x31, 'KeyM': 0x32, - // Format: CSI Vk ; Sc ; Uc ; Kd ; Cs ; Rc _ - result.key = `${C0.ESC}[${vk};${sc};${uc};${kd};${cs};${rc}_`; - result.cancel = true; + // Digits + 'Digit1': 0x02, 'Digit2': 0x03, 'Digit3': 0x04, 'Digit4': 0x05, 'Digit5': 0x06, + 'Digit6': 0x07, 'Digit7': 0x08, 'Digit8': 0x09, 'Digit9': 0x0A, 'Digit0': 0x0B, - return result; + // Function keys + 'F1': 0x3B, 'F2': 0x3C, 'F3': 0x3D, 'F4': 0x3E, 'F5': 0x3F, 'F6': 0x40, + 'F7': 0x41, 'F8': 0x42, 'F9': 0x43, 'F10': 0x44, 'F11': 0x57, 'F12': 0x58, + + // Numpad + 'Numpad0': 0x52, 'Numpad1': 0x4F, 'Numpad2': 0x50, 'Numpad3': 0x51, 'Numpad4': 0x4B, + 'Numpad5': 0x4C, 'Numpad6': 0x4D, 'Numpad7': 0x47, 'Numpad8': 0x48, 'Numpad9': 0x49, + 'NumpadMultiply': 0x37, 'NumpadAdd': 0x4E, 'NumpadSubtract': 0x4A, + 'NumpadDecimal': 0x53, 'NumpadDivide': 0x35, 'NumpadEnter': 0x1C, + 'NumLock': 0x45, + + // Navigation (extended keys) + 'ArrowUp': 0x48, 'ArrowDown': 0x50, 'ArrowLeft': 0x4B, 'ArrowRight': 0x4D, + 'Home': 0x47, 'End': 0x4F, 'PageUp': 0x49, 'PageDown': 0x51, + 'Insert': 0x52, 'Delete': 0x53, + + // Modifiers + 'ShiftLeft': 0x2A, 'ShiftRight': 0x36, + 'ControlLeft': 0x1D, 'ControlRight': 0x1D, + 'AltLeft': 0x38, 'AltRight': 0x38, + 'CapsLock': 0x3A, 'ScrollLock': 0x46, + + // Special keys + 'Escape': 0x01, 'Enter': 0x1C, 'Tab': 0x0F, 'Space': 0x39, + 'Backspace': 0x0E, 'Pause': 0x45, + + // OEM keys + 'Semicolon': 0x27, 'Equal': 0x0D, 'Comma': 0x33, 'Minus': 0x0C, + 'Period': 0x34, 'Slash': 0x35, 'Backquote': 0x29, + 'BracketLeft': 0x1A, 'Backslash': 0x2B, 'BracketRight': 0x1B, 'Quote': 0x28 + }; + + /** + * Codes that represent enhanced keys (extended keyboard keys). + */ + private readonly _enhancedKeyCodes = new Set([ + 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', + 'Home', 'End', 'PageUp', 'PageDown', 'Insert', 'Delete', + 'NumpadEnter', 'NumpadDivide', + 'ControlRight', 'AltRight', + 'PrintScreen', 'Pause', 'ContextMenu', + 'MetaLeft', 'MetaRight' + ]); + + /** + * Mapping of special keys (ev.key values) to their Unicode control character codes. + * These keys have multi-character ev.key strings but produce control characters. + * @see https://docs.microsoft.com/en-us/windows/console/key-event-record-str + */ + private readonly _keyToControlChar: { [key: string]: number } = { + 'Enter': 0x0D, // Carriage return + 'Backspace': 0x08, // Backspace + 'Tab': 0x09, // Horizontal tab + 'Escape': 0x1B // Escape + }; + + /** + * Get the Win32 virtual key code for a keyboard event. + */ + private _getVirtualKeyCode(ev: IKeyboardEvent): number { + const vk = this._codeToVk[ev.code]; + if (vk !== undefined) { + return vk; + } + // Fall back to keyCode for unmapped keys + return ev.keyCode || 0; + } + + /** + * Get the Win32 scan code for a keyboard event. + * Returns 0 if unknown (scan codes vary by hardware). + */ + private _getScanCode(ev: IKeyboardEvent): number { + return this._codeToScancode[ev.code] || 0; + } + + /** + * Get the unicode character for a keyboard event. + * Returns 0 for non-character keys. + */ + private _getUnicodeChar(ev: IKeyboardEvent): number { + // Handle special keys that produce control characters + // Ctrl modifies some of these: Ctrl+Enter=LF, Ctrl+Backspace=DEL + if (ev.ctrlKey && !ev.altKey && !ev.metaKey) { + if (ev.key === 'Enter') { + return 0x0A; // Line feed (Ctrl+Enter) + } + if (ev.key === 'Backspace') { + return 0x7F; // DEL (Ctrl+Backspace) + } + } + + // Check for special keys that always produce control characters + const controlChar = this._keyToControlChar[ev.key]; + if (controlChar !== undefined) { + return controlChar; + } + + // Only single-character keys produce unicode output + if (ev.key.length === 1) { + const codePoint = ev.key.codePointAt(0) || 0; + + // Handle Ctrl+letter combinations - these produce control characters (0x01-0x1A) + if (ev.ctrlKey && !ev.altKey && !ev.metaKey) { + // Convert A-Z or a-z to control character (Ctrl+A = 0x01, Ctrl+C = 0x03, etc.) + if (codePoint >= 0x41 && codePoint <= 0x5A) { // A-Z + return codePoint - 0x40; + } + if (codePoint >= 0x61 && codePoint <= 0x7A) { // a-z + return codePoint - 0x60; + } + } + + return codePoint; + } + return 0; + } + + /** + * Get the Win32 control key state flags. + */ + private _getControlKeyState(ev: IKeyboardEvent): number { + let state = 0; + + if (ev.shiftKey) { + state |= Win32ControlKeyState.SHIFT_PRESSED; + } + + // Note: We can't distinguish left/right for ctrl/alt in standard browser events, + // so we use the generic pressed flags. The right-side flags are used when + // we can detect them (e.g., via code property). + if (ev.ctrlKey) { + if (ev.code === 'ControlRight') { + state |= Win32ControlKeyState.RIGHT_CTRL_PRESSED; + } else { + state |= Win32ControlKeyState.LEFT_CTRL_PRESSED; + } + } + + if (ev.altKey) { + if (ev.code === 'AltRight') { + state |= Win32ControlKeyState.RIGHT_ALT_PRESSED; + } else { + state |= Win32ControlKeyState.LEFT_ALT_PRESSED; + } + } + + // Check for enhanced key + if (this._enhancedKeyCodes.has(ev.code)) { + state |= Win32ControlKeyState.ENHANCED_KEY; + } + + return state; + } + + /** + * Evaluate a keyboard event using Win32 input mode. + * + * @param ev The keyboard event. + * @param isKeyDown Whether this is a keydown (true) or keyup (false) event. + * @returns The keyboard result with the encoded key sequence. + */ + public evaluateKeyboardEvent(ev: IKeyboardEvent, isKeyDown: boolean): IKeyboardResult { + const vk = this._getVirtualKeyCode(ev); + const sc = this._getScanCode(ev); + const uc = this._getUnicodeChar(ev); + const kd = isKeyDown ? 1 : 0; + const cs = this._getControlKeyState(ev); + const rc = 1; // Repeat count, always 1 for now + + // Format: CSI Vk ; Sc ; Uc ; Kd ; Cs ; Rc _ + return { + type: KeyboardResultType.SEND_KEY, + cancel: true, + key: `${C0.ESC}[${vk};${sc};${uc};${kd};${cs};${rc}_` + }; + } } diff --git a/src/common/services/LogService.ts b/src/common/services/LogService.ts index 12c5e3b0..8bc1e0aa 100644 --- a/src/common/services/LogService.ts +++ b/src/common/services/LogService.ts @@ -43,9 +43,6 @@ export class LogService extends Disposable implements ILogService { super(); this._updateLogLevel(); this._register(this._optionsService.onSpecificOptionChange('logLevel', () => this._updateLogLevel())); - - // For trace logging, assume the latest created log service is valid - traceLogger = this; } private _updateLogLevel(): void { @@ -95,30 +92,3 @@ export class LogService extends Disposable implements ILogService { } } } - -let traceLogger: ILogService; -export function setTraceLogger(logger: ILogService): void { - traceLogger = logger; -} - -/** - * A decorator that can be used to automatically log trace calls to the decorated function. - */ -export function traceCall(_target: any, key: string, descriptor: any): any { - if (typeof descriptor.value !== 'function') { - throw new Error('not supported'); - } - const fnKey = 'value'; - const fn = descriptor.value; - descriptor[fnKey] = function (...args: any[]) { - // Early exit - if (traceLogger.logLevel !== LogLevelEnum.TRACE) { - return fn.apply(this, args); - } - - traceLogger.trace(`GlyphRenderer#${fn.name}(${args.map(e => JSON.stringify(e)).join(', ')})`); - const result = fn.apply(this, args); - traceLogger.trace(`GlyphRenderer#${fn.name} return`, result); - return result; - }; -}