mirror of
https://github.com/xtermjs/xterm.js.git
synced 2026-02-01 16:48:16 +00:00
Merge pull request #5600 from Tyriar/4198_kitty
Implement kitty keyboard protocol (CSI =|?|>|< u)
This commit is contained in:
commit
91c4761b82
@ -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');
|
||||
}
|
||||
|
||||
@ -119,9 +119,14 @@ 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' }
|
||||
];
|
||||
const stringOptions: { [key: string]: string[] | null } = {
|
||||
cursorStyle: ['block', 'underline', 'bar'],
|
||||
cursorInactiveStyle: ['outline', 'block', 'bar', 'underline', 'none'],
|
||||
@ -156,6 +161,10 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
|
||||
booleanOptions.forEach(o => {
|
||||
html += `<div class="option"><label><input id="opt-${o}" type="checkbox" ${this._terminal.options[o] ? 'checked' : ''}/> ${o}</label></div>`;
|
||||
});
|
||||
nestedBooleanOptions.forEach(({ label, parent, prop }) => {
|
||||
const checked = this._terminal.options[parent]?.[prop] ?? false;
|
||||
html += `<div class="option"><label><input id="opt-${label.replace('.', '-')}" type="checkbox" ${checked ? 'checked' : ''}/> ${label}</label></div>`;
|
||||
});
|
||||
html += '</div><div class="option-group">';
|
||||
numberOptions.forEach(o => {
|
||||
html += `<div class="option"><label>${o} <input id="opt-${o}" type="number" value="${this._terminal.options[o] ?? ''}" step="${o === 'lineHeight' || o === 'scrollSensitivity' ? '0.1' : '1'}"/></label></div>`;
|
||||
@ -187,6 +196,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', () => {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
41
src/browser/services/KeyboardService.ts
Normal file
41
src/browser/services/KeyboardService.ts
Normal file
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 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 { 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 {
|
||||
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 {
|
||||
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));
|
||||
}
|
||||
}
|
||||
@ -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<ICharSizeService>('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<IKeyboardService>('KeyboardService');
|
||||
export interface IKeyboardService {
|
||||
serviceBrand: undefined;
|
||||
evaluateKeyDown(event: KeyboardEvent): IKeyboardResult;
|
||||
evaluateKeyUp(event: KeyboardEvent): IKeyboardResult | undefined;
|
||||
readonly useKitty: boolean;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
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());
|
||||
});
|
||||
coreService.onData(data => {
|
||||
const m = data.match(/\x1b\[(.*?);(.*?)R/);
|
||||
if (m) {
|
||||
cpr.push([parseInt(m[1]), parseInt(m[2])]);
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
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;
|
||||
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']);
|
||||
});
|
||||
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;
|
||||
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']);
|
||||
});
|
||||
await inputHandler.parseP('hello world!\r\n\x1bP1;2asome data\x1b\\second line');
|
||||
assert.deepEqual(getLines(bufferService, 2), ['hello world!', 'second line']);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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);
|
||||
@ -2232,6 +2244,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) {
|
||||
@ -2654,10 +2672,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 +3002,6 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* OSC 2; <data> ST (set window title)
|
||||
* Proxy to set window title.
|
||||
@ -3496,6 +3513,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 {
|
||||
|
||||
@ -114,6 +114,13 @@ export class MockCoreService implements ICoreService {
|
||||
synchronizedOutput: false,
|
||||
wraparound: true
|
||||
};
|
||||
public kittyKeyboard = {
|
||||
flags: 0,
|
||||
mainFlags: 0,
|
||||
altFlags: 0,
|
||||
mainStack: [] as number[],
|
||||
altStack: [] as number[]
|
||||
};
|
||||
public onData: Event<string> = new Emitter<string>().event;
|
||||
public onUserInput: Event<void> = new Emitter<void>().event;
|
||||
public onBinary: Event<string> = new Emitter<string>().event;
|
||||
|
||||
@ -277,6 +277,23 @@ export interface IDecPrivateModes {
|
||||
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;
|
||||
|
||||
676
src/common/input/KittyKeyboard.test.ts
Normal file
676
src/common/input/KittyKeyboard.test.ts
Normal file
@ -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> = {}): 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
513
src/common/input/KittyKeyboard.ts
Normal file
513
src/common/input/KittyKeyboard.ts
Normal file
@ -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;
|
||||
}
|
||||
@ -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';
|
||||
|
||||
@ -26,6 +26,14 @@ const DEFAULT_DEC_PRIVATE_MODES: IDecPrivateModes = Object.freeze({
|
||||
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 +41,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<string>());
|
||||
public readonly onData = this._onData.event;
|
||||
@ -51,11 +60,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 {
|
||||
|
||||
@ -54,7 +54,8 @@ export const DEFAULT_OPTIONS: Readonly<Required<ITerminalOptions>> = {
|
||||
termName: 'xterm',
|
||||
cancelEvents: false,
|
||||
overviewRuler: {},
|
||||
quirks: {}
|
||||
quirks: {},
|
||||
vtExtensions: {}
|
||||
};
|
||||
|
||||
const FONT_WEIGHT_OPTIONS: Extract<FontWeight, string>[] = ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'];
|
||||
|
||||
@ -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<string>;
|
||||
readonly onUserInput: Event<void>;
|
||||
@ -265,6 +266,7 @@ export interface ITerminalOptions {
|
||||
overviewRuler?: IOverviewRulerOptions;
|
||||
quirks?: ITerminalQuirks;
|
||||
scrollOnEraseInDisplay?: boolean;
|
||||
vtExtensions?: IVtExtensions;
|
||||
|
||||
[key: string]: any;
|
||||
cancelEvents: boolean;
|
||||
@ -306,6 +308,11 @@ export interface ITerminalQuirks {
|
||||
allowSetCursorBlink?: boolean;
|
||||
}
|
||||
|
||||
export interface IVtExtensions {
|
||||
kittyKeyboard?: boolean;
|
||||
kittySgrBoldFaintControl?: boolean;
|
||||
}
|
||||
|
||||
export const IOscLinkService = createDecorator<IOscLinkService>('OscLinkService');
|
||||
export interface IOscLinkService {
|
||||
serviceBrand: undefined;
|
||||
|
||||
29
typings/xterm-headless.d.ts
vendored
29
typings/xterm-headless.d.ts
vendored
@ -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,30 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* A replacement logger for `console`.
|
||||
*/
|
||||
|
||||
41
typings/xterm.d.ts
vendored
41
typings/xterm.d.ts
vendored
@ -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,30 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pty information for Windows.
|
||||
*/
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user