diff --git a/bin/lint_changes.js b/bin/lint_changes.js
index a8edb44c1d..6ed7ae5fa7 100644
--- a/bin/lint_changes.js
+++ b/bin/lint_changes.js
@@ -33,7 +33,7 @@ if (files.length === 0) {
console.log(`Linting ${files.length} changed file(s)...`);
-const eslintArgs = ['--max-warnings', '0'];
+const eslintArgs = ['--max-warnings', '0', '--no-warn-ignored'];
if (fix) {
eslintArgs.push('--fix');
}
diff --git a/demo/client/client.ts b/demo/client/client.ts
index 68202ad7ca..1ed8366961 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
diff --git a/demo/client/components/window/optionsWindow.ts b/demo/client/components/window/optionsWindow.ts
index d084784fd4..7f8c4e2fc8 100644
--- a/demo/client/components/window/optionsWindow.ts
+++ b/demo/client/components/window/optionsWindow.ts
@@ -119,9 +119,15 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
'overviewRuler',
'quirks',
'theme',
+ 'vtExtensions',
'windowOptions',
'windowsPty',
];
+ const nestedBooleanOptions: { label: string, parent: string, prop: string }[] = [
+ { label: 'vtExtensions.kittyKeyboard', parent: 'vtExtensions', prop: 'kittyKeyboard' },
+ { label: 'vtExtensions.kittySgrBoldFaintControl', parent: 'vtExtensions', prop: 'kittySgrBoldFaintControl' },
+ { label: 'vtExtensions.win32InputMode', parent: 'vtExtensions', prop: 'win32InputMode' }
+ ];
const stringOptions: { [key: string]: string[] | null } = {
cursorStyle: ['block', 'underline', 'bar'],
cursorInactiveStyle: ['outline', 'block', 'bar', 'underline', 'none'],
@@ -156,6 +162,10 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
booleanOptions.forEach(o => {
html += `
';
numberOptions.forEach(o => {
html += `
`;
@@ -187,6 +197,13 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
}
});
});
+ nestedBooleanOptions.forEach(({ label, parent, prop }) => {
+ const input = document.getElementById(`opt-${label.replace('.', '-')}`) as HTMLInputElement;
+ addDomListener(input, 'change', () => {
+ console.log('change', label, input.checked);
+ this._terminal.options[parent] = { ...this._terminal.options[parent], [prop]: input.checked };
+ });
+ });
numberOptions.forEach(o => {
const input = document.getElementById(`opt-${o}`) as HTMLInputElement;
addDomListener(input, 'change', () => {
diff --git a/src/browser/CoreBrowserTerminal.ts b/src/browser/CoreBrowserTerminal.ts
index abc363ed14..dca090f673 100644
--- a/src/browser/CoreBrowserTerminal.ts
+++ b/src/browser/CoreBrowserTerminal.ts
@@ -39,8 +39,9 @@ import { LinkProviderService } from 'browser/services/LinkProviderService';
import { MouseService } from 'browser/services/MouseService';
import { RenderService } from 'browser/services/RenderService';
import { SelectionService } from 'browser/services/SelectionService';
-import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, ILinkProviderService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
+import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IKeyboardService, ILinkProviderService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
import { ThemeService } from 'browser/services/ThemeService';
+import { KeyboardService } from 'browser/services/KeyboardService';
import { channels, color } from 'common/Color';
import { CoreTerminal } from 'common/CoreTerminal';
import * as Browser from 'common/Platform';
@@ -48,7 +49,6 @@ import { ColorRequestType, CoreMouseAction, CoreMouseButton, CoreMouseEventType,
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { IBuffer } from 'common/buffer/Types';
import { C0, C1_ESCAPED } from 'common/data/EscapeSequences';
-import { evaluateKeyboardEvent } from 'common/input/Keyboard';
import { toRgbString } from 'common/input/XParseColor';
import { DecorationService } from 'common/services/DecorationService';
import { IDecorationService } from 'common/services/Services';
@@ -80,8 +80,9 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
private _customWheelEventHandler: CustomWheelEventHandler | undefined;
// Browser services
- private _decorationService: DecorationService;
- private _linkProviderService: ILinkProviderService;
+ private readonly _decorationService: DecorationService;
+ private readonly _keyboardService: IKeyboardService;
+ private readonly _linkProviderService: ILinkProviderService;
// Optional browser services
private _charSizeService: ICharSizeService | undefined;
@@ -173,6 +174,8 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
this._decorationService = this._instantiationService.createInstance(DecorationService);
this._instantiationService.setService(IDecorationService, this._decorationService);
+ this._keyboardService = this._instantiationService.createInstance(KeyboardService);
+ this._instantiationService.setService(IKeyboardService, this._keyboardService);
this._linkProviderService = this._instantiationService.createInstance(LinkProviderService);
this._instantiationService.setService(ILinkProviderService, this._linkProviderService);
this._linkProviderService.registerLinkProvider(this._instantiationService.createInstance(OscLinkProvider));
@@ -1081,7 +1084,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
this._unprocessedDeadKey = true;
}
- const result = evaluateKeyboardEvent(event, this.coreService.decPrivateModes.applicationCursorKeys, this.browser.isMac, this.options.macOptionIsMeta);
+ const result = this._keyboardService.evaluateKeyDown(event);
this.updateCursorStyle(event);
@@ -1109,8 +1112,9 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
}
// HACK: Process A-Z in the keypress event to fix an issue with macOS IMEs where lower case
- // letters cannot be input while caps lock is on.
- if (event.key && !event.ctrlKey && !event.altKey && !event.metaKey && event.key.length === 1) {
+ // letters cannot be input while caps lock is on. Skip this hack when using kitty protocol
+ // as it needs to send proper CSI u sequences for all key events.
+ if (!this._keyboardService.useKitty && event.key && !event.ctrlKey && !event.altKey && !event.metaKey && event.key.length === 1) {
if (event.key.charCodeAt(0) >= 65 && event.key.charCodeAt(0) <= 90) {
return true;
}
@@ -1168,6 +1172,12 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
this.focus();
}
+ // Handle key release for Kitty keyboard protocol
+ const result = this._keyboardService.evaluateKeyUp(ev);
+ if (result?.key) {
+ this.coreService.triggerDataEvent(result.key, true);
+ }
+
this.updateCursorStyle(ev);
this._keyPressHandled = false;
}
diff --git a/src/browser/public/Terminal.ts b/src/browser/public/Terminal.ts
index 157ceb1457..442f8b6a7f 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
new file mode 100644
index 0000000000..478a262e9c
--- /dev/null
+++ b/src/browser/services/KeyboardService.ts
@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) 2025 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+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';
+
+export class KeyboardService implements IKeyboardService {
+ public serviceBrand: undefined;
+
+ constructor(
+ @ICoreService private readonly _coreService: ICoreService,
+ @IOptionsService private readonly _optionsService: IOptionsService
+ ) {
+ }
+
+ 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)
+ : evaluateKeyboardEvent(event, this._coreService.decPrivateModes.applicationCursorKeys, isMac, this._optionsService.rawOptions.macOptionIsMeta);
+ }
+
+ public evaluateKeyUp(event: KeyboardEvent): IKeyboardResult | undefined {
+ // Win32 input mode sends key up events
+ if (this.useWin32InputMode) {
+ return evaluateKeyboardEventWin32(event, false);
+ }
+ const kittyFlags = this._coreService.kittyKeyboard.flags;
+ if (this.useKitty && (kittyFlags & KittyKeyboardFlags.REPORT_EVENT_TYPES)) {
+ return evaluateKeyboardEventKitty(event, kittyFlags, KittyKeyboardEventType.RELEASE);
+ }
+ return undefined;
+ }
+
+ public get useKitty(): boolean {
+ 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);
+ }
+}
diff --git a/src/browser/services/Services.ts b/src/browser/services/Services.ts
index 6103ada1b1..cac62915aa 100644
--- a/src/browser/services/Services.ts
+++ b/src/browser/services/Services.ts
@@ -7,7 +7,7 @@ import { IRenderDimensions, IRenderer } from 'browser/renderer/shared/Types';
import { IColorSet, ILink, ReadonlyColorSet } from 'browser/Types';
import { ISelectionRedrawRequestEvent as ISelectionRequestRedrawEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
import { createDecorator } from 'common/services/ServiceRegistry';
-import { AllColorIndex, IDisposable } from 'common/Types';
+import { AllColorIndex, IDisposable, IKeyboardResult } from 'common/Types';
import type { Event } from 'vs/base/common/event';
export const ICharSizeService = createDecorator
('CharSizeService');
@@ -156,3 +156,11 @@ export interface ILinkProviderService extends IDisposable {
export interface ILinkProvider {
provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void;
}
+
+export const IKeyboardService = createDecorator('KeyboardService');
+export interface IKeyboardService {
+ serviceBrand: undefined;
+ evaluateKeyDown(event: KeyboardEvent): IKeyboardResult;
+ evaluateKeyUp(event: KeyboardEvent): IKeyboardResult | undefined;
+ readonly useKitty: boolean;
+}
diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts
index 751cfcf18d..5ba2342a38 100644
--- a/src/common/InputHandler.test.ts
+++ b/src/common/InputHandler.test.ts
@@ -2429,62 +2429,111 @@ describe('InputHandler', () => {
}
});
});
-});
+ describe('InputHandler - kitty keyboard', () => {
+ let bufferService: IBufferService;
+ let coreService: ICoreService;
+ let optionsService: MockOptionsService;
+ let inputHandler: TestInputHandler;
-describe('InputHandler - async handlers', () => {
- let bufferService: IBufferService;
- let coreService: ICoreService;
- let optionsService: MockOptionsService;
- let inputHandler: TestInputHandler;
+ beforeEach(() => {
+ optionsService = new MockOptionsService({ vtExtensions: { kittyKeyboard: true } });
+ bufferService = new BufferService(optionsService);
+ bufferService.resize(80, 30);
+ coreService = new CoreService(bufferService, new MockLogService(), optionsService);
+ inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
+ });
- beforeEach(() => {
- optionsService = new MockOptionsService();
- bufferService = new BufferService(optionsService);
- bufferService.resize(80, 30);
- coreService = new CoreService(bufferService, new MockLogService(), optionsService);
- coreService.onData(data => { console.log(data); });
+ describe('stack limit', () => {
+ it('should evict oldest entry when stack exceeds 16 entries', async () => {
+ for (let i = 1; i <= 20; i++) {
+ await inputHandler.parseP(`\x1b[>${i}u`);
+ }
+ assert.strictEqual(coreService.kittyKeyboard.mainStack.length, 16);
+ assert.strictEqual(coreService.kittyKeyboard.mainStack[0], 4);
+ });
+ });
- inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
- });
+ describe('buffer switch', () => {
+ it('should maintain separate flags for main and alt screens', async () => {
+ await inputHandler.parseP('\x1b[>5u');
+ assert.strictEqual(coreService.kittyKeyboard.flags, 5);
+ await inputHandler.parseP('\x1b[?1049h');
+ assert.strictEqual(coreService.kittyKeyboard.flags, 0);
+ assert.strictEqual(coreService.kittyKeyboard.mainFlags, 5);
+ await inputHandler.parseP('\x1b[>7u');
+ assert.strictEqual(coreService.kittyKeyboard.flags, 7);
+ await inputHandler.parseP('\x1b[?1049l');
+ assert.strictEqual(coreService.kittyKeyboard.flags, 5);
+ assert.strictEqual(coreService.kittyKeyboard.altFlags, 7);
+ });
+ });
- it('async CUP with CPR check', async () => {
- const cup: number[][] = [];
- const cpr: number[][] = [];
- inputHandler.registerCsiHandler({ final: 'H' }, async params => {
- cup.push(params.toArray() as number[]);
- await new Promise(res => setTimeout(res, 50));
- // late call of real repositioning
- return inputHandler.cursorPosition(params);
- });
- coreService.onData(data => {
- const m = data.match(/\x1b\[(.*?);(.*?)R/);
- if (m) {
- cpr.push([parseInt(m[1]), parseInt(m[2])]);
- }
+ describe('pop reset', () => {
+ it('should reset flags to 0 when stack is emptied', async () => {
+ await inputHandler.parseP('\x1b[>5u');
+ assert.strictEqual(coreService.kittyKeyboard.flags, 5);
+ await inputHandler.parseP('\x1b[<10u');
+ assert.strictEqual(coreService.kittyKeyboard.flags, 0);
+ });
});
- await inputHandler.parseP('aaa\x1b[3;4H\x1b[6nbbb\x1b[6;8H\x1b[6n');
- assert.deepEqual(cup, cpr);
});
- it('async OSC between', async () => {
- inputHandler.registerOscHandler(1000, async data => {
- await new Promise(res => setTimeout(res, 50));
- assert.deepEqual(getLines(bufferService, 2), ['hello world!', '']);
- assert.equal(data, 'some data');
- return true;
- });
- await inputHandler.parseP('hello world!\r\n\x1b]1000;some data\x07second line');
- assert.deepEqual(getLines(bufferService, 2), ['hello world!', 'second line']);
- });
- it('async DCS between', async () => {
- inputHandler.registerDcsHandler({ final: 'a' }, async (data, params) => {
- await new Promise(res => setTimeout(res, 50));
- assert.deepEqual(getLines(bufferService, 2), ['hello world!', '']);
- assert.equal(data, 'some data');
- assert.deepEqual(params.toArray(), [1, 2]);
- return true;
- });
- await inputHandler.parseP('hello world!\r\n\x1bP1;2asome data\x1b\\second line');
- assert.deepEqual(getLines(bufferService, 2), ['hello world!', 'second line']);
+
+
+ describe('InputHandler - async handlers', () => {
+ let bufferService: IBufferService;
+ let coreService: ICoreService;
+ let optionsService: MockOptionsService;
+ let inputHandler: TestInputHandler;
+
+ beforeEach(() => {
+ optionsService = new MockOptionsService();
+ bufferService = new BufferService(optionsService);
+ bufferService.resize(80, 30);
+ coreService = new CoreService(bufferService, new MockLogService(), optionsService);
+ coreService.onData(data => { console.log(data); });
+
+ inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
+ });
+
+ it('async CUP with CPR check', async () => {
+ const cup: number[][] = [];
+ const cpr: number[][] = [];
+ inputHandler.registerCsiHandler({ final: 'H' }, async params => {
+ cup.push(params.toArray() as number[]);
+ await new Promise(res => setTimeout(res, 50));
+ // late call of real repositioning
+ return inputHandler.cursorPosition(params);
+ });
+ coreService.onData(data => {
+ const m = data.match(/\x1b\[(.*?);(.*?)R/);
+ if (m) {
+ cpr.push([parseInt(m[1]), parseInt(m[2])]);
+ }
+ });
+ await inputHandler.parseP('aaa\x1b[3;4H\x1b[6nbbb\x1b[6;8H\x1b[6n');
+ assert.deepEqual(cup, cpr);
+ });
+ it('async OSC between', async () => {
+ inputHandler.registerOscHandler(1000, async data => {
+ await new Promise(res => setTimeout(res, 50));
+ assert.deepEqual(getLines(bufferService, 2), ['hello world!', '']);
+ assert.equal(data, 'some data');
+ return true;
+ });
+ await inputHandler.parseP('hello world!\r\n\x1b]1000;some data\x07second line');
+ assert.deepEqual(getLines(bufferService, 2), ['hello world!', 'second line']);
+ });
+ it('async DCS between', async () => {
+ inputHandler.registerDcsHandler({ final: 'a' }, async (data, params) => {
+ await new Promise(res => setTimeout(res, 50));
+ assert.deepEqual(getLines(bufferService, 2), ['hello world!', '']);
+ assert.equal(data, 'some data');
+ assert.deepEqual(params.toArray(), [1, 2]);
+ return true;
+ });
+ await inputHandler.parseP('hello world!\r\n\x1bP1;2asome data\x1b\\second line');
+ assert.deepEqual(getLines(bufferService, 2), ['hello world!', 'second line']);
+ });
});
});
diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts
index 3076ed8a9e..b2b75143e9 100644
--- a/src/common/InputHandler.ts
+++ b/src/common/InputHandler.ts
@@ -270,6 +270,12 @@ export class InputHandler extends Disposable implements IInputHandler {
this._parser.registerCsiHandler({ intermediates: '$', final: 'p' }, params => this.requestMode(params, true));
this._parser.registerCsiHandler({ prefix: '?', intermediates: '$', final: 'p' }, params => this.requestMode(params, false));
+ // Kitty keyboard protocol handlers
+ this._parser.registerCsiHandler({ prefix: '=', final: 'u' }, params => this.kittyKeyboardSet(params));
+ this._parser.registerCsiHandler({ prefix: '?', final: 'u' }, params => this.kittyKeyboardQuery(params));
+ this._parser.registerCsiHandler({ prefix: '>', final: 'u' }, params => this.kittyKeyboardPush(params));
+ this._parser.registerCsiHandler({ prefix: '<', final: 'u' }, params => this.kittyKeyboardPop(params));
+
/**
* execute handler
*/
@@ -2003,6 +2009,12 @@ export class InputHandler extends Disposable implements IInputHandler {
// FALL-THROUGH
case 47: // alt screen buffer
case 1047: // alt screen buffer
+ // Swap kitty keyboard flags: save main, restore alt
+ if (this._optionsService.rawOptions.vtExtensions?.kittyKeyboard) {
+ const state = this._coreService.kittyKeyboard;
+ state.mainFlags = state.flags;
+ state.flags = state.altFlags;
+ }
this._bufferService.buffers.activateAltBuffer(this._eraseAttrData());
this._coreService.isCursorInitialized = true;
this._onRequestRefreshRows.fire(undefined);
@@ -2014,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;
@@ -2232,6 +2249,12 @@ export class InputHandler extends Disposable implements IInputHandler {
// FALL-THROUGH
case 47: // normal screen buffer
case 1047: // normal screen buffer - clearing it first
+ // Swap kitty keyboard flags: save alt, restore main
+ if (this._optionsService.rawOptions.vtExtensions?.kittyKeyboard) {
+ const state = this._coreService.kittyKeyboard;
+ state.altFlags = state.flags;
+ state.flags = state.mainFlags;
+ }
// Ensure the selection manager has the correct buffer
this._bufferService.buffers.activateNormalBuffer();
if (params.params[i] === 1049) {
@@ -2248,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;
@@ -2343,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);
}
@@ -2654,10 +2683,10 @@ export class InputHandler extends Disposable implements IInputHandler {
} else if (p === 55) {
// not overline
attr.bg &= ~BgFlags.OVERLINE;
- } else if (p === 221) {
+ } else if (p === 221 && (this._optionsService.rawOptions.vtExtensions?.kittySgrBoldFaintControl ?? true)) {
// not bold (kitty extension)
attr.fg &= ~FgFlags.BOLD;
- } else if (p === 222) {
+ } else if (p === 222 && (this._optionsService.rawOptions.vtExtensions?.kittySgrBoldFaintControl ?? true)) {
// not faint (kitty extension)
attr.bg &= ~BgFlags.DIM;
} else if (p === 59) {
@@ -2984,7 +3013,6 @@ export class InputHandler extends Disposable implements IInputHandler {
return true;
}
-
/**
* OSC 2; ST (set window title)
* Proxy to set window title.
@@ -3496,6 +3524,107 @@ export class InputHandler extends Disposable implements IInputHandler {
public markRangeDirty(y1: number, y2: number): void {
this._dirtyRowTracker.markRangeDirty(y1, y2);
}
+
+ // #region Kitty keyboard
+
+ /**
+ * CSI = flags ; mode u
+ * Set Kitty keyboard protocol flags.
+ * mode: 1=set, 2=set-only-specified, 3=reset-only-specified
+ *
+ * @vt: #Y CSI KKBDSET "Kitty Keyboard Set" "CSI = Ps ; Pm u" "Set Kitty keyboard protocol flags."
+ */
+ public kittyKeyboardSet(params: IParams): boolean {
+ if (!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard) {
+ return true;
+ }
+ const flags = params.params[0] || 0;
+ const mode = params.params[1] || 1;
+ const state = this._coreService.kittyKeyboard;
+
+ switch (mode) {
+ case 1: // Set all flags
+ state.flags = flags;
+ break;
+ case 2: // Set only specified flags (OR)
+ state.flags |= flags;
+ break;
+ case 3: // Reset only specified flags (AND NOT)
+ state.flags &= ~flags;
+ break;
+ }
+ return true;
+ }
+
+ /**
+ * CSI ? u
+ * Query Kitty keyboard protocol flags.
+ * Terminal responds with CSI ? flags u
+ *
+ * @vt: #Y CSI KKBDQUERY "Kitty Keyboard Query" "CSI ? u" "Query Kitty keyboard protocol flags."
+ */
+ public kittyKeyboardQuery(params: IParams): boolean {
+ if (!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard) {
+ return true;
+ }
+ const flags = this._coreService.kittyKeyboard.flags;
+ this._coreService.triggerDataEvent(`${C0.ESC}[?${flags}u`);
+ return true;
+ }
+
+ /**
+ * CSI > flags u
+ * Push Kitty keyboard flags onto stack and set new flags.
+ *
+ * @vt: #Y CSI KKBDPUSH "Kitty Keyboard Push" "CSI > Ps u" "Push keyboard flags to stack and set new flags."
+ */
+ public kittyKeyboardPush(params: IParams): boolean {
+ if (!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard) {
+ return true;
+ }
+ const flags = params.params[0] || 0;
+ const state = this._coreService.kittyKeyboard;
+ const isAlt = this._bufferService.buffer === this._bufferService.buffers.alt;
+ const stack = isAlt ? state.altStack : state.mainStack;
+
+ // Evict oldest entry if stack is full (DoS protection, limit of 16)
+ if (stack.length >= 16) {
+ stack.shift();
+ }
+
+ // Push current flags onto stack and set new flags
+ stack.push(state.flags);
+ state.flags = flags;
+ return true;
+ }
+
+ /**
+ * CSI < count u
+ * Pop Kitty keyboard flags from stack.
+ *
+ * @vt: #Y CSI KKBDPOP "Kitty Keyboard Pop" "CSI < Ps u" "Pop keyboard flags from stack."
+ */
+ public kittyKeyboardPop(params: IParams): boolean {
+ if (!this._optionsService.rawOptions.vtExtensions?.kittyKeyboard) {
+ return true;
+ }
+ const count = Math.max(1, params.params[0] || 1);
+ const state = this._coreService.kittyKeyboard;
+ const isAlt = this._bufferService.buffer === this._bufferService.buffers.alt;
+ const stack = isAlt ? state.altStack : state.mainStack;
+
+ // Pop specified number of entries from stack
+ for (let i = 0; i < count && stack.length > 0; i++) {
+ state.flags = stack.pop()!;
+ }
+ // If stack is empty after popping, reset to 0
+ if (stack.length === 0 && count > 0) {
+ state.flags = 0;
+ }
+ return true;
+ }
+
+ // #endregion
}
export interface IDirtyRowTracker {
diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts
index ef9140e499..2d44c32efe 100644
--- a/src/common/TestUtils.test.ts
+++ b/src/common/TestUtils.test.ts
@@ -112,8 +112,16 @@ export class MockCoreService implements ICoreService {
reverseWraparound: false,
sendFocus: false,
synchronizedOutput: false,
+ win32InputMode: false,
wraparound: true
};
+ public kittyKeyboard = {
+ flags: 0,
+ mainFlags: 0,
+ altFlags: 0,
+ mainStack: [] as number[],
+ altStack: [] as number[]
+ };
public onData: Event = new Emitter().event;
public onUserInput: Event = new Emitter().event;
public onBinary: Event = new Emitter().event;
diff --git a/src/common/Types.ts b/src/common/Types.ts
index 0ccb1483f9..269b30c85e 100644
--- a/src/common/Types.ts
+++ b/src/common/Types.ts
@@ -274,9 +274,27 @@ export interface IDecPrivateModes {
reverseWraparound: boolean;
sendFocus: boolean;
synchronizedOutput: boolean;
+ win32InputMode: boolean;
wraparound: boolean; // defaults: xterm - true, vt100 - false
}
+/**
+ * Kitty keyboard protocol state.
+ * Maintains per-screen stacks of enhancement flags.
+ */
+export interface IKittyKeyboardState {
+ /** Current active enhancement flags (for current screen) */
+ flags: number;
+ /** Saved flags for main screen when alt is active */
+ mainFlags: number;
+ /** Saved flags for alternate screen when main is active */
+ altFlags: number;
+ /** Stack of flags for main screen */
+ mainStack: number[];
+ /** Stack of flags for alternate screen */
+ altStack: number[];
+}
+
export interface IRowRange {
start: number;
end: number;
diff --git a/src/common/input/KittyKeyboard.test.ts b/src/common/input/KittyKeyboard.test.ts
new file mode 100644
index 0000000000..3e068f4674
--- /dev/null
+++ b/src/common/input/KittyKeyboard.test.ts
@@ -0,0 +1,676 @@
+
+import { assert } from 'chai';
+import { evaluateKeyboardEventKitty, KittyKeyboardEventType, KittyKeyboardFlags, shouldUseKittyProtocol } from 'common/input/KittyKeyboard';
+import { IKeyboardResult, IKeyboardEvent } 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'
+ };
+}
+
+describe('KittyKeyboard', () => {
+ describe('shouldUseKittyProtocol', () => {
+ it('should return false when flags are 0', () => {
+ assert.strictEqual(shouldUseKittyProtocol(0), false);
+ });
+
+ it('should return true when any flag is set', () => {
+ assert.strictEqual(shouldUseKittyProtocol(KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES), true);
+ assert.strictEqual(shouldUseKittyProtocol(KittyKeyboardFlags.REPORT_EVENT_TYPES), true);
+ assert.strictEqual(shouldUseKittyProtocol(0b11111), true);
+ });
+ });
+
+ describe('evaluateKeyboardEventKitty', () => {
+ describe('modifier encoding (value = 1 + modifiers)', () => {
+ const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES;
+
+ it('shift=2 (1+1)', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', shiftKey: true }), flags);
+ assert.strictEqual(result.key, '\x1b[97;2u');
+ });
+
+ it('alt=3 (1+2)', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', altKey: true }), flags);
+ assert.strictEqual(result.key, '\x1b[97;3u');
+ });
+
+ it('ctrl=5 (1+4)', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', ctrlKey: true }), flags);
+ assert.strictEqual(result.key, '\x1b[97;5u');
+ });
+
+ it('super/meta=9 (1+8)', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', metaKey: true }), flags);
+ assert.strictEqual(result.key, '\x1b[97;9u');
+ });
+
+ it('ctrl+shift=6 (1+4+1)', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', ctrlKey: true, shiftKey: true }), flags);
+ assert.strictEqual(result.key, '\x1b[97;6u');
+ });
+
+ it('ctrl+alt=7 (1+4+2)', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', ctrlKey: true, altKey: true }), flags);
+ assert.strictEqual(result.key, '\x1b[97;7u');
+ });
+
+ it('ctrl+alt+shift=8 (1+4+2+1)', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', ctrlKey: true, altKey: true, shiftKey: true }), flags);
+ assert.strictEqual(result.key, '\x1b[97;8u');
+ });
+
+ it('ctrl+super=13 (1+4+8)', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', ctrlKey: true, metaKey: true }), flags);
+ assert.strictEqual(result.key, '\x1b[97;13u');
+ });
+
+ it('all four modifiers=16 (1+1+2+4+8)', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', shiftKey: true, altKey: true, ctrlKey: true, metaKey: true }), flags);
+ assert.strictEqual(result.key, '\x1b[97;16u');
+ });
+
+ it('no modifiers omits modifier field', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'Escape' }), flags);
+ assert.strictEqual(result.key, '\x1b[27u');
+ });
+ });
+
+ describe('C0 control keys with DISAMBIGUATE_ESCAPE_CODES', () => {
+ const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES;
+
+ it('Escape → CSI 27 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'Escape' }), flags);
+ assert.strictEqual(result.key, '\x1b[27u');
+ });
+
+ it('Enter → CSI 13 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'Enter' }), flags);
+ assert.strictEqual(result.key, '\x1b[13u');
+ });
+
+ it('Tab → CSI 9 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'Tab' }), flags);
+ assert.strictEqual(result.key, '\x1b[9u');
+ });
+
+ it('Backspace → CSI 127 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'Backspace' }), flags);
+ assert.strictEqual(result.key, '\x1b[127u');
+ });
+
+ it('Space → CSI 32 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: ' ' }), flags);
+ assert.strictEqual(result.key, '\x1b[32u');
+ });
+
+ it('Shift+Tab → CSI 9;2 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'Tab', shiftKey: true }), flags);
+ assert.strictEqual(result.key, '\x1b[9;2u');
+ });
+
+ it('Ctrl+Enter → CSI 13;5 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'Enter', ctrlKey: true }), flags);
+ assert.strictEqual(result.key, '\x1b[13;5u');
+ });
+
+ it('Alt+Escape → CSI 27;3 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'Escape', altKey: true }), flags);
+ assert.strictEqual(result.key, '\x1b[27;3u');
+ });
+ });
+
+ describe('navigation keys', () => {
+ const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES;
+
+ it('Insert → CSI 2 ~', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'Insert' }), flags);
+ assert.strictEqual(result.key, '\x1b[2~');
+ });
+
+ it('Delete → CSI 3 ~', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'Delete' }), flags);
+ assert.strictEqual(result.key, '\x1b[3~');
+ });
+
+ it('PageUp → CSI 5 ~', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'PageUp' }), flags);
+ assert.strictEqual(result.key, '\x1b[5~');
+ });
+
+ it('PageDown → CSI 6 ~', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'PageDown' }), flags);
+ assert.strictEqual(result.key, '\x1b[6~');
+ });
+
+ it('Home → CSI H', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'Home' }), flags);
+ assert.strictEqual(result.key, '\x1b[H');
+ });
+
+ it('End → CSI F', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'End' }), flags);
+ assert.strictEqual(result.key, '\x1b[F');
+ });
+
+ it('Shift+PageUp → CSI 5;2 ~', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'PageUp', shiftKey: true }), flags);
+ assert.strictEqual(result.key, '\x1b[5;2~');
+ });
+
+ it('Ctrl+Home → CSI 1;5 H', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'Home', ctrlKey: true }), flags);
+ assert.strictEqual(result.key, '\x1b[1;5H');
+ });
+ });
+
+ describe('arrow keys', () => {
+ const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES;
+
+ it('ArrowUp → CSI A', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'ArrowUp' }), flags);
+ assert.strictEqual(result.key, '\x1b[A');
+ });
+
+ it('ArrowDown → CSI B', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'ArrowDown' }), flags);
+ assert.strictEqual(result.key, '\x1b[B');
+ });
+
+ it('ArrowRight → CSI C', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'ArrowRight' }), flags);
+ assert.strictEqual(result.key, '\x1b[C');
+ });
+
+ it('ArrowLeft → CSI D', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'ArrowLeft' }), flags);
+ assert.strictEqual(result.key, '\x1b[D');
+ });
+
+ it('Shift+ArrowUp → CSI 1;2 A', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'ArrowUp', shiftKey: true }), flags);
+ assert.strictEqual(result.key, '\x1b[1;2A');
+ });
+
+ it('Ctrl+ArrowLeft → CSI 1;5 D', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'ArrowLeft', ctrlKey: true }), flags);
+ assert.strictEqual(result.key, '\x1b[1;5D');
+ });
+
+ it('Ctrl+Shift+ArrowRight → CSI 1;6 C', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'ArrowRight', ctrlKey: true, shiftKey: true }), flags);
+ assert.strictEqual(result.key, '\x1b[1;6C');
+ });
+ });
+
+ describe('function keys F1-F12', () => {
+ const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES;
+
+ it('F1 → CSI P (SS3 form)', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'F1' }), flags);
+ assert.strictEqual(result.key, '\x1bOP');
+ });
+
+ it('F2 → CSI Q (SS3 form)', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'F2' }), flags);
+ assert.strictEqual(result.key, '\x1bOQ');
+ });
+
+ it('F3 → CSI R (SS3 form)', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'F3' }), flags);
+ assert.strictEqual(result.key, '\x1bOR');
+ });
+
+ it('F4 → CSI S (SS3 form)', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'F4' }), flags);
+ assert.strictEqual(result.key, '\x1bOS');
+ });
+
+ it('F5 → CSI 15 ~', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'F5' }), flags);
+ assert.strictEqual(result.key, '\x1b[15~');
+ });
+
+ it('F6 → CSI 17 ~', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'F6' }), flags);
+ assert.strictEqual(result.key, '\x1b[17~');
+ });
+
+ it('F7 → CSI 18 ~', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'F7' }), flags);
+ assert.strictEqual(result.key, '\x1b[18~');
+ });
+
+ it('F8 → CSI 19 ~', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'F8' }), flags);
+ assert.strictEqual(result.key, '\x1b[19~');
+ });
+
+ it('F9 → CSI 20 ~', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'F9' }), flags);
+ assert.strictEqual(result.key, '\x1b[20~');
+ });
+
+ it('F10 → CSI 21 ~', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'F10' }), flags);
+ assert.strictEqual(result.key, '\x1b[21~');
+ });
+
+ it('F11 → CSI 23 ~', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'F11' }), flags);
+ assert.strictEqual(result.key, '\x1b[23~');
+ });
+
+ it('F12 → CSI 24 ~', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'F12' }), flags);
+ assert.strictEqual(result.key, '\x1b[24~');
+ });
+
+ it('Shift+F1 → CSI 1;2 P', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'F1', shiftKey: true }), flags);
+ assert.strictEqual(result.key, '\x1b[1;2P');
+ });
+
+ it('Ctrl+F5 → CSI 15;5 ~', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'F5', ctrlKey: true }), flags);
+ assert.strictEqual(result.key, '\x1b[15;5~');
+ });
+ });
+
+ describe('extended function keys F13-F35 (Private Use Area)', () => {
+ const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES;
+
+ it('F13 → CSI 57376 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'F13' }), flags);
+ assert.strictEqual(result.key, '\x1b[57376u');
+ });
+
+ it('F14 → CSI 57377 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'F14' }), flags);
+ assert.strictEqual(result.key, '\x1b[57377u');
+ });
+
+ it('F20 → CSI 57383 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'F20' }), flags);
+ assert.strictEqual(result.key, '\x1b[57383u');
+ });
+
+ it('F24 → CSI 57387 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'F24' }), flags);
+ assert.strictEqual(result.key, '\x1b[57387u');
+ });
+ });
+
+ describe('numpad keys (Private Use Area)', () => {
+ const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES;
+
+ it('Numpad0 → CSI 57399 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: '0', code: 'Numpad0' }), flags);
+ assert.strictEqual(result.key, '\x1b[57399u');
+ });
+
+ it('Numpad1 → CSI 57400 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: '1', code: 'Numpad1' }), flags);
+ assert.strictEqual(result.key, '\x1b[57400u');
+ });
+
+ it('Numpad9 → CSI 57408 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: '9', code: 'Numpad9' }), flags);
+ assert.strictEqual(result.key, '\x1b[57408u');
+ });
+
+ it('NumpadDecimal → CSI 57409 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: '.', code: 'NumpadDecimal' }), flags);
+ assert.strictEqual(result.key, '\x1b[57409u');
+ });
+
+ it('NumpadDivide → CSI 57410 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: '/', code: 'NumpadDivide' }), flags);
+ assert.strictEqual(result.key, '\x1b[57410u');
+ });
+
+ it('NumpadMultiply → CSI 57411 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: '*', code: 'NumpadMultiply' }), flags);
+ assert.strictEqual(result.key, '\x1b[57411u');
+ });
+
+ it('NumpadSubtract → CSI 57412 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: '-', code: 'NumpadSubtract' }), flags);
+ assert.strictEqual(result.key, '\x1b[57412u');
+ });
+
+ it('NumpadAdd → CSI 57413 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: '+', code: 'NumpadAdd' }), flags);
+ assert.strictEqual(result.key, '\x1b[57413u');
+ });
+
+ it('NumpadEnter → CSI 57414 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'Enter', code: 'NumpadEnter' }), flags);
+ assert.strictEqual(result.key, '\x1b[57414u');
+ });
+
+ it('NumpadEqual → CSI 57415 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: '=', code: 'NumpadEqual' }), flags);
+ assert.strictEqual(result.key, '\x1b[57415u');
+ });
+
+ it('Ctrl+Numpad5 → CSI 57404;5 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: '5', code: 'Numpad5', ctrlKey: true }), flags);
+ assert.strictEqual(result.key, '\x1b[57404;5u');
+ });
+ });
+
+ describe('modifier keys (Private Use Area)', () => {
+ const flags = KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES;
+
+ it('Left Shift → CSI 57441 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'Shift', code: 'ShiftLeft', shiftKey: true }), flags);
+ assert.strictEqual(result.key, '\x1b[57441;2u');
+ });
+
+ it('Right Shift → CSI 57447 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'Shift', code: 'ShiftRight', shiftKey: true }), flags);
+ assert.strictEqual(result.key, '\x1b[57447;2u');
+ });
+
+ it('Left Control → CSI 57442 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'Control', code: 'ControlLeft', ctrlKey: true }), flags);
+ assert.strictEqual(result.key, '\x1b[57442;5u');
+ });
+
+ it('Right Control → CSI 57448 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'Control', code: 'ControlRight', ctrlKey: true }), flags);
+ assert.strictEqual(result.key, '\x1b[57448;5u');
+ });
+
+ it('Left Alt → CSI 57443 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'Alt', code: 'AltLeft', altKey: true }), flags);
+ assert.strictEqual(result.key, '\x1b[57443;3u');
+ });
+
+ it('Right Alt → CSI 57449 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'Alt', code: 'AltRight', altKey: true }), flags);
+ assert.strictEqual(result.key, '\x1b[57449;3u');
+ });
+
+ it('Left Meta/Super → CSI 57444 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'Meta', code: 'MetaLeft', metaKey: true }), flags);
+ assert.strictEqual(result.key, '\x1b[57444;9u');
+ });
+
+ it('Right Meta/Super → CSI 57450 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'Meta', code: 'MetaRight', metaKey: true }), flags);
+ assert.strictEqual(result.key, '\x1b[57450;9u');
+ });
+
+ it('CapsLock → CSI 57358 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'CapsLock', code: 'CapsLock' }), flags);
+ assert.strictEqual(result.key, '\x1b[57358u');
+ });
+
+ it('NumLock → CSI 57360 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'NumLock', code: 'NumLock' }), flags);
+ assert.strictEqual(result.key, '\x1b[57360u');
+ });
+
+ it('ScrollLock → CSI 57359 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'ScrollLock', code: 'ScrollLock' }), flags);
+ assert.strictEqual(result.key, '\x1b[57359u');
+ });
+ });
+
+ describe('event types (press/repeat/release)', () => {
+ const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES | KittyKeyboardFlags.REPORT_EVENT_TYPES;
+
+ it('press event (default, no suffix)', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'a' }), flags, KittyKeyboardEventType.PRESS);
+ assert.strictEqual(result.key, '\x1b[97u');
+ });
+
+ it('press event explicit :1 when modifiers present', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', ctrlKey: true }), flags, KittyKeyboardEventType.PRESS);
+ assert.strictEqual(result.key, '\x1b[97;5u');
+ });
+
+ it('repeat event → :2 suffix', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'a' }), flags, KittyKeyboardEventType.REPEAT);
+ assert.strictEqual(result.key, '\x1b[97;1:2u');
+ });
+
+ it('release event → :3 suffix', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'a' }), flags, KittyKeyboardEventType.RELEASE);
+ assert.strictEqual(result.key, '\x1b[97;1:3u');
+ });
+
+ it('release with modifier → mod:3', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', ctrlKey: true }), flags, KittyKeyboardEventType.RELEASE);
+ assert.strictEqual(result.key, '\x1b[97;5:3u');
+ });
+
+ it('repeat with modifier → mod:2', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', shiftKey: true, altKey: true }), flags, KittyKeyboardEventType.REPEAT);
+ assert.strictEqual(result.key, '\x1b[97;4:2u');
+ });
+
+ it('functional key release → CSI code;1:3 ~', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'Delete' }), flags, KittyKeyboardEventType.RELEASE);
+ assert.strictEqual(result.key, '\x1b[3;1:3~');
+ });
+
+ it('modifier key release includes its own bit cleared', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'Shift', code: 'ShiftLeft', shiftKey: false }), flags, KittyKeyboardEventType.RELEASE);
+ assert.strictEqual(result.key, '\x1b[57441;1:3u');
+ });
+ });
+
+ describe('REPORT_ALL_KEYS_AS_ESCAPE_CODES flag', () => {
+ const flags = KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES;
+
+ it('lowercase letter → CSI codepoint u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'a' }), flags);
+ assert.strictEqual(result.key, '\x1b[97u');
+ });
+
+ it('uppercase letter uses lowercase codepoint', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'A', shiftKey: true }), flags);
+ assert.strictEqual(result.key, '\x1b[97;2u');
+ });
+
+ it('digit → CSI codepoint u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: '5' }), flags);
+ assert.strictEqual(result.key, '\x1b[53u');
+ });
+
+ it('punctuation → CSI codepoint u', () => {
+ assert.strictEqual(evaluateKeyboardEventKitty(createEvent({ key: '.' }), flags).key, '\x1b[46u');
+ assert.strictEqual(evaluateKeyboardEventKitty(createEvent({ key: ',' }), flags).key, '\x1b[44u');
+ assert.strictEqual(evaluateKeyboardEventKitty(createEvent({ key: ';' }), flags).key, '\x1b[59u');
+ assert.strictEqual(evaluateKeyboardEventKitty(createEvent({ key: '/' }), flags).key, '\x1b[47u');
+ });
+
+ it('brackets → CSI codepoint u', () => {
+ assert.strictEqual(evaluateKeyboardEventKitty(createEvent({ key: '[' }), flags).key, '\x1b[91u');
+ assert.strictEqual(evaluateKeyboardEventKitty(createEvent({ key: ']' }), flags).key, '\x1b[93u');
+ });
+
+ it('space → CSI 32 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: ' ' }), flags);
+ assert.strictEqual(result.key, '\x1b[32u');
+ });
+ });
+
+ describe('REPORT_ASSOCIATED_TEXT flag', () => {
+ const flags = KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES | KittyKeyboardFlags.REPORT_ASSOCIATED_TEXT;
+
+ it('regular key includes text codepoint', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'a' }), flags);
+ assert.strictEqual(result.key, '\x1b[97;;97u');
+ });
+
+ it('shifted key includes shifted text', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'A', shiftKey: true }), flags);
+ assert.strictEqual(result.key, '\x1b[97;2;65u');
+ });
+
+ it('Ctrl+key omits text (control code)', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', ctrlKey: true }), flags);
+ assert.strictEqual(result.key, '\x1b[97;5u');
+ });
+
+ it('functional key has no text', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'Escape' }), flags);
+ assert.strictEqual(result.key, '\x1b[27u');
+ });
+
+ it('release event has no text', () => {
+ const flagsWithEvents = flags | KittyKeyboardFlags.REPORT_EVENT_TYPES;
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'a' }), flagsWithEvents, KittyKeyboardEventType.RELEASE);
+ assert.strictEqual(result.key, '\x1b[97;1:3u');
+ });
+
+ it('digit with text', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: '5' }), flags);
+ assert.strictEqual(result.key, '\x1b[53;;53u');
+ });
+
+ it('Shift+digit shows shifted symbol', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: '%', shiftKey: true, code: 'Digit5' }), flags);
+ assert.strictEqual(result.key, '\x1b[53;2;37u');
+ });
+ });
+
+ describe('REPORT_ALTERNATE_KEYS flag', () => {
+ const flags = KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES | KittyKeyboardFlags.REPORT_ALTERNATE_KEYS;
+
+ it('Shift+a includes shifted key → CSI 97:65 ; 2 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'A', shiftKey: true, code: 'KeyA' }), flags);
+ assert.strictEqual(result.key, '\x1b[97:65;2u');
+ });
+
+ it('unshifted key has no alternate', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', code: 'KeyA' }), flags);
+ assert.strictEqual(result.key, '\x1b[97u');
+ });
+
+ it('Shift+5 includes shifted key → CSI 53:37 ; 2 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: '%', shiftKey: true, code: 'Digit5' }), flags);
+ assert.strictEqual(result.key, '\x1b[53:37;2u');
+ });
+
+ it('functional keys have no shifted alternate', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'Escape', shiftKey: true }), flags);
+ assert.strictEqual(result.key, '\x1b[27;2u');
+ });
+ });
+
+ describe('REPORT_ALTERNATE_KEYS with REPORT_ASSOCIATED_TEXT', () => {
+ const flags = KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES | KittyKeyboardFlags.REPORT_ALTERNATE_KEYS | KittyKeyboardFlags.REPORT_ASSOCIATED_TEXT;
+
+ it('Shift+a → CSI 97:65 ; 2 ; 65 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'A', shiftKey: true, code: 'KeyA' }), flags);
+ assert.strictEqual(result.key, '\x1b[97:65;2;65u');
+ });
+
+ it('Shift+a release → CSI 97:65 ; 2:3 u (no text)', () => {
+ const flagsWithEvents = flags | KittyKeyboardFlags.REPORT_EVENT_TYPES;
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'A', shiftKey: true, code: 'KeyA' }), flagsWithEvents, KittyKeyboardEventType.RELEASE);
+ assert.strictEqual(result.key, '\x1b[97:65;2:3u');
+ });
+ });
+
+ describe('release events without REPORT_EVENT_TYPES', () => {
+ const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES;
+
+ it('should not generate key sequence for release events', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'a' }), flags, KittyKeyboardEventType.RELEASE);
+ assert.strictEqual(result.key, undefined);
+ });
+ });
+
+ describe('edge cases', () => {
+ const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES;
+
+ it('always uses lowercase codepoint for letters', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'A', shiftKey: true }), flags);
+ assert.strictEqual(result.key, '\x1b[97;2u');
+ });
+
+ it('ctrl+shift+a sends lowercase codepoint 97', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'A', ctrlKey: true, shiftKey: true }), flags);
+ assert.strictEqual(result.key, '\x1b[97;6u');
+ });
+
+ it('Dead key produces no output', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'Dead' }), flags);
+ assert.strictEqual(result.key, undefined);
+ });
+
+ it('Unidentified key produces no output', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'Unidentified' }), flags);
+ assert.strictEqual(result.key, undefined);
+ });
+
+ it('PrintScreen → CSI 57361 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'PrintScreen' }), flags);
+ assert.strictEqual(result.key, '\x1b[57361u');
+ });
+
+ it('Pause → CSI 57362 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'Pause' }), flags);
+ assert.strictEqual(result.key, '\x1b[57362u');
+ });
+
+ it('ContextMenu → CSI 57363 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'ContextMenu' }), flags);
+ assert.strictEqual(result.key, '\x1b[57363u');
+ });
+ });
+
+ describe('media keys (Private Use Area)', () => {
+ const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES;
+
+ it('MediaPlayPause → CSI 57430 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'MediaPlayPause' }), flags);
+ assert.strictEqual(result.key, '\x1b[57430u');
+ });
+
+ it('MediaStop → CSI 57432 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'MediaStop' }), flags);
+ assert.strictEqual(result.key, '\x1b[57432u');
+ });
+
+ it('MediaTrackNext → CSI 57435 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'MediaTrackNext' }), flags);
+ assert.strictEqual(result.key, '\x1b[57435u');
+ });
+
+ it('MediaTrackPrevious → CSI 57436 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'MediaTrackPrevious' }), flags);
+ assert.strictEqual(result.key, '\x1b[57436u');
+ });
+
+ it('AudioVolumeDown → CSI 57438 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'AudioVolumeDown' }), flags);
+ assert.strictEqual(result.key, '\x1b[57438u');
+ });
+
+ it('AudioVolumeUp → CSI 57439 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'AudioVolumeUp' }), flags);
+ assert.strictEqual(result.key, '\x1b[57439u');
+ });
+
+ it('AudioVolumeMute → CSI 57440 u', () => {
+ const result = evaluateKeyboardEventKitty(createEvent({ key: 'AudioVolumeMute' }), flags);
+ assert.strictEqual(result.key, '\x1b[57440u');
+ });
+ });
+ });
+});
diff --git a/src/common/input/KittyKeyboard.ts b/src/common/input/KittyKeyboard.ts
new file mode 100644
index 0000000000..0dfd0b5649
--- /dev/null
+++ b/src/common/input/KittyKeyboard.ts
@@ -0,0 +1,513 @@
+/**
+ * Copyright (c) 2025 The xterm.js authors. All rights reserved.
+ * @license MIT
+ *
+ * Kitty keyboard protocol implementation.
+ * @see https://sw.kovidgoyal.net/kitty/keyboard-protocol/
+ */
+
+import { IKeyboardEvent, IKeyboardResult, KeyboardResultType } from 'common/Types';
+import { C0 } from 'common/data/EscapeSequences';
+
+/**
+ * Kitty keyboard protocol enhancement flags (bitfield).
+ */
+export const enum KittyKeyboardFlags {
+ NONE = 0b00000,
+ /** Disambiguate escape codes - fixes ambiguous legacy encodings */
+ DISAMBIGUATE_ESCAPE_CODES = 0b00001,
+ /** Report event types - press/repeat/release */
+ REPORT_EVENT_TYPES = 0b00010,
+ /** Report alternate keys - shifted key and base layout key */
+ REPORT_ALTERNATE_KEYS = 0b00100,
+ /** Report all keys as escape codes - text-producing keys as CSI u */
+ REPORT_ALL_KEYS_AS_ESCAPE_CODES = 0b01000,
+ /** Report associated text - includes text codepoints in escape code */
+ REPORT_ASSOCIATED_TEXT = 0b10000,
+}
+
+/**
+ * Kitty keyboard event types.
+ */
+export const enum KittyKeyboardEventType {
+ PRESS = 1,
+ REPEAT = 2,
+ RELEASE = 3,
+}
+
+/**
+ * Kitty modifier bits (different from xterm modifier encoding).
+ * Value sent = 1 + modifier_bits
+ */
+export const enum KittyKeyboardModifiers {
+ SHIFT = 0b00000001,
+ ALT = 0b00000010,
+ CTRL = 0b00000100,
+ SUPER = 0b00001000,
+ HYPER = 0b00010000,
+ META = 0b00100000,
+ CAPS_LOCK = 0b01000000,
+ NUM_LOCK = 0b10000000,
+}
+
+/**
+ * Functional key codes for Kitty protocol.
+ * Keys that don't produce text have specific unicode codepoint mappings.
+ */
+const FUNCTIONAL_KEY_CODES: { [key: string]: number } = {
+ 'Escape': 27,
+ 'Enter': 13,
+ 'Tab': 9,
+ 'Backspace': 127,
+ 'CapsLock': 57358,
+ 'ScrollLock': 57359,
+ 'NumLock': 57360,
+ 'PrintScreen': 57361,
+ 'Pause': 57362,
+ 'ContextMenu': 57363,
+ // F13-F35 (F1-F12 use legacy encoding)
+ 'F13': 57376,
+ 'F14': 57377,
+ 'F15': 57378,
+ 'F16': 57379,
+ 'F17': 57380,
+ 'F18': 57381,
+ 'F19': 57382,
+ 'F20': 57383,
+ 'F21': 57384,
+ 'F22': 57385,
+ 'F23': 57386,
+ 'F24': 57387,
+ 'F25': 57388,
+ // Keypad keys
+ 'KP_0': 57399,
+ 'KP_1': 57400,
+ 'KP_2': 57401,
+ 'KP_3': 57402,
+ 'KP_4': 57403,
+ 'KP_5': 57404,
+ 'KP_6': 57405,
+ 'KP_7': 57406,
+ 'KP_8': 57407,
+ 'KP_9': 57408,
+ 'KP_Decimal': 57409,
+ 'KP_Divide': 57410,
+ 'KP_Multiply': 57411,
+ 'KP_Subtract': 57412,
+ 'KP_Add': 57413,
+ 'KP_Enter': 57414,
+ 'KP_Equal': 57415,
+ // Modifier keys
+ 'ShiftLeft': 57441,
+ 'ShiftRight': 57447,
+ 'ControlLeft': 57442,
+ 'ControlRight': 57448,
+ 'AltLeft': 57443,
+ 'AltRight': 57449,
+ 'MetaLeft': 57444,
+ 'MetaRight': 57450,
+ // Media keys
+ 'MediaPlayPause': 57430,
+ 'MediaStop': 57432,
+ 'MediaTrackNext': 57435,
+ 'MediaTrackPrevious': 57436,
+ 'AudioVolumeDown': 57438,
+ 'AudioVolumeUp': 57439,
+ 'AudioVolumeMute': 57440
+};
+
+/**
+ * Keys that use CSI ~ encoding with a number parameter.
+ */
+const CSI_TILDE_KEYS: { [key: string]: number } = {
+ 'Insert': 2,
+ 'Delete': 3,
+ 'PageUp': 5,
+ 'PageDown': 6,
+ 'F5': 15,
+ 'F6': 17,
+ 'F7': 18,
+ 'F8': 19,
+ 'F9': 20,
+ 'F10': 21,
+ 'F11': 23,
+ 'F12': 24
+};
+
+/**
+ * Keys that use CSI letter encoding (arrows, Home, End).
+ */
+const CSI_LETTER_KEYS: { [key: string]: string } = {
+ 'ArrowUp': 'A',
+ 'ArrowDown': 'B',
+ 'ArrowRight': 'C',
+ 'ArrowLeft': 'D',
+ 'Home': 'H',
+ 'End': 'F'
+};
+
+/**
+ * Function keys F1-F4 use SS3 encoding without modifiers.
+ */
+const SS3_FUNCTION_KEYS: { [key: string]: string } = {
+ 'F1': 'P',
+ 'F2': 'Q',
+ 'F3': 'R',
+ 'F4': 'S'
+};
+
+/**
+ * Map browser key codes to Kitty numpad codes.
+ */
+function getNumpadKeyCode(ev: IKeyboardEvent): number | undefined {
+ // Detect numpad via code property
+ if (ev.code.startsWith('Numpad')) {
+ const suffix = ev.code.slice(6);
+ if (suffix >= '0' && suffix <= '9') {
+ return 57399 + parseInt(suffix, 10);
+ }
+ switch (suffix) {
+ case 'Decimal': return 57409;
+ case 'Divide': return 57410;
+ case 'Multiply': return 57411;
+ case 'Subtract': return 57412;
+ case 'Add': return 57413;
+ case 'Enter': return 57414;
+ case 'Equal': return 57415;
+ }
+ }
+ return undefined;
+}
+
+/**
+ * Get modifier key code from code property.
+ */
+function getModifierKeyCode(ev: IKeyboardEvent): number | undefined {
+ switch (ev.code) {
+ case 'ShiftLeft': return 57441;
+ case 'ShiftRight': return 57447;
+ case 'ControlLeft': return 57442;
+ case 'ControlRight': return 57448;
+ case 'AltLeft': return 57443;
+ case 'AltRight': return 57449;
+ case 'MetaLeft': return 57444;
+ case 'MetaRight': return 57450;
+ }
+ return undefined;
+}
+
+/**
+ * Encode modifiers for Kitty protocol.
+ * Returns 1 + modifier bits, or 0 if no modifiers.
+ */
+function encodeModifiers(ev: IKeyboardEvent): number {
+ let mods = 0;
+ if (ev.shiftKey) mods |= KittyKeyboardModifiers.SHIFT;
+ if (ev.altKey) mods |= KittyKeyboardModifiers.ALT;
+ if (ev.ctrlKey) mods |= KittyKeyboardModifiers.CTRL;
+ if (ev.metaKey) mods |= KittyKeyboardModifiers.SUPER;
+ return mods > 0 ? mods + 1 : 0;
+}
+
+/**
+ * Get the unicode key code for a keyboard event.
+ * Returns the lowercase codepoint for letters.
+ * For shifted keys, uses the code property to get the base key.
+ */
+function getKeyCode(ev: IKeyboardEvent): number | undefined {
+ // Check for numpad first
+ const numpadCode = getNumpadKeyCode(ev);
+ if (numpadCode !== undefined) {
+ return numpadCode;
+ }
+
+ // Check for modifier keys
+ const modifierCode = getModifierKeyCode(ev);
+ if (modifierCode !== undefined) {
+ return modifierCode;
+ }
+
+ // Check functional keys
+ const funcCode = FUNCTIONAL_KEY_CODES[ev.key];
+ if (funcCode !== undefined) {
+ return funcCode;
+ }
+
+ // For shifted keys, use code property to get base key
+ if (ev.shiftKey && ev.code) {
+ // Handle Digit0-Digit9
+ if (ev.code.startsWith('Digit') && ev.code.length === 6) {
+ const digit = ev.code.charAt(5);
+ if (digit >= '0' && digit <= '9') {
+ return digit.charCodeAt(0);
+ }
+ }
+ // Handle KeyA-KeyZ
+ if (ev.code.startsWith('Key') && ev.code.length === 4) {
+ const letter = ev.code.charAt(3).toLowerCase();
+ return letter.charCodeAt(0);
+ }
+ }
+
+ // For regular keys, use the key character's codepoint
+ // Always use lowercase for letters (per spec)
+ if (ev.key.length === 1) {
+ const code = ev.key.codePointAt(0)!;
+ // Convert uppercase A-Z to lowercase a-z
+ if (code >= 65 && code <= 90) {
+ return code + 32;
+ }
+ return code;
+ }
+
+ return undefined;
+}
+
+/**
+ * Check if a key is a modifier key.
+ */
+function isModifierKey(ev: IKeyboardEvent): boolean {
+ return ev.key === 'Shift' || ev.key === 'Control' || ev.key === 'Alt' || ev.key === 'Meta';
+}
+
+/**
+ * Evaluate a keyboard event using Kitty keyboard protocol.
+ *
+ * @param ev The keyboard event.
+ * @param flags The active Kitty keyboard enhancement flags.
+ * @param eventType The event type (press, repeat, release).
+ * @returns The keyboard result with the encoded key sequence.
+ */
+export function evaluateKeyboardEventKitty(
+ ev: IKeyboardEvent,
+ flags: number,
+ eventType: KittyKeyboardEventType = KittyKeyboardEventType.PRESS
+): IKeyboardResult {
+ const result: IKeyboardResult = {
+ type: KeyboardResultType.SEND_KEY,
+ cancel: false,
+ key: undefined
+ };
+
+ const modifiers = encodeModifiers(ev);
+ const isMod = isModifierKey(ev);
+ const reportEventTypes = !!(flags & KittyKeyboardFlags.REPORT_EVENT_TYPES);
+
+ // Don't report release events unless flag is set
+ if (!reportEventTypes && eventType === KittyKeyboardEventType.RELEASE) {
+ return result;
+ }
+
+ // Modifier-only keys require REPORT_ALL_KEYS_AS_ESCAPE_CODES or REPORT_EVENT_TYPES
+ if (isMod && !(flags & KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES) && !reportEventTypes) {
+ return result;
+ }
+
+ // Check for CSI letter keys (arrows, Home, End)
+ const csiLetter = CSI_LETTER_KEYS[ev.key];
+ if (csiLetter) {
+ result.key = buildCsiLetterSequence(csiLetter, modifiers, eventType, reportEventTypes);
+ result.cancel = true;
+ return result;
+ }
+
+ // Check for SS3/CSI function keys (F1-F4)
+ const ss3Letter = SS3_FUNCTION_KEYS[ev.key];
+ if (ss3Letter) {
+ result.key = buildSs3Sequence(ss3Letter, modifiers, eventType, reportEventTypes);
+ result.cancel = true;
+ return result;
+ }
+
+ // Check for CSI ~ keys (Insert, Delete, PageUp/Down, F5-F12)
+ const tildeCode = CSI_TILDE_KEYS[ev.key];
+ if (tildeCode !== undefined) {
+ result.key = buildCsiTildeSequence(tildeCode, modifiers, eventType, reportEventTypes);
+ result.cancel = true;
+ return result;
+ }
+
+ // Get the key code for CSI u encoding
+ const keyCode = getKeyCode(ev);
+ if (keyCode === undefined) {
+ return result;
+ }
+
+ const isFunc = FUNCTIONAL_KEY_CODES[ev.key] !== undefined || getNumpadKeyCode(ev) !== undefined;
+
+ // Determine if we should use CSI u encoding
+ let useCsiU = false;
+
+ if (flags & KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES) {
+ useCsiU = true;
+ } else if (reportEventTypes) {
+ useCsiU = true;
+ } else if (flags & KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES) {
+ // Modifier-only keys already handled above
+ // Use CSI u for keys that would be ambiguous in legacy encoding
+ if (keyCode === 27 || keyCode === 127 || keyCode === 13 || keyCode === 9 || keyCode === 32) {
+ // Escape, Backspace, Enter, Tab, Space
+ useCsiU = true;
+ } else if (isFunc) {
+ useCsiU = true;
+ } else if (modifiers > 0) {
+ // Any modified key
+ useCsiU = true;
+ }
+ }
+
+ if (useCsiU) {
+ result.key = buildCsiUSequence(ev, keyCode, modifiers, eventType, flags, isFunc, isMod);
+ result.cancel = true;
+ } else {
+ // Legacy-compatible encoding for text keys without modifiers
+ if (ev.key.length === 1 && !ev.ctrlKey && !ev.altKey && !ev.metaKey) {
+ result.key = ev.key;
+ }
+ }
+
+ return result;
+}
+
+/**
+ * Build CSI letter sequence for arrow keys, Home, End.
+ * Format: CSI [1;mod] letter
+ */
+function buildCsiLetterSequence(
+ letter: string,
+ modifiers: number,
+ eventType: KittyKeyboardEventType,
+ reportEventTypes: boolean
+): string {
+ const needsEventType = reportEventTypes && eventType !== KittyKeyboardEventType.PRESS;
+
+ if (modifiers > 0 || needsEventType) {
+ let seq = C0.ESC + '[1;' + (modifiers > 0 ? modifiers : '1');
+ if (needsEventType) {
+ seq += ':' + eventType;
+ }
+ seq += letter;
+ return seq;
+ }
+ return C0.ESC + '[' + letter;
+}
+
+/**
+ * Build SS3 sequence for F1-F4.
+ * Without modifiers: SS3 letter
+ * With modifiers: CSI 1;mod letter
+ */
+function buildSs3Sequence(
+ letter: string,
+ modifiers: number,
+ eventType: KittyKeyboardEventType,
+ reportEventTypes: boolean
+): string {
+ const needsEventType = reportEventTypes && eventType !== KittyKeyboardEventType.PRESS;
+
+ if (modifiers > 0 || needsEventType) {
+ let seq = C0.ESC + '[1;' + (modifiers > 0 ? modifiers : '1');
+ if (needsEventType) {
+ seq += ':' + eventType;
+ }
+ seq += letter;
+ return seq;
+ }
+ return C0.ESC + 'O' + letter;
+}
+
+/**
+ * Build CSI ~ sequence for Insert, Delete, PageUp/Down, F5-F12.
+ * Format: CSI number [;mod[:event]] ~
+ */
+function buildCsiTildeSequence(
+ number: number,
+ modifiers: number,
+ eventType: KittyKeyboardEventType,
+ reportEventTypes: boolean
+): string {
+ const needsEventType = reportEventTypes && eventType !== KittyKeyboardEventType.PRESS;
+
+ let seq = C0.ESC + '[' + number;
+ if (modifiers > 0 || needsEventType) {
+ seq += ';' + (modifiers > 0 ? modifiers : '1');
+ if (needsEventType) {
+ seq += ':' + eventType;
+ }
+ }
+ seq += '~';
+ return seq;
+}
+
+/**
+ * Build CSI u sequence.
+ * Format: CSI keycode[:shifted[:base]] [;mod[:event][;text]] u
+ */
+function buildCsiUSequence(
+ ev: IKeyboardEvent,
+ keyCode: number,
+ modifiers: number,
+ eventType: KittyKeyboardEventType,
+ flags: number,
+ isFunc: boolean,
+ isMod: boolean
+): string {
+ const reportEventTypes = !!(flags & KittyKeyboardFlags.REPORT_EVENT_TYPES);
+ const reportAlternateKeys = !!(flags & KittyKeyboardFlags.REPORT_ALTERNATE_KEYS);
+
+ let seq = C0.ESC + '[' + keyCode;
+
+ // Add shifted key alternate if REPORT_ALTERNATE_KEYS is set and shift is pressed
+ // Only for text-producing keys (not functional or modifier keys)
+ let shiftedKey: number | undefined;
+ if (reportAlternateKeys && ev.shiftKey && ev.key.length === 1 && !isFunc && !isMod) {
+ shiftedKey = ev.key.codePointAt(0);
+ seq += ':' + shiftedKey;
+ }
+
+ // Check if we need associated text (press and repeat events, not release)
+ // Only for text-producing keys (not functional or modifier keys)
+ // Also don't include text when ctrl is pressed (produces control code)
+ const reportAssociatedText = !!(flags & KittyKeyboardFlags.REPORT_ASSOCIATED_TEXT) &&
+ eventType !== KittyKeyboardEventType.RELEASE &&
+ ev.key.length === 1 &&
+ !isFunc &&
+ !isMod &&
+ !ev.ctrlKey;
+ const textCode = reportAssociatedText ? ev.key.codePointAt(0) : undefined;
+
+ // Determine if we need event type suffix
+ // For repeat: only include :2 when there's no text (text implies it's still useful input)
+ // For release: always include :3
+ const needsEventType = reportEventTypes &&
+ eventType !== KittyKeyboardEventType.PRESS &&
+ (eventType === KittyKeyboardEventType.RELEASE || textCode === undefined);
+
+ if (modifiers > 0 || needsEventType || textCode !== undefined) {
+ seq += ';';
+ if (modifiers > 0) {
+ seq += modifiers;
+ } else if (needsEventType) {
+ seq += '1';
+ }
+ if (needsEventType) {
+ seq += ':' + eventType;
+ }
+ }
+
+ // Add associated text if requested
+ if (textCode !== undefined) {
+ seq += ';' + textCode;
+ }
+
+ seq += 'u';
+ return seq;
+}
+
+/**
+ * Check if a keyboard event should be handled by Kitty protocol.
+ * Returns true if Kitty flags are active and the event should use Kitty encoding.
+ */
+export function shouldUseKittyProtocol(flags: number): boolean {
+ return flags > 0;
+}
diff --git a/src/common/input/Win32InputMode.test.ts b/src/common/input/Win32InputMode.test.ts
new file mode 100644
index 0000000000..b3b72e6f20
--- /dev/null
+++ b/src/common/input/Win32InputMode.test.ts
@@ -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;
+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) => 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);
+ }));
+ });
+ });
+});
diff --git a/src/common/input/Win32InputMode.ts b/src/common/input/Win32InputMode.ts
new file mode 100644
index 0000000000..c1c88cf268
--- /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 = 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;
+}
diff --git a/src/common/services/CoreService.ts b/src/common/services/CoreService.ts
index 7b5f532d3b..7e1ef64b65 100644
--- a/src/common/services/CoreService.ts
+++ b/src/common/services/CoreService.ts
@@ -5,7 +5,7 @@
import { clone } from 'common/Clone';
import { Disposable } from 'vs/base/common/lifecycle';
-import { IDecPrivateModes, IModes } from 'common/Types';
+import { IDecPrivateModes, IKittyKeyboardState, IModes } from 'common/Types';
import { IBufferService, ICoreService, ILogService, IOptionsService } from 'common/services/Services';
import { Emitter } from 'vs/base/common/event';
@@ -23,9 +23,18 @@ const DEFAULT_DEC_PRIVATE_MODES: IDecPrivateModes = Object.freeze({
reverseWraparound: false,
sendFocus: false,
synchronizedOutput: false,
+ win32InputMode: false,
wraparound: true // defaults: xterm - true, vt100 - false
});
+const DEFAULT_KITTY_KEYBOARD_STATE = (): IKittyKeyboardState => ({
+ flags: 0,
+ mainFlags: 0,
+ altFlags: 0,
+ mainStack: [],
+ altStack: []
+});
+
export class CoreService extends Disposable implements ICoreService {
public serviceBrand: any;
@@ -33,6 +42,7 @@ export class CoreService extends Disposable implements ICoreService {
public isCursorHidden: boolean = false;
public modes: IModes;
public decPrivateModes: IDecPrivateModes;
+ public kittyKeyboard: IKittyKeyboardState;
private readonly _onData = this._register(new Emitter());
public readonly onData = this._onData.event;
@@ -51,11 +61,13 @@ export class CoreService extends Disposable implements ICoreService {
super();
this.modes = clone(DEFAULT_MODES);
this.decPrivateModes = clone(DEFAULT_DEC_PRIVATE_MODES);
+ this.kittyKeyboard = DEFAULT_KITTY_KEYBOARD_STATE();
}
public reset(): void {
this.modes = clone(DEFAULT_MODES);
this.decPrivateModes = clone(DEFAULT_DEC_PRIVATE_MODES);
+ this.kittyKeyboard = DEFAULT_KITTY_KEYBOARD_STATE();
}
public triggerDataEvent(data: string, wasUserInput: boolean = false): void {
diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts
index 54c3db23ab..f08f61f899 100644
--- a/src/common/services/OptionsService.ts
+++ b/src/common/services/OptionsService.ts
@@ -54,7 +54,8 @@ export const DEFAULT_OPTIONS: Readonly> = {
termName: 'xterm',
cancelEvents: false,
overviewRuler: {},
- quirks: {}
+ quirks: {},
+ vtExtensions: {}
};
const FONT_WEIGHT_OPTIONS: Extract[] = ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'];
diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts
index 3febd90733..d9698899b2 100644
--- a/src/common/services/Services.ts
+++ b/src/common/services/Services.ts
@@ -4,7 +4,7 @@
*/
import { IDecoration, IDecorationOptions, ILinkHandler, ILogger, IWindowsPty, type IOverviewRulerOptions } from '@xterm/xterm';
-import { CoreMouseEncoding, CoreMouseEventType, CursorInactiveStyle, CursorStyle, IAttributeData, ICharset, IColor, ICoreMouseEvent, ICoreMouseProtocol, IDecPrivateModes, IDisposable, IModes, IOscLinkData, IWindowOptions } from 'common/Types';
+import { CoreMouseEncoding, CoreMouseEventType, CursorInactiveStyle, CursorStyle, IAttributeData, ICharset, IColor, ICoreMouseEvent, ICoreMouseProtocol, IDecPrivateModes, IDisposable, IKittyKeyboardState, IModes, IOscLinkData, IWindowOptions } from 'common/Types';
import { IBuffer, IBufferSet } from 'common/buffer/Types';
import { createDecorator } from 'common/services/ServiceRegistry';
import type { Emitter, Event } from 'vs/base/common/event';
@@ -85,6 +85,7 @@ export interface ICoreService {
readonly modes: IModes;
readonly decPrivateModes: IDecPrivateModes;
+ readonly kittyKeyboard: IKittyKeyboardState;
readonly onData: Event;
readonly onUserInput: Event;
@@ -265,6 +266,7 @@ export interface ITerminalOptions {
overviewRuler?: IOverviewRulerOptions;
quirks?: ITerminalQuirks;
scrollOnEraseInDisplay?: boolean;
+ vtExtensions?: IVtExtensions;
[key: string]: any;
cancelEvents: boolean;
@@ -306,6 +308,12 @@ export interface ITerminalQuirks {
allowSetCursorBlink?: boolean;
}
+export interface IVtExtensions {
+ kittyKeyboard?: boolean;
+ kittySgrBoldFaintControl?: boolean;
+ win32InputMode?: boolean;
+}
+
export const IOscLinkService = createDecorator('OscLinkService');
export interface IOscLinkService {
serviceBrand: undefined;
diff --git a/src/headless/public/Terminal.test.ts b/src/headless/public/Terminal.test.ts
index 059b4a5051..0b35fb0e7a 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/src/headless/public/Terminal.ts b/src/headless/public/Terminal.ts
index e0a47e1f5d..18659b3ceb 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/test/playwright/Terminal.test.ts b/test/playwright/Terminal.test.ts
index d90b361bcf..9985fe67c8 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
});
});
diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts
index abc28d0816..ef7a0af57d 100644
--- a/typings/xterm-headless.d.ts
+++ b/typings/xterm-headless.d.ts
@@ -231,6 +231,11 @@ declare module '@xterm/headless' {
* All features are disabled by default for security reasons.
*/
windowOptions?: IWindowOptions;
+
+ /**
+ * Enable various VT extensions. All extensions are disabled by default.
+ */
+ vtExtensions?: IVtExtensions;
}
/**
@@ -313,6 +318,39 @@ declare module '@xterm/headless' {
buildNumber?: number;
}
+ /**
+ * Enable VT extensions that are not part of the core VT specification.
+ */
+ export interface IVtExtensions {
+ /**
+ * Whether the [kitty keyboard protocol][0] (`CSI =|?|>|< u`) is enabled.
+ * When enabled, the terminal will respond to keyboard protocol queries and
+ * allow programs to enable enhanced keyboard reporting. The default is
+ * false.
+ *
+ * [0]: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
+ */
+ kittyKeyboard?: boolean;
+
+ /**
+ * Whether [SGR 221 (not bold) and SGR 222 (not faint) are enabled][0].
+ * These are kitty extensions that allow resetting bold and faint
+ * independently. The default is true.
+ *
+ * [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;
+ }
+
/**
* A replacement logger for `console`.
*/
@@ -1358,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 fb74491cbe..f2cd3c2386 100644
--- a/typings/xterm.d.ts
+++ b/typings/xterm.d.ts
@@ -199,6 +199,12 @@ declare module '@xterm/xterm' {
*/
minimumContrastRatio?: number;
+ /**
+ * Controls the visibility and style of the overview ruler which visualizes
+ * decorations underneath the scroll bar.
+ */
+ overviewRuler?: IOverviewRulerOptions;
+
/**
* Control various quirks features that are either non-standard or standard
* in but generally rejected in modern terminals.
@@ -284,6 +290,11 @@ declare module '@xterm/xterm' {
*/
theme?: ITheme;
+ /**
+ * Enable various VT extensions. All extensions are disabled by default.
+ */
+ vtExtensions?: IVtExtensions;
+
/**
* Compatibility information when the pty is known to be hosted on Windows.
* Setting this will turn on certain heuristics/workarounds depending on the
@@ -313,12 +324,6 @@ declare module '@xterm/xterm' {
* All features are disabled by default for security reasons.
*/
windowOptions?: IWindowOptions;
-
- /**
- * Controls the visibility and style of the overview ruler which visualizes
- * decorations underneath the scroll bar.
- */
- overviewRuler?: IOverviewRulerOptions;
}
/**
@@ -430,6 +435,39 @@ declare module '@xterm/xterm' {
allowSetCursorBlink?: boolean;
}
+ /**
+ * Enable certain optional VT extensions.
+ */
+ export interface IVtExtensions {
+ /**
+ * Whether the [kitty keyboard protocol][0] (`CSI =|?|>|< u`) is enabled.
+ * When enabled, the terminal will respond to keyboard protocol queries and
+ * allow programs to enable enhanced keyboard reporting. The default is
+ * false.
+ *
+ * [0]: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
+ */
+ kittyKeyboard?: boolean;
+
+ /**
+ * Whether [SGR 221 (not bold) and SGR 222 (not faint) are enabled][0].
+ * These are kitty extensions that allow resetting bold and faint
+ * independently. The default is true.
+ *
+ * [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;
+ }
+
/**
* Pty information for Windows.
*/
@@ -1978,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`
*/