From d35481c3f97128ba1225c90f05accf1fc8f84e13 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 30 Jan 2026 07:07:55 -0800 Subject: [PATCH] Move win32 input mode into a class to avoid init cost --- src/browser/services/KeyboardService.ts | 15 +- src/common/input/Win32InputMode.test.ts | 12 +- src/common/input/Win32InputMode.ts | 507 ++++++++++++------------ 3 files changed, 268 insertions(+), 266 deletions(-) diff --git a/src/browser/services/KeyboardService.ts b/src/browser/services/KeyboardService.ts index 478a262e..afdde381 100644 --- a/src/browser/services/KeyboardService.ts +++ b/src/browser/services/KeyboardService.ts @@ -6,7 +6,7 @@ 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 { Win32InputMode } from 'common/input/Win32InputMode'; import { isMac } from 'common/Platform'; import { ICoreService, IOptionsService } from 'common/services/Services'; import { IKeyboardResult } from 'common/Types'; @@ -14,16 +14,25 @@ import { IKeyboardResult } from 'common/Types'; export class KeyboardService implements IKeyboardService { public serviceBrand: undefined; + private _win32InputMode: Win32InputMode | 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; + } + 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 @@ -34,7 +43,7 @@ export class KeyboardService implements IKeyboardService { 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)) { 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}_` + }; + } }