Merge pull request #5600 from Tyriar/4198_kitty

Implement kitty keyboard protocol (CSI =|?|>|< u)
This commit is contained in:
Daniel Imms
2026-01-10 04:46:38 -08:00
committed by GitHub
16 changed files with 1600 additions and 68 deletions
+1 -1
View File
@@ -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', () => {
+17 -7
View File
@@ -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
View 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));
}
}
+9 -1
View File
@@ -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;
}
+96 -47
View File
@@ -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']);
});
});
+121 -3
View File
@@ -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 {
+7
View File
@@ -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;
+17
View File
@@ -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;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+12 -1
View File
@@ -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 {
+2 -1
View File
@@ -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'];
+8 -1
View File
@@ -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
View File
@@ -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`.
*/
+35 -6
View File
@@ -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.
*/