mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge pull request #5600 from Tyriar/4198_kitty
Implement kitty keyboard protocol (CSI =|?|>|< u)
This commit is contained in:
+1
-1
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
|
||||
+121
-3
@@ -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;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
Vendored
+29
@@ -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`.
|
||||
*/
|
||||
|
||||
Vendored
+35
-6
@@ -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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user