mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
@@ -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
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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.useWin32InputMode) {
|
||||
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.useWin32InputMode) {
|
||||
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 useWin32InputMode(): boolean {
|
||||
return !!(this._optionsService.rawOptions.vtExtensions?.win32InputMode && this._coreService.decPrivateModes.win32InputMode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -112,6 +112,7 @@ export class MockCoreService implements ICoreService {
|
||||
reverseWraparound: false,
|
||||
sendFocus: false,
|
||||
synchronizedOutput: false,
|
||||
win32InputMode: false,
|
||||
wraparound: true
|
||||
};
|
||||
public kittyKeyboard = {
|
||||
|
||||
@@ -274,6 +274,7 @@ export interface IDecPrivateModes {
|
||||
reverseWraparound: boolean;
|
||||
sendFocus: boolean;
|
||||
synchronizedOutput: boolean;
|
||||
win32InputMode: boolean;
|
||||
wraparound: boolean; // defaults: xterm - true, vt100 - false
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { evaluateKeyboardEventWin32, Win32ControlKeyState } from 'common/input/Win32InputMode';
|
||||
import { IKeyboardEvent, KeyboardResultType } from 'common/Types';
|
||||
|
||||
type EventOpts = Partial<IKeyboardEvent>;
|
||||
const ev = (opts: EventOpts): IKeyboardEvent => ({
|
||||
altKey: false, ctrlKey: false, shiftKey: false, metaKey: false,
|
||||
keyCode: 0, code: '', key: '', type: 'keydown', ...opts
|
||||
});
|
||||
|
||||
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<typeof parse>) => 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('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 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('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('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 (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('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('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('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('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('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('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('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);
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 = 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,
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
|
||||
|
||||
@@ -311,6 +311,7 @@ export interface ITerminalQuirks {
|
||||
export interface IVtExtensions {
|
||||
kittyKeyboard?: boolean;
|
||||
kittySgrBoldFaintControl?: boolean;
|
||||
win32InputMode?: boolean;
|
||||
}
|
||||
|
||||
export const IOscLinkService = createDecorator<IOscLinkService>('OscLinkService');
|
||||
|
||||
@@ -411,6 +411,7 @@ describe('Headless API Tests', function (): void {
|
||||
sendFocusMode: false,
|
||||
showCursor: true,
|
||||
synchronizedOutputMode: false,
|
||||
win32InputMode: false,
|
||||
wraparoundMode: true
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -672,6 +672,7 @@ test.describe('API Integration Tests', () => {
|
||||
sendFocusMode: false,
|
||||
showCursor: true,
|
||||
synchronizedOutputMode: false,
|
||||
win32InputMode: false,
|
||||
wraparoundMode: true
|
||||
});
|
||||
});
|
||||
|
||||
Vendored
+16
@@ -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`
|
||||
*/
|
||||
|
||||
Vendored
+16
@@ -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`
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user