Move win32 input mode into a class to avoid init cost

This commit is contained in:
Daniel Imms
2026-01-30 07:07:55 -08:00
parent 01450f044a
commit d35481c3f9
3 changed files with 268 additions and 266 deletions
+12 -3
View File
@@ -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)) {
+7 -5
View File
@@ -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<IKeyboardEvent>;
@@ -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<typeof parse>) => 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);
});
+249 -258
View File
@@ -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}_`
};
}
}