mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
@@ -49,6 +49,7 @@ 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 { evaluateKeyboardEventKitty, KittyKeyboardEventType, shouldUseKittyProtocol } from 'common/input/KittyKeyboard';
|
||||
import { toRgbString } from 'common/input/XParseColor';
|
||||
import { DecorationService } from 'common/services/DecorationService';
|
||||
import { IDecorationService } from 'common/services/Services';
|
||||
@@ -1081,7 +1082,11 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
this._unprocessedDeadKey = true;
|
||||
}
|
||||
|
||||
const result = evaluateKeyboardEvent(event, this.coreService.decPrivateModes.applicationCursorKeys, this.browser.isMac, this.options.macOptionIsMeta);
|
||||
// Use Kitty keyboard protocol if enabled, otherwise use legacy encoding
|
||||
const kittyFlags = this.coreService.kittyKeyboard.flags;
|
||||
const result = shouldUseKittyProtocol(kittyFlags)
|
||||
? evaluateKeyboardEventKitty(event, kittyFlags, event.repeat ? KittyKeyboardEventType.REPEAT : KittyKeyboardEventType.PRESS)
|
||||
: evaluateKeyboardEvent(event, this.coreService.decPrivateModes.applicationCursorKeys, this.browser.isMac, this.options.macOptionIsMeta);
|
||||
|
||||
this.updateCursorStyle(event);
|
||||
|
||||
@@ -1168,6 +1173,15 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
this.focus();
|
||||
}
|
||||
|
||||
// Handle key release for Kitty keyboard protocol
|
||||
const kittyFlags = this.coreService.kittyKeyboard.flags;
|
||||
if (shouldUseKittyProtocol(kittyFlags) && (kittyFlags & 0b10)) { // REPORT_EVENT_TYPES flag
|
||||
const result = evaluateKeyboardEventKitty(ev, kittyFlags, KittyKeyboardEventType.RELEASE);
|
||||
if (result.key) {
|
||||
this.coreService.triggerDataEvent(result.key, true);
|
||||
}
|
||||
}
|
||||
|
||||
this.updateCursorStyle(ev);
|
||||
this._keyPressHandled = false;
|
||||
}
|
||||
|
||||
@@ -264,6 +264,11 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
this._parser.registerCsiHandler({ final: 's' }, params => this.saveCursor(params));
|
||||
this._parser.registerCsiHandler({ final: 't' }, params => this.windowOptions(params));
|
||||
this._parser.registerCsiHandler({ final: 'u' }, params => this.restoreCursor(params));
|
||||
// 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));
|
||||
this._parser.registerCsiHandler({ intermediates: '\'', final: '}' }, params => this.insertColumns(params));
|
||||
this._parser.registerCsiHandler({ intermediates: '\'', final: '~' }, params => this.deleteColumns(params));
|
||||
this._parser.registerCsiHandler({ intermediates: '"', final: 'q' }, params => this.selectProtected(params));
|
||||
@@ -2977,6 +2982,87 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
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 {
|
||||
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 {
|
||||
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;
|
||||
|
||||
// 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 {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* OSC 2; <data> ST (set window title)
|
||||
* Proxy to set window title.
|
||||
|
||||
@@ -114,6 +114,11 @@ export class MockCoreService implements ICoreService {
|
||||
synchronizedOutput: false,
|
||||
wraparound: true
|
||||
};
|
||||
public kittyKeyboard = {
|
||||
flags: 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,19 @@ 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 */
|
||||
flags: number;
|
||||
/** Stack of flags for main screen */
|
||||
mainStack: number[];
|
||||
/** Stack of flags for alternate screen */
|
||||
altStack: number[];
|
||||
}
|
||||
|
||||
export interface IRowRange {
|
||||
start: number;
|
||||
end: number;
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
|
||||
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('with DISAMBIGUATE_ESCAPE_CODES flag', () => {
|
||||
const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES;
|
||||
|
||||
it('should encode Escape as CSI 27 u', () => {
|
||||
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Escape' }), flags);
|
||||
assert.strictEqual(result.key, '\x1b[27u');
|
||||
});
|
||||
|
||||
it('should encode Enter as CSI 13 u', () => {
|
||||
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Enter' }), flags);
|
||||
assert.strictEqual(result.key, '\x1b[13u');
|
||||
});
|
||||
|
||||
it('should encode Backspace as CSI 127 u', () => {
|
||||
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Backspace' }), flags);
|
||||
assert.strictEqual(result.key, '\x1b[127u');
|
||||
});
|
||||
|
||||
it('should encode Tab as CSI 9 u', () => {
|
||||
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Tab' }), flags);
|
||||
assert.strictEqual(result.key, '\x1b[9u');
|
||||
});
|
||||
|
||||
it('should encode Shift+Tab with modifiers', () => {
|
||||
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Tab', shiftKey: true }), flags);
|
||||
assert.strictEqual(result.key, '\x1b[9;2u');
|
||||
});
|
||||
|
||||
it('should encode arrow keys using Kitty codepoints', () => {
|
||||
assert.strictEqual(evaluateKeyboardEventKitty(createEvent({ key: 'ArrowUp' }), flags).key, '\x1b[57417u');
|
||||
assert.strictEqual(evaluateKeyboardEventKitty(createEvent({ key: 'ArrowDown' }), flags).key, '\x1b[57420u');
|
||||
assert.strictEqual(evaluateKeyboardEventKitty(createEvent({ key: 'ArrowLeft' }), flags).key, '\x1b[57419u');
|
||||
assert.strictEqual(evaluateKeyboardEventKitty(createEvent({ key: 'ArrowRight' }), flags).key, '\x1b[57421u');
|
||||
});
|
||||
|
||||
it('should encode F1-F4 using Kitty codepoints', () => {
|
||||
assert.strictEqual(evaluateKeyboardEventKitty(createEvent({ key: 'F1' }), flags).key, '\x1b[57364u');
|
||||
assert.strictEqual(evaluateKeyboardEventKitty(createEvent({ key: 'F2' }), flags).key, '\x1b[57365u');
|
||||
assert.strictEqual(evaluateKeyboardEventKitty(createEvent({ key: 'F3' }), flags).key, '\x1b[57366u');
|
||||
assert.strictEqual(evaluateKeyboardEventKitty(createEvent({ key: 'F4' }), flags).key, '\x1b[57367u');
|
||||
});
|
||||
|
||||
it('should encode Ctrl+A with modifiers', () => {
|
||||
const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', ctrlKey: true }), flags);
|
||||
assert.strictEqual(result.key, '\x1b[97;5u');
|
||||
});
|
||||
|
||||
it('should encode Alt+X with modifiers', () => {
|
||||
const result = evaluateKeyboardEventKitty(createEvent({ key: 'x', altKey: true }), flags);
|
||||
assert.strictEqual(result.key, '\x1b[120;3u');
|
||||
});
|
||||
|
||||
it('should encode Ctrl+Shift+A with combined modifiers', () => {
|
||||
const result = evaluateKeyboardEventKitty(createEvent({ key: 'A', ctrlKey: true, shiftKey: true }), flags);
|
||||
assert.strictEqual(result.key, '\x1b[65;6u');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with REPORT_EVENT_TYPES flag', () => {
|
||||
const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES | KittyKeyboardFlags.REPORT_EVENT_TYPES;
|
||||
|
||||
it('should not include event type for press events', () => {
|
||||
const result = evaluateKeyboardEventKitty(createEvent({ key: 'a' }), flags, KittyKeyboardEventType.PRESS);
|
||||
assert.strictEqual(result.key, '\x1b[97u');
|
||||
});
|
||||
|
||||
it('should include event type for repeat events', () => {
|
||||
const result = evaluateKeyboardEventKitty(createEvent({ key: 'a' }), flags, KittyKeyboardEventType.REPEAT);
|
||||
assert.strictEqual(result.key, '\x1b[97;:2u');
|
||||
});
|
||||
|
||||
it('should include event type for release events', () => {
|
||||
const result = evaluateKeyboardEventKitty(createEvent({ key: 'a' }), flags, KittyKeyboardEventType.RELEASE);
|
||||
assert.strictEqual(result.key, '\x1b[97;:3u');
|
||||
});
|
||||
|
||||
it('should include modifiers and event type', () => {
|
||||
const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', ctrlKey: true }), flags, KittyKeyboardEventType.RELEASE);
|
||||
assert.strictEqual(result.key, '\x1b[97;5:3u');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with REPORT_ALL_KEYS_AS_ESCAPE_CODES flag', () => {
|
||||
const flags = KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES;
|
||||
|
||||
it('should encode regular letters as CSI u', () => {
|
||||
const result = evaluateKeyboardEventKitty(createEvent({ key: 'a' }), flags);
|
||||
assert.strictEqual(result.key, '\x1b[97u');
|
||||
});
|
||||
|
||||
it('should encode numbers as CSI u', () => {
|
||||
const result = evaluateKeyboardEventKitty(createEvent({ key: '1' }), flags);
|
||||
assert.strictEqual(result.key, '\x1b[49u');
|
||||
});
|
||||
|
||||
it('should encode space as CSI 32 u', () => {
|
||||
const result = evaluateKeyboardEventKitty(createEvent({ key: ' ' }), flags);
|
||||
assert.strictEqual(result.key, '\x1b[32u');
|
||||
});
|
||||
});
|
||||
|
||||
describe('numpad keys', () => {
|
||||
const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES;
|
||||
|
||||
it('should encode numpad digits with Kitty codepoints', () => {
|
||||
const result = evaluateKeyboardEventKitty(createEvent({ key: '0', code: 'Numpad0' }), flags);
|
||||
assert.strictEqual(result.key, '\x1b[57399u');
|
||||
});
|
||||
|
||||
it('should encode numpad enter', () => {
|
||||
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Enter', code: 'NumpadEnter' }), flags);
|
||||
assert.strictEqual(result.key, '\x1b[57414u');
|
||||
});
|
||||
});
|
||||
|
||||
describe('modifier keys', () => {
|
||||
const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES | KittyKeyboardFlags.REPORT_EVENT_TYPES;
|
||||
|
||||
it('should encode left shift with correct codepoint', () => {
|
||||
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Shift', code: 'ShiftLeft', shiftKey: true }), flags);
|
||||
assert.strictEqual(result.key, '\x1b[57441;2u');
|
||||
});
|
||||
|
||||
it('should encode right control with correct codepoint', () => {
|
||||
const result = evaluateKeyboardEventKitty(createEvent({ key: 'Control', code: 'ControlRight', ctrlKey: true }), flags);
|
||||
assert.strictEqual(result.key, '\x1b[57448;5u');
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,349 @@
|
||||
/**
|
||||
* 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,
|
||||
'Insert': 2,
|
||||
'Delete': 3,
|
||||
'ArrowLeft': 57419,
|
||||
'ArrowRight': 57421,
|
||||
'ArrowUp': 57417,
|
||||
'ArrowDown': 57420,
|
||||
'PageUp': 57423,
|
||||
'PageDown': 57424,
|
||||
'Home': 57416,
|
||||
'End': 57418,
|
||||
'CapsLock': 57358,
|
||||
'ScrollLock': 57359,
|
||||
'NumLock': 57360,
|
||||
'PrintScreen': 57361,
|
||||
'Pause': 57362,
|
||||
'ContextMenu': 57363,
|
||||
// F1-F35
|
||||
'F1': 57364,
|
||||
'F2': 57365,
|
||||
'F3': 57366,
|
||||
'F4': 57367,
|
||||
'F5': 57368,
|
||||
'F6': 57369,
|
||||
'F7': 57370,
|
||||
'F8': 57371,
|
||||
'F9': 57372,
|
||||
'F10': 57373,
|
||||
'F11': 57374,
|
||||
'F12': 57375,
|
||||
'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
|
||||
};
|
||||
|
||||
/**
|
||||
* 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;
|
||||
// Note: getModifierState would be needed for CAPS_LOCK/NUM_LOCK but not in IKeyboardEvent
|
||||
return mods > 0 ? mods + 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the unicode key code for a keyboard event.
|
||||
*/
|
||||
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 regular keys, use the key character's codepoint
|
||||
if (ev.key.length === 1) {
|
||||
return ev.key.codePointAt(0);
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
// Get the key code
|
||||
const keyCode = getKeyCode(ev);
|
||||
if (keyCode === undefined) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const modifiers = encodeModifiers(ev);
|
||||
const isFunc = FUNCTIONAL_KEY_CODES[ev.key] !== undefined || getNumpadKeyCode(ev) !== undefined;
|
||||
const isMod = isModifierKey(ev);
|
||||
|
||||
// Determine if we should use CSI u encoding or can use legacy
|
||||
let useCsiU = false;
|
||||
|
||||
if (flags & KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES) {
|
||||
// All keys use CSI u
|
||||
useCsiU = true;
|
||||
} else if (flags & KittyKeyboardFlags.REPORT_EVENT_TYPES) {
|
||||
// When reporting event types, use CSI u for all keys
|
||||
useCsiU = true;
|
||||
} else if (flags & KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES) {
|
||||
// Use CSI u for:
|
||||
// - Modifier-only keys when reporting event types
|
||||
// - Keys that would be ambiguous in legacy encoding
|
||||
// - Escape key
|
||||
// - Backspace
|
||||
// - Tab (when shifted)
|
||||
// - Enter
|
||||
if (isMod && (flags & KittyKeyboardFlags.REPORT_EVENT_TYPES)) {
|
||||
useCsiU = true;
|
||||
} else if (keyCode === 27 || keyCode === 127 || keyCode === 13) {
|
||||
// Escape, Backspace, Enter
|
||||
useCsiU = true;
|
||||
} else if (keyCode === 9 && ev.shiftKey) {
|
||||
// Shift+Tab
|
||||
useCsiU = true;
|
||||
} else if (isFunc) {
|
||||
useCsiU = true;
|
||||
} else if (modifiers > 0) {
|
||||
// Any modified key
|
||||
useCsiU = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if we should report this event type
|
||||
const reportEventTypes = !!(flags & KittyKeyboardFlags.REPORT_EVENT_TYPES);
|
||||
if (!reportEventTypes && eventType === KittyKeyboardEventType.RELEASE) {
|
||||
// Don't report release events unless flag is set
|
||||
return result;
|
||||
}
|
||||
|
||||
if (useCsiU) {
|
||||
// Build CSI u sequence: CSI keycode ; modifiers:event-type u
|
||||
// Format: CSI <keycode>[:<shifted>][:<base>] ; <modifiers>[:<event>] u
|
||||
let seq = C0.ESC + '[' + keyCode;
|
||||
|
||||
// Add modifiers and event type
|
||||
if (modifiers > 0 || (reportEventTypes && eventType !== KittyKeyboardEventType.PRESS)) {
|
||||
seq += ';';
|
||||
if (modifiers > 0) {
|
||||
seq += modifiers;
|
||||
}
|
||||
if (reportEventTypes && eventType !== KittyKeyboardEventType.PRESS) {
|
||||
seq += ':' + eventType;
|
||||
}
|
||||
}
|
||||
|
||||
// Add associated text if requested and available
|
||||
if ((flags & KittyKeyboardFlags.REPORT_ASSOCIATED_TEXT) && ev.key.length === 1 && !isFunc && !isMod) {
|
||||
const textCode = ev.key.codePointAt(0);
|
||||
if (textCode !== undefined && textCode !== keyCode) {
|
||||
// Append text as ; text u
|
||||
if (!seq.includes(';')) {
|
||||
seq += ';';
|
||||
}
|
||||
seq += ';' + textCode;
|
||||
}
|
||||
}
|
||||
|
||||
seq += 'u';
|
||||
result.key = seq;
|
||||
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;
|
||||
}
|
||||
// Otherwise no key sequence (will fall through to legacy handling)
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,12 @@ const DEFAULT_DEC_PRIVATE_MODES: IDecPrivateModes = Object.freeze({
|
||||
wraparound: true // defaults: xterm - true, vt100 - false
|
||||
});
|
||||
|
||||
const DEFAULT_KITTY_KEYBOARD_STATE = (): IKittyKeyboardState => ({
|
||||
flags: 0,
|
||||
mainStack: [],
|
||||
altStack: []
|
||||
});
|
||||
|
||||
export class CoreService extends Disposable implements ICoreService {
|
||||
public serviceBrand: any;
|
||||
|
||||
@@ -33,6 +39,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 +58,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 {
|
||||
|
||||
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user