From ee47054678c7b1270235bc13e174e3a43018b3ec Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 10 Jan 2026 05:27:03 -0800 Subject: [PATCH 1/8] Implement win32-input-mode Fixes #2357 --- .../client/components/window/optionsWindow.ts | 3 +- src/browser/public/Terminal.ts | 1 + src/browser/services/KeyboardService.ts | 13 + src/common/InputHandler.ts | 11 + src/common/TestUtils.test.ts | 1 + src/common/Types.ts | 1 + src/common/input/Win32InputMode.test.ts | 290 ++++++++++++++++++ src/common/input/Win32InputMode.ts | 264 ++++++++++++++++ src/common/services/CoreService.ts | 1 + src/common/services/Services.ts | 1 + src/headless/public/Terminal.ts | 1 + typings/xterm-headless.d.ts | 16 + typings/xterm.d.ts | 16 + 13 files changed, 618 insertions(+), 1 deletion(-) create mode 100644 src/common/input/Win32InputMode.test.ts create mode 100644 src/common/input/Win32InputMode.ts diff --git a/demo/client/components/window/optionsWindow.ts b/demo/client/components/window/optionsWindow.ts index 18f37b58..7f8c4e2f 100644 --- a/demo/client/components/window/optionsWindow.ts +++ b/demo/client/components/window/optionsWindow.ts @@ -125,7 +125,8 @@ export class OptionsWindow extends BaseWindow implements IControlWindow { ]; const nestedBooleanOptions: { label: string, parent: string, prop: string }[] = [ { label: 'vtExtensions.kittyKeyboard', parent: 'vtExtensions', prop: 'kittyKeyboard' }, - { label: 'vtExtensions.kittySgrBoldFaintControl', parent: 'vtExtensions', prop: 'kittySgrBoldFaintControl' } + { label: 'vtExtensions.kittySgrBoldFaintControl', parent: 'vtExtensions', prop: 'kittySgrBoldFaintControl' }, + { label: 'vtExtensions.win32InputMode', parent: 'vtExtensions', prop: 'win32InputMode' } ]; const stringOptions: { [key: string]: string[] | null } = { cursorStyle: ['block', 'underline', 'bar'], diff --git a/src/browser/public/Terminal.ts b/src/browser/public/Terminal.ts index 157ceb14..442f8b6a 100644 --- a/src/browser/public/Terminal.ts +++ b/src/browser/public/Terminal.ts @@ -125,6 +125,7 @@ export class Terminal extends Disposable implements ITerminalApi { sendFocusMode: m.sendFocus, showCursor: !this._core.coreService.isCursorHidden, synchronizedOutputMode: m.synchronizedOutput, + win32InputMode: m.win32InputMode, wraparoundMode: m.wraparound }; } diff --git a/src/browser/services/KeyboardService.ts b/src/browser/services/KeyboardService.ts index da2368a4..1095d30f 100644 --- a/src/browser/services/KeyboardService.ts +++ b/src/browser/services/KeyboardService.ts @@ -6,6 +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 { isMac } from 'common/Platform'; import { ICoreService, IOptionsService } from 'common/services/Services'; import { IKeyboardResult } from 'common/Types'; @@ -20,6 +21,10 @@ export class KeyboardService implements IKeyboardService { } public evaluateKeyDown(event: KeyboardEvent): IKeyboardResult { + // Win32 input mode takes priority (most raw) + if (this.useWin32) { + return evaluateKeyboardEventWin32(event, true); + } const kittyFlags = this._coreService.kittyKeyboard.flags; return this.useKitty ? evaluateKeyboardEventKitty(event, kittyFlags, event.repeat ? KittyKeyboardEventType.REPEAT : KittyKeyboardEventType.PRESS) @@ -27,6 +32,10 @@ export class KeyboardService implements IKeyboardService { } public evaluateKeyUp(event: KeyboardEvent): IKeyboardResult | undefined { + // Win32 input mode sends key up events + if (this.useWin32) { + return evaluateKeyboardEventWin32(event, false); + } const kittyFlags = this._coreService.kittyKeyboard.flags; if (this.useKitty && (kittyFlags & KittyKeyboardFlags.REPORT_EVENT_TYPES)) { return evaluateKeyboardEventKitty(event, kittyFlags, KittyKeyboardEventType.RELEASE); @@ -38,4 +47,8 @@ export class KeyboardService implements IKeyboardService { const kittyFlags = this._coreService.kittyKeyboard.flags; return !!(this._optionsService.rawOptions.vtExtensions?.kittyKeyboard && shouldUseKittyProtocol(kittyFlags)); } + + public get useWin32(): boolean { + return !!(this._optionsService.rawOptions.vtExtensions?.win32InputMode && this._coreService.decPrivateModes.win32InputMode); + } } diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 85f6f725..b2b75143 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -2026,6 +2026,11 @@ export class InputHandler extends Disposable implements IInputHandler { case 2026: // synchronized output (https://github.com/contour-terminal/vt-extensions/blob/master/synchronized-output.md) this._coreService.decPrivateModes.synchronizedOutput = true; break; + case 9001: // win32-input-mode (https://github.com/microsoft/terminal/blob/main/doc/specs/%234999%20-%20Improved%20keyboard%20handling%20in%20Conpty.md) + if (this._optionsService.rawOptions.vtExtensions?.win32InputMode) { + this._coreService.decPrivateModes.win32InputMode = true; + } + break; } } return true; @@ -2266,6 +2271,11 @@ export class InputHandler extends Disposable implements IInputHandler { this._coreService.decPrivateModes.synchronizedOutput = false; this._onRequestRefreshRows.fire(undefined); break; + case 9001: // win32-input-mode + if (this._optionsService.rawOptions.vtExtensions?.win32InputMode) { + this._coreService.decPrivateModes.win32InputMode = false; + } + break; } } return true; @@ -2361,6 +2371,7 @@ export class InputHandler extends Disposable implements IInputHandler { if (p === 47 || p === 1047 || p === 1049) return f(p, b2v(active === alt)); if (p === 2004) return f(p, b2v(dm.bracketedPasteMode)); if (p === 2026) return f(p, b2v(dm.synchronizedOutput)); + if (p === 9001) return this._optionsService.rawOptions.vtExtensions?.win32InputMode ? f(p, b2v(dm.win32InputMode)) : f(p, V.NOT_RECOGNIZED); return f(p, V.NOT_RECOGNIZED); } diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 9bfb2d19..2d44c32e 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -112,6 +112,7 @@ export class MockCoreService implements ICoreService { reverseWraparound: false, sendFocus: false, synchronizedOutput: false, + win32InputMode: false, wraparound: true }; public kittyKeyboard = { diff --git a/src/common/Types.ts b/src/common/Types.ts index 9ad294f0..269b30c8 100644 --- a/src/common/Types.ts +++ b/src/common/Types.ts @@ -274,6 +274,7 @@ export interface IDecPrivateModes { reverseWraparound: boolean; sendFocus: boolean; synchronizedOutput: boolean; + win32InputMode: boolean; wraparound: boolean; // defaults: xterm - true, vt100 - false } diff --git a/src/common/input/Win32InputMode.test.ts b/src/common/input/Win32InputMode.test.ts new file mode 100644 index 00000000..1bfd5884 --- /dev/null +++ b/src/common/input/Win32InputMode.test.ts @@ -0,0 +1,290 @@ + +import { assert } from 'chai'; +import { evaluateKeyboardEventWin32, Win32ControlKeyState } from 'common/input/Win32InputMode'; +import { IKeyboardEvent, KeyboardResultType } from 'common/Types'; + +function createEvent(partialEvent: Partial = {}): IKeyboardEvent { + return { + altKey: partialEvent.altKey || false, + ctrlKey: partialEvent.ctrlKey || false, + shiftKey: partialEvent.shiftKey || false, + metaKey: partialEvent.metaKey || false, + keyCode: partialEvent.keyCode !== undefined ? partialEvent.keyCode : 0, + code: partialEvent.code || '', + key: partialEvent.key || '', + type: partialEvent.type || 'keydown' + }; +} + +function parseWin32Sequence(seq: string): { vk: number, sc: number, uc: number, kd: number, cs: number, rc: number } | null { + const match = seq.match(/^\x1b\[(\d+);(\d+);(\d+);(\d+);(\d+);(\d+)_$/); + if (!match) { + return null; + } + return { + vk: parseInt(match[1], 10), + sc: parseInt(match[2], 10), + uc: parseInt(match[3], 10), + kd: parseInt(match[4], 10), + cs: parseInt(match[5], 10), + rc: parseInt(match[6], 10) + }; +} + +describe('Win32InputMode', () => { + describe('evaluateKeyboardEventWin32', () => { + describe('basic key encoding', () => { + it('should encode letter key press', () => { + const result = evaluateKeyboardEventWin32(createEvent({ code: 'KeyA', key: 'a', keyCode: 65 }), true); + assert.strictEqual(result.type, KeyboardResultType.SEND_KEY); + assert.strictEqual(result.cancel, true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x41); + assert.strictEqual(parsed.uc, 97); + assert.strictEqual(parsed.kd, 1); + assert.strictEqual(parsed.rc, 1); + }); + + it('should encode letter key release', () => { + const result = evaluateKeyboardEventWin32(createEvent({ code: 'KeyA', key: 'a', keyCode: 65 }), false); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.kd, 0); + }); + + it('should encode digit key', () => { + const result = evaluateKeyboardEventWin32(createEvent({ code: 'Digit1', key: '1', keyCode: 49 }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x31); + assert.strictEqual(parsed.uc, 49); + }); + + it('should encode Enter key', () => { + const result = evaluateKeyboardEventWin32(createEvent({ code: 'Enter', key: 'Enter', keyCode: 13 }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x0D); + assert.strictEqual(parsed.uc, 0); + }); + + it('should encode Escape key', () => { + const result = evaluateKeyboardEventWin32(createEvent({ code: 'Escape', key: 'Escape', keyCode: 27 }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x1B); + }); + + it('should encode Space key', () => { + const result = evaluateKeyboardEventWin32(createEvent({ code: 'Space', key: ' ', keyCode: 32 }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x20); + assert.strictEqual(parsed.uc, 32); + }); + }); + + describe('modifier encoding', () => { + it('should encode shift modifier', () => { + const result = evaluateKeyboardEventWin32(createEvent({ + code: 'KeyA', + key: 'A', + keyCode: 65, + shiftKey: true + }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.ok(parsed.cs & Win32ControlKeyState.SHIFT_PRESSED); + }); + + it('should encode ctrl modifier (left)', () => { + const result = evaluateKeyboardEventWin32(createEvent({ + code: 'KeyA', + key: 'a', + keyCode: 65, + ctrlKey: true + }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.ok(parsed.cs & Win32ControlKeyState.LEFT_CTRL_PRESSED); + }); + + it('should encode ctrl modifier (right)', () => { + const result = evaluateKeyboardEventWin32(createEvent({ + code: 'ControlRight', + key: 'Control', + keyCode: 17, + ctrlKey: true + }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.ok(parsed.cs & Win32ControlKeyState.RIGHT_CTRL_PRESSED); + assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); + }); + + it('should encode alt modifier (left)', () => { + const result = evaluateKeyboardEventWin32(createEvent({ + code: 'KeyA', + key: 'a', + keyCode: 65, + altKey: true + }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.ok(parsed.cs & Win32ControlKeyState.LEFT_ALT_PRESSED); + }); + + it('should encode alt modifier (right)', () => { + const result = evaluateKeyboardEventWin32(createEvent({ + code: 'AltRight', + key: 'Alt', + keyCode: 18, + altKey: true + }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.ok(parsed.cs & Win32ControlKeyState.RIGHT_ALT_PRESSED); + assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); + }); + + it('should encode multiple modifiers', () => { + const result = evaluateKeyboardEventWin32(createEvent({ + code: 'KeyA', + key: 'A', + keyCode: 65, + shiftKey: true, + ctrlKey: true, + altKey: true + }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.ok(parsed.cs & Win32ControlKeyState.SHIFT_PRESSED); + assert.ok(parsed.cs & Win32ControlKeyState.LEFT_CTRL_PRESSED); + assert.ok(parsed.cs & Win32ControlKeyState.LEFT_ALT_PRESSED); + }); + }); + + describe('function keys', () => { + it('should encode F1', () => { + const result = evaluateKeyboardEventWin32(createEvent({ code: 'F1', key: 'F1', keyCode: 112 }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x70); + }); + + it('should encode F12', () => { + const result = evaluateKeyboardEventWin32(createEvent({ code: 'F12', key: 'F12', keyCode: 123 }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x7B); + }); + }); + + describe('navigation keys', () => { + it('should encode arrow up with ENHANCED_KEY flag', () => { + const result = evaluateKeyboardEventWin32(createEvent({ code: 'ArrowUp', key: 'ArrowUp', keyCode: 38 }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x26); + assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); + }); + + it('should encode Home with ENHANCED_KEY flag', () => { + const result = evaluateKeyboardEventWin32(createEvent({ code: 'Home', key: 'Home', keyCode: 36 }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x24); + assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); + }); + + it('should encode Insert with ENHANCED_KEY flag', () => { + const result = evaluateKeyboardEventWin32(createEvent({ code: 'Insert', key: 'Insert', keyCode: 45 }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x2D); + assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); + }); + + it('should encode Delete with ENHANCED_KEY flag', () => { + const result = evaluateKeyboardEventWin32(createEvent({ code: 'Delete', key: 'Delete', keyCode: 46 }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x2E); + assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); + }); + }); + + describe('numpad keys', () => { + it('should encode Numpad0', () => { + const result = evaluateKeyboardEventWin32(createEvent({ code: 'Numpad0', key: '0', keyCode: 96 }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x60); + }); + + it('should encode NumpadEnter with ENHANCED_KEY', () => { + const result = evaluateKeyboardEventWin32(createEvent({ code: 'NumpadEnter', key: 'Enter', keyCode: 13 }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x0D); + assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); + }); + }); + + describe('unicode character', () => { + it('should include unicode codepoint for printable characters', () => { + const result = evaluateKeyboardEventWin32(createEvent({ code: 'KeyA', key: 'a', keyCode: 65 }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.uc, 97); + }); + + it('should include unicode codepoint for shifted characters', () => { + const result = evaluateKeyboardEventWin32(createEvent({ + code: 'KeyA', + key: 'A', + keyCode: 65, + shiftKey: true + }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.uc, 65); + }); + + it('should set unicode to 0 for non-printable keys', () => { + const result = evaluateKeyboardEventWin32(createEvent({ code: 'ArrowUp', key: 'ArrowUp', keyCode: 38 }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.uc, 0); + }); + }); + + describe('scan codes', () => { + it('should include approximate scan code for letter', () => { + const result = evaluateKeyboardEventWin32(createEvent({ code: 'KeyA', key: 'a', keyCode: 65 }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.sc, 0x1E); + }); + + it('should include approximate scan code for Escape', () => { + const result = evaluateKeyboardEventWin32(createEvent({ code: 'Escape', key: 'Escape', keyCode: 27 }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.sc, 0x01); + }); + }); + + describe('sequence format', () => { + it('should produce valid CSI sequence format', () => { + const result = evaluateKeyboardEventWin32(createEvent({ code: 'KeyA', key: 'a', keyCode: 65 }), true); + assert.ok(result.key); + assert.ok(result.key.startsWith('\x1b[')); + assert.ok(result.key.endsWith('_')); + const parts = result.key.slice(2, -1).split(';'); + assert.strictEqual(parts.length, 6); + }); + }); + }); +}); diff --git a/src/common/input/Win32InputMode.ts b/src/common/input/Win32InputMode.ts new file mode 100644 index 00000000..c606fcfc --- /dev/null +++ b/src/common/input/Win32InputMode.ts @@ -0,0 +1,264 @@ +/** + * Copyright (c) 2026 The xterm.js authors. All rights reserved. + * @license MIT + * + * Win32 input mode implementation. + * @see https://github.com/microsoft/terminal/blob/main/doc/specs/%234999%20-%20Improved%20keyboard%20handling%20in%20Conpty.md + * + * Format: CSI Vk ; Sc ; Uc ; Kd ; Cs ; Rc _ + * Vk: Virtual key code (decimal) + * Sc: Scan code (decimal) + * Uc: Unicode character (decimal codepoint, 0 if none) + * Kd: Key down (1) or up (0) + * Cs: Control key state (modifier flags) + * Rc: Repeat count (usually 1) + */ + +import { IKeyboardEvent, IKeyboardResult, KeyboardResultType } from 'common/Types'; +import { C0 } from 'common/data/EscapeSequences'; + +/** + * Win32 control key state flags (from Windows API). + */ +export const enum Win32ControlKeyState { + RIGHT_ALT_PRESSED = 0x0001, + LEFT_ALT_PRESSED = 0x0002, + RIGHT_CTRL_PRESSED = 0x0004, + LEFT_CTRL_PRESSED = 0x0008, + SHIFT_PRESSED = 0x0010, + NUMLOCK_ON = 0x0020, + SCROLLLOCK_ON = 0x0040, + CAPSLOCK_ON = 0x0080, + ENHANCED_KEY = 0x0100, +} + +/** + * Mapping from browser KeyboardEvent.code to Win32 virtual key codes. + * Based on https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes + */ +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, + + // 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, + + // 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, + + // 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, + + // 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', +]); + +/** + * 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 { + // Only single-character keys produce unicode output + if (ev.key.length === 1) { + return ev.key.codePointAt(0) || 0; + } + 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 + }; + + 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 + + // Format: CSI Vk ; Sc ; Uc ; Kd ; Cs ; Rc _ + result.key = `${C0.ESC}[${vk};${sc};${uc};${kd};${cs};${rc}_`; + result.cancel = true; + + return result; +} diff --git a/src/common/services/CoreService.ts b/src/common/services/CoreService.ts index e160e6e9..7e1ef64b 100644 --- a/src/common/services/CoreService.ts +++ b/src/common/services/CoreService.ts @@ -23,6 +23,7 @@ const DEFAULT_DEC_PRIVATE_MODES: IDecPrivateModes = Object.freeze({ reverseWraparound: false, sendFocus: false, synchronizedOutput: false, + win32InputMode: false, wraparound: true // defaults: xterm - true, vt100 - false }); diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 70d145e0..d9698899 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -311,6 +311,7 @@ export interface ITerminalQuirks { export interface IVtExtensions { kittyKeyboard?: boolean; kittySgrBoldFaintControl?: boolean; + win32InputMode?: boolean; } export const IOscLinkService = createDecorator('OscLinkService'); diff --git a/src/headless/public/Terminal.ts b/src/headless/public/Terminal.ts index e0a47e1f..18659b3c 100644 --- a/src/headless/public/Terminal.ts +++ b/src/headless/public/Terminal.ts @@ -124,6 +124,7 @@ export class Terminal extends Disposable implements ITerminalApi { sendFocusMode: m.sendFocus, showCursor: !this._core.coreService.isCursorHidden, synchronizedOutputMode: m.synchronizedOutput, + win32InputMode: m.win32InputMode, wraparoundMode: m.wraparound }; } diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts index a9b1d6e6..ef7a0af5 100644 --- a/typings/xterm-headless.d.ts +++ b/typings/xterm-headless.d.ts @@ -340,6 +340,15 @@ declare module '@xterm/headless' { * [0]: https://sw.kovidgoyal.net/kitty/misc-protocol/ */ kittySgrBoldFaintControl?: boolean; + + /** + * Whether [win32-input-mode][0] (`DECSET 9001`) is enabled. When enabled, + * the terminal will allow programs to enable win32 INPUT_RECORD keyboard + * reporting via `CSI ? 9001 h`. The default is false. + * + * [0]: https://github.com/microsoft/terminal/blob/main/doc/specs/%234999%20-%20Improved%20keyboard%20handling%20in%20Conpty.md + */ + win32InputMode?: boolean; } /** @@ -1387,6 +1396,13 @@ declare module '@xterm/headless' { * disabled, allowing for atomic screen updates without tearing. */ readonly synchronizedOutputMode: boolean; + /** + * Win32 Input Mode: `CSI ? 9 0 0 1 h` + * + * When enabled, keyboard input is sent as Win32 INPUT_RECORD format: + * `CSI Vk ; Sc ; Uc ; Kd ; Cs ; Rc _` + */ + readonly win32InputMode: boolean; /** * Auto-Wrap Mode (DECAWM): `CSI ? 7 h` */ diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index a6fb140c..f2cd3c23 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -457,6 +457,15 @@ declare module '@xterm/xterm' { * [0]: https://sw.kovidgoyal.net/kitty/misc-protocol/ */ kittySgrBoldFaintControl?: boolean; + + /** + * Whether [win32-input-mode][0] (`DECSET 9001`) is enabled. When enabled, + * the terminal will allow programs to enable win32 INPUT_RECORD keyboard + * reporting via `CSI ? 9001 h`. The default is false. + * + * [0]: https://github.com/microsoft/terminal/blob/main/doc/specs/%234999%20-%20Improved%20keyboard%20handling%20in%20Conpty.md + */ + win32InputMode?: boolean; } /** @@ -2007,6 +2016,13 @@ declare module '@xterm/xterm' { * disabled, allowing for atomic screen updates without tearing. */ readonly synchronizedOutputMode: boolean; + /** + * Win32 Input Mode: `CSI ? 9 0 0 1 h` + * + * When enabled, keyboard input is sent as Win32 INPUT_RECORD format: + * `CSI Vk ; Sc ; Uc ; Kd ; Cs ; Rc _` + */ + readonly win32InputMode: boolean; /** * Auto-Wrap Mode (DECAWM): `CSI ? 7 h` */ From 6c2792a1d5455e1516e4696f9803b9fc4fad7497 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 10 Jan 2026 05:33:08 -0800 Subject: [PATCH 2/8] Fix tests --- src/headless/public/Terminal.test.ts | 1 + test/playwright/Terminal.test.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/headless/public/Terminal.test.ts b/src/headless/public/Terminal.test.ts index 059b4a50..0b35fb0e 100644 --- a/src/headless/public/Terminal.test.ts +++ b/src/headless/public/Terminal.test.ts @@ -411,6 +411,7 @@ describe('Headless API Tests', function (): void { sendFocusMode: false, showCursor: true, synchronizedOutputMode: false, + win32InputMode: false, wraparoundMode: true }); }); diff --git a/test/playwright/Terminal.test.ts b/test/playwright/Terminal.test.ts index d90b361b..9985fe67 100644 --- a/test/playwright/Terminal.test.ts +++ b/test/playwright/Terminal.test.ts @@ -672,6 +672,7 @@ test.describe('API Integration Tests', () => { sendFocusMode: false, showCursor: true, synchronizedOutputMode: false, + win32InputMode: false, wraparoundMode: true }); }); From ebe18b3da521178eefbe7d9ed24d4af5ddb88f63 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 10 Jan 2026 05:42:17 -0800 Subject: [PATCH 3/8] Add more test cases --- src/common/input/Win32InputMode.test.ts | 326 ++++++++++++++++++++++++ 1 file changed, 326 insertions(+) diff --git a/src/common/input/Win32InputMode.test.ts b/src/common/input/Win32InputMode.test.ts index 1bfd5884..7ca65cf8 100644 --- a/src/common/input/Win32InputMode.test.ts +++ b/src/common/input/Win32InputMode.test.ts @@ -286,5 +286,331 @@ describe('Win32InputMode', () => { assert.strictEqual(parts.length, 6); }); }); + + describe('standalone modifier keys', () => { + it('should encode ShiftLeft alone', () => { + const result = evaluateKeyboardEventWin32(createEvent({ + code: 'ShiftLeft', + key: 'Shift', + keyCode: 16, + shiftKey: true + }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x10); + assert.ok(parsed.cs & Win32ControlKeyState.SHIFT_PRESSED); + }); + + it('should encode ShiftRight alone', () => { + const result = evaluateKeyboardEventWin32(createEvent({ + code: 'ShiftRight', + key: 'Shift', + keyCode: 16, + shiftKey: true + }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x10); + assert.ok(parsed.cs & Win32ControlKeyState.SHIFT_PRESSED); + }); + + it('should encode ControlLeft alone', () => { + const result = evaluateKeyboardEventWin32(createEvent({ + code: 'ControlLeft', + key: 'Control', + keyCode: 17, + ctrlKey: true + }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x11); + assert.ok(parsed.cs & Win32ControlKeyState.LEFT_CTRL_PRESSED); + }); + + it('should encode ControlRight alone', () => { + const result = evaluateKeyboardEventWin32(createEvent({ + code: 'ControlRight', + key: 'Control', + keyCode: 17, + ctrlKey: true + }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x11); + assert.ok(parsed.cs & Win32ControlKeyState.RIGHT_CTRL_PRESSED); + assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); + }); + + it('should encode AltLeft alone', () => { + const result = evaluateKeyboardEventWin32(createEvent({ + code: 'AltLeft', + key: 'Alt', + keyCode: 18, + altKey: true + }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x12); + assert.ok(parsed.cs & Win32ControlKeyState.LEFT_ALT_PRESSED); + }); + + it('should encode AltRight alone', () => { + const result = evaluateKeyboardEventWin32(createEvent({ + code: 'AltRight', + key: 'Alt', + keyCode: 18, + altKey: true + }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x12); + assert.ok(parsed.cs & Win32ControlKeyState.RIGHT_ALT_PRESSED); + assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); + }); + + it('should encode modifier key release', () => { + const result = evaluateKeyboardEventWin32(createEvent({ + code: 'ShiftLeft', + key: 'Shift', + keyCode: 16, + shiftKey: false + }), false); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.kd, 0); + }); + }); + + describe('problem keys from spec', () => { + it('should encode Ctrl+Space', () => { + const result = evaluateKeyboardEventWin32(createEvent({ + code: 'Space', + key: ' ', + keyCode: 32, + ctrlKey: true + }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x20); + assert.ok(parsed.cs & Win32ControlKeyState.LEFT_CTRL_PRESSED); + }); + + it('should encode Shift+Enter', () => { + const result = evaluateKeyboardEventWin32(createEvent({ + code: 'Enter', + key: 'Enter', + keyCode: 13, + shiftKey: true + }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x0D); + assert.ok(parsed.cs & Win32ControlKeyState.SHIFT_PRESSED); + }); + + it('should encode Ctrl+Break', () => { + const result = evaluateKeyboardEventWin32(createEvent({ + code: 'Pause', + key: 'Pause', + keyCode: 19, + ctrlKey: true + }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x13); + assert.ok(parsed.cs & Win32ControlKeyState.LEFT_CTRL_PRESSED); + }); + + it('should encode Ctrl+Alt+/', () => { + const result = evaluateKeyboardEventWin32(createEvent({ + code: 'Slash', + key: '/', + keyCode: 191, + ctrlKey: true, + altKey: true + }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.ok(parsed.cs & Win32ControlKeyState.LEFT_CTRL_PRESSED); + assert.ok(parsed.cs & Win32ControlKeyState.LEFT_ALT_PRESSED); + }); + }); + + describe('additional navigation keys', () => { + it('should encode Tab', () => { + const result = evaluateKeyboardEventWin32(createEvent({ code: 'Tab', key: 'Tab', keyCode: 9 }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x09); + }); + + it('should encode Backspace', () => { + const result = evaluateKeyboardEventWin32(createEvent({ code: 'Backspace', key: 'Backspace', keyCode: 8 }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x08); + }); + + it('should encode PageUp with ENHANCED_KEY flag', () => { + const result = evaluateKeyboardEventWin32(createEvent({ code: 'PageUp', key: 'PageUp', keyCode: 33 }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x21); + assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); + }); + + it('should encode PageDown with ENHANCED_KEY flag', () => { + const result = evaluateKeyboardEventWin32(createEvent({ code: 'PageDown', key: 'PageDown', keyCode: 34 }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x22); + assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); + }); + + it('should encode End with ENHANCED_KEY flag', () => { + const result = evaluateKeyboardEventWin32(createEvent({ code: 'End', key: 'End', keyCode: 35 }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x23); + assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); + }); + + it('should encode ArrowDown with ENHANCED_KEY flag', () => { + const result = evaluateKeyboardEventWin32(createEvent({ code: 'ArrowDown', key: 'ArrowDown', keyCode: 40 }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x28); + assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); + }); + + it('should encode ArrowLeft with ENHANCED_KEY flag', () => { + const result = evaluateKeyboardEventWin32(createEvent({ code: 'ArrowLeft', key: 'ArrowLeft', keyCode: 37 }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x25); + assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); + }); + + it('should encode ArrowRight with ENHANCED_KEY flag', () => { + const result = evaluateKeyboardEventWin32(createEvent({ code: 'ArrowRight', key: 'ArrowRight', keyCode: 39 }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x27); + assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); + }); + }); + + describe('additional numpad keys', () => { + it('should encode NumpadAdd', () => { + const result = evaluateKeyboardEventWin32(createEvent({ code: 'NumpadAdd', key: '+', keyCode: 107 }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x6B); + }); + + it('should encode NumpadSubtract', () => { + const result = evaluateKeyboardEventWin32(createEvent({ code: 'NumpadSubtract', key: '-', keyCode: 109 }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x6D); + }); + + it('should encode NumpadMultiply', () => { + const result = evaluateKeyboardEventWin32(createEvent({ code: 'NumpadMultiply', key: '*', keyCode: 106 }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x6A); + }); + + it('should encode NumpadDivide with ENHANCED_KEY', () => { + const result = evaluateKeyboardEventWin32(createEvent({ code: 'NumpadDivide', key: '/', keyCode: 111 }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x6F); + assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); + }); + + it('should encode NumpadDecimal', () => { + const result = evaluateKeyboardEventWin32(createEvent({ code: 'NumpadDecimal', key: '.', keyCode: 110 }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x6E); + }); + }); + + describe('meta key', () => { + it('should encode MetaLeft', () => { + const result = evaluateKeyboardEventWin32(createEvent({ + code: 'MetaLeft', + key: 'Meta', + keyCode: 91, + metaKey: true + }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x5B); + assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); + }); + + it('should encode MetaRight', () => { + const result = evaluateKeyboardEventWin32(createEvent({ + code: 'MetaRight', + key: 'Meta', + keyCode: 92, + metaKey: true + }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x5C); + assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); + }); + }); + + describe('unicode characters', () => { + it('should encode extended ASCII character', () => { + const result = evaluateKeyboardEventWin32(createEvent({ + code: 'KeyE', + key: 'é', + keyCode: 69 + }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.uc, 233); + }); + + it('should encode symbol character', () => { + const result = evaluateKeyboardEventWin32(createEvent({ + code: 'Digit4', + key: '$', + keyCode: 52, + shiftKey: true + }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.uc, 36); + }); + }); + + describe('function key range', () => { + it('should encode F5', () => { + const result = evaluateKeyboardEventWin32(createEvent({ code: 'F5', key: 'F5', keyCode: 116 }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x74); + }); + + it('should encode Ctrl+F1', () => { + const result = evaluateKeyboardEventWin32(createEvent({ + code: 'F1', + key: 'F1', + keyCode: 112, + ctrlKey: true + }), true); + const parsed = parseWin32Sequence(result.key!); + assert.ok(parsed); + assert.strictEqual(parsed.vk, 0x70); + assert.ok(parsed.cs & Win32ControlKeyState.LEFT_CTRL_PRESSED); + }); + }); }); }); From 0946386cc459a91fb90e4dd7e77f4662fc0bdc1f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 10 Jan 2026 05:48:30 -0800 Subject: [PATCH 4/8] Make unit tests more compact --- src/common/input/Win32InputMode.test.ts | 713 +++++------------------- 1 file changed, 144 insertions(+), 569 deletions(-) diff --git a/src/common/input/Win32InputMode.test.ts b/src/common/input/Win32InputMode.test.ts index 7ca65cf8..c61f02cd 100644 --- a/src/common/input/Win32InputMode.test.ts +++ b/src/common/input/Win32InputMode.test.ts @@ -3,614 +3,189 @@ import { assert } from 'chai'; import { evaluateKeyboardEventWin32, Win32ControlKeyState } from 'common/input/Win32InputMode'; import { IKeyboardEvent, KeyboardResultType } from 'common/Types'; -function createEvent(partialEvent: Partial = {}): IKeyboardEvent { - return { - altKey: partialEvent.altKey || false, - ctrlKey: partialEvent.ctrlKey || false, - shiftKey: partialEvent.shiftKey || false, - metaKey: partialEvent.metaKey || false, - keyCode: partialEvent.keyCode !== undefined ? partialEvent.keyCode : 0, - code: partialEvent.code || '', - key: partialEvent.key || '', - type: partialEvent.type || 'keydown' - }; -} +type EventOpts = Partial; +const ev = (opts: EventOpts): IKeyboardEvent => ({ + altKey: false, ctrlKey: false, shiftKey: false, metaKey: false, + keyCode: 0, code: '', key: '', type: 'keydown', ...opts +}); -function parseWin32Sequence(seq: string): { vk: number, sc: number, uc: number, kd: number, cs: number, rc: number } | null { - const match = seq.match(/^\x1b\[(\d+);(\d+);(\d+);(\d+);(\d+);(\d+)_$/); - if (!match) { - return null; - } - return { - vk: parseInt(match[1], 10), - sc: parseInt(match[2], 10), - uc: parseInt(match[3], 10), - kd: parseInt(match[4], 10), - cs: parseInt(match[5], 10), - rc: parseInt(match[6], 10) - }; -} +const parse = (seq: string) => { + const m = seq.match(/^\x1b\[(\d+);(\d+);(\d+);(\d+);(\d+);(\d+)_$/); + return m ? { vk: +m[1], sc: +m[2], uc: +m[3], kd: +m[4], cs: +m[5], rc: +m[6] } : null; +}; + +const test = (opts: EventOpts, isDown: boolean, check: (p: ReturnType) => void) => { + const result = evaluateKeyboardEventWin32(ev(opts), isDown); + const parsed = parse(result.key!); + assert.ok(parsed); + check(parsed); +}; describe('Win32InputMode', () => { describe('evaluateKeyboardEventWin32', () => { describe('basic key encoding', () => { - it('should encode letter key press', () => { - const result = evaluateKeyboardEventWin32(createEvent({ code: 'KeyA', key: 'a', keyCode: 65 }), true); + it('letter key press', () => { + const result = evaluateKeyboardEventWin32(ev({ code: 'KeyA', key: 'a', keyCode: 65 }), true); assert.strictEqual(result.type, KeyboardResultType.SEND_KEY); assert.strictEqual(result.cancel, true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x41); - assert.strictEqual(parsed.uc, 97); - assert.strictEqual(parsed.kd, 1); - assert.strictEqual(parsed.rc, 1); - }); - - it('should encode letter key release', () => { - const result = evaluateKeyboardEventWin32(createEvent({ code: 'KeyA', key: 'a', keyCode: 65 }), false); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.kd, 0); - }); - - it('should encode digit key', () => { - const result = evaluateKeyboardEventWin32(createEvent({ code: 'Digit1', key: '1', keyCode: 49 }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x31); - assert.strictEqual(parsed.uc, 49); - }); - - it('should encode Enter key', () => { - const result = evaluateKeyboardEventWin32(createEvent({ code: 'Enter', key: 'Enter', keyCode: 13 }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x0D); - assert.strictEqual(parsed.uc, 0); - }); - - it('should encode Escape key', () => { - const result = evaluateKeyboardEventWin32(createEvent({ code: 'Escape', key: 'Escape', keyCode: 27 }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x1B); - }); - - it('should encode Space key', () => { - const result = evaluateKeyboardEventWin32(createEvent({ code: 'Space', key: ' ', keyCode: 32 }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x20); - assert.strictEqual(parsed.uc, 32); + const p = parse(result.key!); + assert.ok(p); + assert.deepStrictEqual([p.vk, p.uc, p.kd, p.rc], [0x41, 97, 1, 1]); }); + it('letter key release', () => test({ code: 'KeyA', key: 'a', keyCode: 65 }, false, p => assert.strictEqual(p!.kd, 0))); + it('digit key', () => test({ code: 'Digit1', key: '1', keyCode: 49 }, true, p => assert.deepStrictEqual([p!.vk, p!.uc], [0x31, 49]))); + it('Enter key', () => test({ code: 'Enter', key: 'Enter', keyCode: 13 }, true, p => assert.deepStrictEqual([p!.vk, p!.uc], [0x0D, 0]))); + it('Escape key', () => test({ code: 'Escape', key: 'Escape', keyCode: 27 }, true, p => assert.strictEqual(p!.vk, 0x1B))); + it('Space key', () => test({ code: 'Space', key: ' ', keyCode: 32 }, true, p => assert.deepStrictEqual([p!.vk, p!.uc], [0x20, 32]))); }); describe('modifier encoding', () => { - it('should encode shift modifier', () => { - const result = evaluateKeyboardEventWin32(createEvent({ - code: 'KeyA', - key: 'A', - keyCode: 65, - shiftKey: true - }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.ok(parsed.cs & Win32ControlKeyState.SHIFT_PRESSED); - }); - - it('should encode ctrl modifier (left)', () => { - const result = evaluateKeyboardEventWin32(createEvent({ - code: 'KeyA', - key: 'a', - keyCode: 65, - ctrlKey: true - }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.ok(parsed.cs & Win32ControlKeyState.LEFT_CTRL_PRESSED); - }); - - it('should encode ctrl modifier (right)', () => { - const result = evaluateKeyboardEventWin32(createEvent({ - code: 'ControlRight', - key: 'Control', - keyCode: 17, - ctrlKey: true - }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.ok(parsed.cs & Win32ControlKeyState.RIGHT_CTRL_PRESSED); - assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); - }); - - it('should encode alt modifier (left)', () => { - const result = evaluateKeyboardEventWin32(createEvent({ - code: 'KeyA', - key: 'a', - keyCode: 65, - altKey: true - }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.ok(parsed.cs & Win32ControlKeyState.LEFT_ALT_PRESSED); - }); - - it('should encode alt modifier (right)', () => { - const result = evaluateKeyboardEventWin32(createEvent({ - code: 'AltRight', - key: 'Alt', - keyCode: 18, - altKey: true - }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.ok(parsed.cs & Win32ControlKeyState.RIGHT_ALT_PRESSED); - assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); - }); - - it('should encode multiple modifiers', () => { - const result = evaluateKeyboardEventWin32(createEvent({ - code: 'KeyA', - key: 'A', - keyCode: 65, - shiftKey: true, - ctrlKey: true, - altKey: true - }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.ok(parsed.cs & Win32ControlKeyState.SHIFT_PRESSED); - assert.ok(parsed.cs & Win32ControlKeyState.LEFT_CTRL_PRESSED); - assert.ok(parsed.cs & Win32ControlKeyState.LEFT_ALT_PRESSED); - }); + it('shift', () => test({ code: 'KeyA', key: 'A', keyCode: 65, shiftKey: true }, true, p => assert.ok(p!.cs & Win32ControlKeyState.SHIFT_PRESSED))); + it('ctrl left', () => test({ code: 'KeyA', key: 'a', keyCode: 65, ctrlKey: true }, true, p => assert.ok(p!.cs & Win32ControlKeyState.LEFT_CTRL_PRESSED))); + it('ctrl right', () => test({ code: 'ControlRight', key: 'Control', keyCode: 17, ctrlKey: true }, true, p => { + assert.ok(p!.cs & Win32ControlKeyState.RIGHT_CTRL_PRESSED); + assert.ok(p!.cs & Win32ControlKeyState.ENHANCED_KEY); + })); + it('alt left', () => test({ code: 'KeyA', key: 'a', keyCode: 65, altKey: true }, true, p => assert.ok(p!.cs & Win32ControlKeyState.LEFT_ALT_PRESSED))); + it('alt right', () => test({ code: 'AltRight', key: 'Alt', keyCode: 18, altKey: true }, true, p => { + assert.ok(p!.cs & Win32ControlKeyState.RIGHT_ALT_PRESSED); + assert.ok(p!.cs & Win32ControlKeyState.ENHANCED_KEY); + })); + it('multiple modifiers', () => test({ code: 'KeyA', key: 'A', keyCode: 65, shiftKey: true, ctrlKey: true, altKey: true }, true, p => { + assert.ok(p!.cs & Win32ControlKeyState.SHIFT_PRESSED); + assert.ok(p!.cs & Win32ControlKeyState.LEFT_CTRL_PRESSED); + assert.ok(p!.cs & Win32ControlKeyState.LEFT_ALT_PRESSED); + })); }); describe('function keys', () => { - it('should encode F1', () => { - const result = evaluateKeyboardEventWin32(createEvent({ code: 'F1', key: 'F1', keyCode: 112 }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x70); - }); - - it('should encode F12', () => { - const result = evaluateKeyboardEventWin32(createEvent({ code: 'F12', key: 'F12', keyCode: 123 }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x7B); - }); + it('F1', () => test({ code: 'F1', key: 'F1', keyCode: 112 }, true, p => assert.strictEqual(p!.vk, 0x70))); + it('F5', () => test({ code: 'F5', key: 'F5', keyCode: 116 }, true, p => assert.strictEqual(p!.vk, 0x74))); + it('F12', () => test({ code: 'F12', key: 'F12', keyCode: 123 }, true, p => assert.strictEqual(p!.vk, 0x7B))); + it('Ctrl+F1', () => test({ code: 'F1', key: 'F1', keyCode: 112, ctrlKey: true }, true, p => { + assert.strictEqual(p!.vk, 0x70); + assert.ok(p!.cs & Win32ControlKeyState.LEFT_CTRL_PRESSED); + })); }); - describe('navigation keys', () => { - it('should encode arrow up with ENHANCED_KEY flag', () => { - const result = evaluateKeyboardEventWin32(createEvent({ code: 'ArrowUp', key: 'ArrowUp', keyCode: 38 }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x26); - assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); - }); - - it('should encode Home with ENHANCED_KEY flag', () => { - const result = evaluateKeyboardEventWin32(createEvent({ code: 'Home', key: 'Home', keyCode: 36 }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x24); - assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); - }); - - it('should encode Insert with ENHANCED_KEY flag', () => { - const result = evaluateKeyboardEventWin32(createEvent({ code: 'Insert', key: 'Insert', keyCode: 45 }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x2D); - assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); - }); - - it('should encode Delete with ENHANCED_KEY flag', () => { - const result = evaluateKeyboardEventWin32(createEvent({ code: 'Delete', key: 'Delete', keyCode: 46 }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x2E); - assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); + describe('navigation keys (ENHANCED_KEY)', () => { + const navKeys: [string, string, number, number][] = [ + ['ArrowUp', 'ArrowUp', 38, 0x26], + ['ArrowDown', 'ArrowDown', 40, 0x28], + ['ArrowLeft', 'ArrowLeft', 37, 0x25], + ['ArrowRight', 'ArrowRight', 39, 0x27], + ['Home', 'Home', 36, 0x24], + ['End', 'End', 35, 0x23], + ['PageUp', 'PageUp', 33, 0x21], + ['PageDown', 'PageDown', 34, 0x22], + ['Insert', 'Insert', 45, 0x2D], + ['Delete', 'Delete', 46, 0x2E], + ]; + navKeys.forEach(([code, key, keyCode, vk]) => { + it(code, () => test({ code, key, keyCode }, true, p => { + assert.strictEqual(p!.vk, vk); + assert.ok(p!.cs & Win32ControlKeyState.ENHANCED_KEY); + })); }); + it('Tab', () => test({ code: 'Tab', key: 'Tab', keyCode: 9 }, true, p => assert.strictEqual(p!.vk, 0x09))); + it('Backspace', () => test({ code: 'Backspace', key: 'Backspace', keyCode: 8 }, true, p => assert.strictEqual(p!.vk, 0x08))); }); describe('numpad keys', () => { - it('should encode Numpad0', () => { - const result = evaluateKeyboardEventWin32(createEvent({ code: 'Numpad0', key: '0', keyCode: 96 }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x60); - }); - - it('should encode NumpadEnter with ENHANCED_KEY', () => { - const result = evaluateKeyboardEventWin32(createEvent({ code: 'NumpadEnter', key: 'Enter', keyCode: 13 }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x0D); - assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); - }); + it('Numpad0', () => test({ code: 'Numpad0', key: '0', keyCode: 96 }, true, p => assert.strictEqual(p!.vk, 0x60))); + it('NumpadEnter (ENHANCED)', () => test({ code: 'NumpadEnter', key: 'Enter', keyCode: 13 }, true, p => { + assert.strictEqual(p!.vk, 0x0D); + assert.ok(p!.cs & Win32ControlKeyState.ENHANCED_KEY); + })); + it('NumpadAdd', () => test({ code: 'NumpadAdd', key: '+', keyCode: 107 }, true, p => assert.strictEqual(p!.vk, 0x6B))); + it('NumpadSubtract', () => test({ code: 'NumpadSubtract', key: '-', keyCode: 109 }, true, p => assert.strictEqual(p!.vk, 0x6D))); + it('NumpadMultiply', () => test({ code: 'NumpadMultiply', key: '*', keyCode: 106 }, true, p => assert.strictEqual(p!.vk, 0x6A))); + it('NumpadDivide (ENHANCED)', () => test({ code: 'NumpadDivide', key: '/', keyCode: 111 }, true, p => { + assert.strictEqual(p!.vk, 0x6F); + assert.ok(p!.cs & Win32ControlKeyState.ENHANCED_KEY); + })); + it('NumpadDecimal', () => test({ code: 'NumpadDecimal', key: '.', keyCode: 110 }, true, p => assert.strictEqual(p!.vk, 0x6E))); }); describe('unicode character', () => { - it('should include unicode codepoint for printable characters', () => { - const result = evaluateKeyboardEventWin32(createEvent({ code: 'KeyA', key: 'a', keyCode: 65 }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.uc, 97); - }); - - it('should include unicode codepoint for shifted characters', () => { - const result = evaluateKeyboardEventWin32(createEvent({ - code: 'KeyA', - key: 'A', - keyCode: 65, - shiftKey: true - }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.uc, 65); - }); - - it('should set unicode to 0 for non-printable keys', () => { - const result = evaluateKeyboardEventWin32(createEvent({ code: 'ArrowUp', key: 'ArrowUp', keyCode: 38 }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.uc, 0); - }); + it('printable', () => test({ code: 'KeyA', key: 'a', keyCode: 65 }, true, p => assert.strictEqual(p!.uc, 97))); + it('shifted', () => test({ code: 'KeyA', key: 'A', keyCode: 65, shiftKey: true }, true, p => assert.strictEqual(p!.uc, 65))); + it('non-printable is 0', () => test({ code: 'ArrowUp', key: 'ArrowUp', keyCode: 38 }, true, p => assert.strictEqual(p!.uc, 0))); + it('extended ASCII', () => test({ code: 'KeyE', key: 'é', keyCode: 69 }, true, p => assert.strictEqual(p!.uc, 233))); + it('symbol', () => test({ code: 'Digit4', key: '$', keyCode: 52, shiftKey: true }, true, p => assert.strictEqual(p!.uc, 36))); }); describe('scan codes', () => { - it('should include approximate scan code for letter', () => { - const result = evaluateKeyboardEventWin32(createEvent({ code: 'KeyA', key: 'a', keyCode: 65 }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.sc, 0x1E); - }); - - it('should include approximate scan code for Escape', () => { - const result = evaluateKeyboardEventWin32(createEvent({ code: 'Escape', key: 'Escape', keyCode: 27 }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.sc, 0x01); - }); + it('letter A', () => test({ code: 'KeyA', key: 'a', keyCode: 65 }, true, p => assert.strictEqual(p!.sc, 0x1E))); + it('Escape', () => test({ code: 'Escape', key: 'Escape', keyCode: 27 }, true, p => assert.strictEqual(p!.sc, 0x01))); }); describe('sequence format', () => { - it('should produce valid CSI sequence format', () => { - const result = evaluateKeyboardEventWin32(createEvent({ code: 'KeyA', key: 'a', keyCode: 65 }), true); - assert.ok(result.key); - assert.ok(result.key.startsWith('\x1b[')); - assert.ok(result.key.endsWith('_')); - const parts = result.key.slice(2, -1).split(';'); - assert.strictEqual(parts.length, 6); + it('valid CSI format', () => { + const result = evaluateKeyboardEventWin32(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); }); }); describe('standalone modifier keys', () => { - it('should encode ShiftLeft alone', () => { - const result = evaluateKeyboardEventWin32(createEvent({ - code: 'ShiftLeft', - key: 'Shift', - keyCode: 16, - shiftKey: true - }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x10); - assert.ok(parsed.cs & Win32ControlKeyState.SHIFT_PRESSED); - }); - - it('should encode ShiftRight alone', () => { - const result = evaluateKeyboardEventWin32(createEvent({ - code: 'ShiftRight', - key: 'Shift', - keyCode: 16, - shiftKey: true - }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x10); - assert.ok(parsed.cs & Win32ControlKeyState.SHIFT_PRESSED); - }); - - it('should encode ControlLeft alone', () => { - const result = evaluateKeyboardEventWin32(createEvent({ - code: 'ControlLeft', - key: 'Control', - keyCode: 17, - ctrlKey: true - }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x11); - assert.ok(parsed.cs & Win32ControlKeyState.LEFT_CTRL_PRESSED); - }); - - it('should encode ControlRight alone', () => { - const result = evaluateKeyboardEventWin32(createEvent({ - code: 'ControlRight', - key: 'Control', - keyCode: 17, - ctrlKey: true - }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x11); - assert.ok(parsed.cs & Win32ControlKeyState.RIGHT_CTRL_PRESSED); - assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); - }); - - it('should encode AltLeft alone', () => { - const result = evaluateKeyboardEventWin32(createEvent({ - code: 'AltLeft', - key: 'Alt', - keyCode: 18, - altKey: true - }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x12); - assert.ok(parsed.cs & Win32ControlKeyState.LEFT_ALT_PRESSED); - }); - - it('should encode AltRight alone', () => { - const result = evaluateKeyboardEventWin32(createEvent({ - code: 'AltRight', - key: 'Alt', - keyCode: 18, - altKey: true - }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x12); - assert.ok(parsed.cs & Win32ControlKeyState.RIGHT_ALT_PRESSED); - assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); - }); - - it('should encode modifier key release', () => { - const result = evaluateKeyboardEventWin32(createEvent({ - code: 'ShiftLeft', - key: 'Shift', - keyCode: 16, - shiftKey: false - }), false); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.kd, 0); - }); + it('ShiftLeft', () => test({ code: 'ShiftLeft', key: 'Shift', keyCode: 16, shiftKey: true }, true, p => { + assert.strictEqual(p!.vk, 0x10); + assert.ok(p!.cs & Win32ControlKeyState.SHIFT_PRESSED); + })); + it('ShiftRight', () => test({ code: 'ShiftRight', key: 'Shift', keyCode: 16, shiftKey: true }, true, p => { + assert.strictEqual(p!.vk, 0x10); + assert.ok(p!.cs & Win32ControlKeyState.SHIFT_PRESSED); + })); + it('ControlLeft', () => test({ code: 'ControlLeft', key: 'Control', keyCode: 17, ctrlKey: true }, true, p => { + assert.strictEqual(p!.vk, 0x11); + assert.ok(p!.cs & Win32ControlKeyState.LEFT_CTRL_PRESSED); + })); + it('ControlRight', () => test({ code: 'ControlRight', key: 'Control', keyCode: 17, ctrlKey: true }, true, p => { + assert.strictEqual(p!.vk, 0x11); + assert.ok(p!.cs & Win32ControlKeyState.RIGHT_CTRL_PRESSED); + assert.ok(p!.cs & Win32ControlKeyState.ENHANCED_KEY); + })); + it('AltLeft', () => test({ code: 'AltLeft', key: 'Alt', keyCode: 18, altKey: true }, true, p => { + assert.strictEqual(p!.vk, 0x12); + assert.ok(p!.cs & Win32ControlKeyState.LEFT_ALT_PRESSED); + })); + it('AltRight', () => test({ code: 'AltRight', key: 'Alt', keyCode: 18, altKey: true }, true, p => { + assert.strictEqual(p!.vk, 0x12); + assert.ok(p!.cs & Win32ControlKeyState.RIGHT_ALT_PRESSED); + assert.ok(p!.cs & Win32ControlKeyState.ENHANCED_KEY); + })); + it('modifier release', () => test({ code: 'ShiftLeft', key: 'Shift', keyCode: 16 }, false, p => assert.strictEqual(p!.kd, 0))); }); describe('problem keys from spec', () => { - it('should encode Ctrl+Space', () => { - const result = evaluateKeyboardEventWin32(createEvent({ - code: 'Space', - key: ' ', - keyCode: 32, - ctrlKey: true - }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x20); - assert.ok(parsed.cs & Win32ControlKeyState.LEFT_CTRL_PRESSED); - }); - - it('should encode Shift+Enter', () => { - const result = evaluateKeyboardEventWin32(createEvent({ - code: 'Enter', - key: 'Enter', - keyCode: 13, - shiftKey: true - }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x0D); - assert.ok(parsed.cs & Win32ControlKeyState.SHIFT_PRESSED); - }); - - it('should encode Ctrl+Break', () => { - const result = evaluateKeyboardEventWin32(createEvent({ - code: 'Pause', - key: 'Pause', - keyCode: 19, - ctrlKey: true - }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x13); - assert.ok(parsed.cs & Win32ControlKeyState.LEFT_CTRL_PRESSED); - }); - - it('should encode Ctrl+Alt+/', () => { - const result = evaluateKeyboardEventWin32(createEvent({ - code: 'Slash', - key: '/', - keyCode: 191, - ctrlKey: true, - altKey: true - }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.ok(parsed.cs & Win32ControlKeyState.LEFT_CTRL_PRESSED); - assert.ok(parsed.cs & Win32ControlKeyState.LEFT_ALT_PRESSED); - }); - }); - - describe('additional navigation keys', () => { - it('should encode Tab', () => { - const result = evaluateKeyboardEventWin32(createEvent({ code: 'Tab', key: 'Tab', keyCode: 9 }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x09); - }); - - it('should encode Backspace', () => { - const result = evaluateKeyboardEventWin32(createEvent({ code: 'Backspace', key: 'Backspace', keyCode: 8 }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x08); - }); - - it('should encode PageUp with ENHANCED_KEY flag', () => { - const result = evaluateKeyboardEventWin32(createEvent({ code: 'PageUp', key: 'PageUp', keyCode: 33 }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x21); - assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); - }); - - it('should encode PageDown with ENHANCED_KEY flag', () => { - const result = evaluateKeyboardEventWin32(createEvent({ code: 'PageDown', key: 'PageDown', keyCode: 34 }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x22); - assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); - }); - - it('should encode End with ENHANCED_KEY flag', () => { - const result = evaluateKeyboardEventWin32(createEvent({ code: 'End', key: 'End', keyCode: 35 }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x23); - assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); - }); - - it('should encode ArrowDown with ENHANCED_KEY flag', () => { - const result = evaluateKeyboardEventWin32(createEvent({ code: 'ArrowDown', key: 'ArrowDown', keyCode: 40 }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x28); - assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); - }); - - it('should encode ArrowLeft with ENHANCED_KEY flag', () => { - const result = evaluateKeyboardEventWin32(createEvent({ code: 'ArrowLeft', key: 'ArrowLeft', keyCode: 37 }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x25); - assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); - }); - - it('should encode ArrowRight with ENHANCED_KEY flag', () => { - const result = evaluateKeyboardEventWin32(createEvent({ code: 'ArrowRight', key: 'ArrowRight', keyCode: 39 }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x27); - assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); - }); - }); - - describe('additional numpad keys', () => { - it('should encode NumpadAdd', () => { - const result = evaluateKeyboardEventWin32(createEvent({ code: 'NumpadAdd', key: '+', keyCode: 107 }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x6B); - }); - - it('should encode NumpadSubtract', () => { - const result = evaluateKeyboardEventWin32(createEvent({ code: 'NumpadSubtract', key: '-', keyCode: 109 }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x6D); - }); - - it('should encode NumpadMultiply', () => { - const result = evaluateKeyboardEventWin32(createEvent({ code: 'NumpadMultiply', key: '*', keyCode: 106 }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x6A); - }); - - it('should encode NumpadDivide with ENHANCED_KEY', () => { - const result = evaluateKeyboardEventWin32(createEvent({ code: 'NumpadDivide', key: '/', keyCode: 111 }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x6F); - assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); - }); - - it('should encode NumpadDecimal', () => { - const result = evaluateKeyboardEventWin32(createEvent({ code: 'NumpadDecimal', key: '.', keyCode: 110 }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x6E); - }); + it('Ctrl+Space', () => test({ code: 'Space', key: ' ', keyCode: 32, ctrlKey: true }, true, p => { + assert.strictEqual(p!.vk, 0x20); + assert.ok(p!.cs & Win32ControlKeyState.LEFT_CTRL_PRESSED); + })); + it('Shift+Enter', () => test({ code: 'Enter', key: 'Enter', keyCode: 13, shiftKey: true }, true, p => { + assert.strictEqual(p!.vk, 0x0D); + assert.ok(p!.cs & Win32ControlKeyState.SHIFT_PRESSED); + })); + it('Ctrl+Break', () => test({ code: 'Pause', key: 'Pause', keyCode: 19, ctrlKey: true }, true, p => { + assert.strictEqual(p!.vk, 0x13); + assert.ok(p!.cs & Win32ControlKeyState.LEFT_CTRL_PRESSED); + })); + it('Ctrl+Alt+/', () => test({ code: 'Slash', key: '/', keyCode: 191, ctrlKey: true, altKey: true }, true, p => { + assert.ok(p!.cs & Win32ControlKeyState.LEFT_CTRL_PRESSED); + assert.ok(p!.cs & Win32ControlKeyState.LEFT_ALT_PRESSED); + })); }); describe('meta key', () => { - it('should encode MetaLeft', () => { - const result = evaluateKeyboardEventWin32(createEvent({ - code: 'MetaLeft', - key: 'Meta', - keyCode: 91, - metaKey: true - }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x5B); - assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); - }); - - it('should encode MetaRight', () => { - const result = evaluateKeyboardEventWin32(createEvent({ - code: 'MetaRight', - key: 'Meta', - keyCode: 92, - metaKey: true - }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x5C); - assert.ok(parsed.cs & Win32ControlKeyState.ENHANCED_KEY); - }); - }); - - describe('unicode characters', () => { - it('should encode extended ASCII character', () => { - const result = evaluateKeyboardEventWin32(createEvent({ - code: 'KeyE', - key: 'é', - keyCode: 69 - }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.uc, 233); - }); - - it('should encode symbol character', () => { - const result = evaluateKeyboardEventWin32(createEvent({ - code: 'Digit4', - key: '$', - keyCode: 52, - shiftKey: true - }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.uc, 36); - }); - }); - - describe('function key range', () => { - it('should encode F5', () => { - const result = evaluateKeyboardEventWin32(createEvent({ code: 'F5', key: 'F5', keyCode: 116 }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x74); - }); - - it('should encode Ctrl+F1', () => { - const result = evaluateKeyboardEventWin32(createEvent({ - code: 'F1', - key: 'F1', - keyCode: 112, - ctrlKey: true - }), true); - const parsed = parseWin32Sequence(result.key!); - assert.ok(parsed); - assert.strictEqual(parsed.vk, 0x70); - assert.ok(parsed.cs & Win32ControlKeyState.LEFT_CTRL_PRESSED); - }); + it('MetaLeft', () => test({ code: 'MetaLeft', key: 'Meta', keyCode: 91, metaKey: true }, true, p => { + assert.strictEqual(p!.vk, 0x5B); + assert.ok(p!.cs & Win32ControlKeyState.ENHANCED_KEY); + })); + it('MetaRight', () => test({ code: 'MetaRight', key: 'Meta', keyCode: 92, metaKey: true }, true, p => { + assert.strictEqual(p!.vk, 0x5C); + assert.ok(p!.cs & Win32ControlKeyState.ENHANCED_KEY); + })); }); }); }); From 53dc85eca2a495c192a7a0d0ec0de8858647fd83 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 10 Jan 2026 05:48:48 -0800 Subject: [PATCH 5/8] Add copyright --- src/common/input/Win32InputMode.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/common/input/Win32InputMode.test.ts b/src/common/input/Win32InputMode.test.ts index c61f02cd..b3b72e6f 100644 --- a/src/common/input/Win32InputMode.test.ts +++ b/src/common/input/Win32InputMode.test.ts @@ -1,3 +1,7 @@ +/** + * Copyright (c) 2026 The xterm.js authors. All rights reserved. + * @license MIT + */ import { assert } from 'chai'; import { evaluateKeyboardEventWin32, Win32ControlKeyState } from 'common/input/Win32InputMode'; From 8b35c67dd313e2caef9b8bc0bb43929207f14580 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 10 Jan 2026 05:52:19 -0800 Subject: [PATCH 6/8] useWin32 -> useWin32InputMode --- src/browser/services/KeyboardService.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/browser/services/KeyboardService.ts b/src/browser/services/KeyboardService.ts index 1095d30f..478a262e 100644 --- a/src/browser/services/KeyboardService.ts +++ b/src/browser/services/KeyboardService.ts @@ -22,7 +22,7 @@ export class KeyboardService implements IKeyboardService { public evaluateKeyDown(event: KeyboardEvent): IKeyboardResult { // Win32 input mode takes priority (most raw) - if (this.useWin32) { + if (this.useWin32InputMode) { return evaluateKeyboardEventWin32(event, true); } const kittyFlags = this._coreService.kittyKeyboard.flags; @@ -33,7 +33,7 @@ export class KeyboardService implements IKeyboardService { public evaluateKeyUp(event: KeyboardEvent): IKeyboardResult | undefined { // Win32 input mode sends key up events - if (this.useWin32) { + if (this.useWin32InputMode) { return evaluateKeyboardEventWin32(event, false); } const kittyFlags = this._coreService.kittyKeyboard.flags; @@ -48,7 +48,7 @@ export class KeyboardService implements IKeyboardService { return !!(this._optionsService.rawOptions.vtExtensions?.kittyKeyboard && shouldUseKittyProtocol(kittyFlags)); } - public get useWin32(): boolean { + public get useWin32InputMode(): boolean { return !!(this._optionsService.rawOptions.vtExtensions?.win32InputMode && this._coreService.decPrivateModes.win32InputMode); } } From f88650ec1cb1726693c17ec7cf5bc16c3591969f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 10 Jan 2026 05:54:48 -0800 Subject: [PATCH 7/8] Use win32InputMode in demo on Windows --- demo/client/client.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/demo/client/client.ts b/demo/client/client.ts index 68202ad7..1ed83669 100644 --- a/demo/client/client.ts +++ b/demo/client/client.ts @@ -282,7 +282,10 @@ function createTerminal(): Terminal { buildNumber: 22621 } : undefined, fontFamily: '"Fira Code", monospace, "Powerline Extra Symbols"', - theme: { ...xtermjsTheme } + theme: { ...xtermjsTheme }, + vtExtensions: { + win32InputMode: isWindows + } } as ITerminalOptions); // Load addons From 9d0beb7c3871dbf7e0e17cdd781b70c5a3e338e3 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 10 Jan 2026 05:57:13 -0800 Subject: [PATCH 8/8] Use 0b over 0x for flags --- src/common/input/Win32InputMode.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/common/input/Win32InputMode.ts b/src/common/input/Win32InputMode.ts index c606fcfc..c1c88cf2 100644 --- a/src/common/input/Win32InputMode.ts +++ b/src/common/input/Win32InputMode.ts @@ -21,15 +21,15 @@ import { C0 } from 'common/data/EscapeSequences'; * Win32 control key state flags (from Windows API). */ export const enum Win32ControlKeyState { - RIGHT_ALT_PRESSED = 0x0001, - LEFT_ALT_PRESSED = 0x0002, - RIGHT_CTRL_PRESSED = 0x0004, - LEFT_CTRL_PRESSED = 0x0008, - SHIFT_PRESSED = 0x0010, - NUMLOCK_ON = 0x0020, - SCROLLLOCK_ON = 0x0040, - CAPSLOCK_ON = 0x0080, - ENHANCED_KEY = 0x0100, + RIGHT_ALT_PRESSED = 0b000000001, + LEFT_ALT_PRESSED = 0b000000010, + RIGHT_CTRL_PRESSED = 0b000000100, + LEFT_CTRL_PRESSED = 0b000001000, + SHIFT_PRESSED = 0b000010000, + NUMLOCK_ON = 0b000100000, + SCROLLLOCK_ON = 0b001000000, + CAPSLOCK_ON = 0b010000000, + ENHANCED_KEY = 0b100000000, } /**