Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 87 additions & 2 deletions src/browser/CoreBrowserTerminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ 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 { evaluateKeyboardEvent, encodeKittyKeyboardEvent } from 'common/input/Keyboard';
import { toRgbString } from 'common/input/XParseColor';
import { DecorationService } from 'common/services/DecorationService';
import { IDecorationService } from 'common/services/Services';
Expand Down Expand Up @@ -119,6 +119,12 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
*/
private _unprocessedDeadKey: boolean = false;

/**
* Tracks currently pressed keys for Kitty keyboard protocol repeat/release events.
* Maps key codes to their last press event details.
*/
private _pressedKeys = new Map<string, { key: string, timestamp: number }>();

private _compositionHelper: ICompositionHelper | undefined;
private _accessibilityManager: MutableDisposable<AccessibilityManager> = this._register(new MutableDisposable());

Expand Down Expand Up @@ -1040,7 +1046,10 @@ 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 = evaluateKeyboardEvent(event, this.coreService.decPrivateModes.applicationCursorKeys, this.browser.isMac, this.options.macOptionIsMeta, this.coreService.decPrivateModes.kittyKeyboardFlags);

// Handle Kitty keyboard protocol events (repeat tracking)
this._handleKittyKeyPress(event);

this.updateCursorStyle(event);

Expand Down Expand Up @@ -1102,6 +1111,79 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
this._keyDownHandled = true;
}

/**
* Handles Kitty keyboard protocol key press event tracking and repeat detection
*/
private _handleKittyKeyPress(event: KeyboardEvent): boolean {
const flags = this.coreService.decPrivateModes.kittyKeyboardFlags;
const reportEvents = flags & 2; // KITTY_FLAG_REPORT_EVENTS

if (!reportEvents) {
return false;
}

const keyId = `${event.code}-${event.key}`;
const now = Date.now();
const lastPress = this._pressedKeys.get(keyId);

let eventType = 1; // press
if (lastPress && (now - lastPress.timestamp) < 100) {
eventType = 2; // repeat
}

this._pressedKeys.set(keyId, { key: event.key, timestamp: now });

// Only send separate event if it's a repeat or if REPORT_ALL_KEYS is set
if (eventType === 2 || (flags & 8)) { // KITTY_FLAG_REPORT_ALL_KEYS
const kittySequence = this._generateKittyKeySequence(event, eventType);
if (kittySequence) {
this.coreService.triggerDataEvent(kittySequence, true);
return true;
}
}

return false;
}

/**
* Handles Kitty keyboard protocol key release events
*/
private _handleKittyKeyRelease(event: KeyboardEvent): void {
const flags = this.coreService.decPrivateModes.kittyKeyboardFlags;
const reportEvents = flags & 2; // KITTY_FLAG_REPORT_EVENTS

if (!reportEvents) {
return;
}

const keyId = `${event.code}-${event.key}`;
const wasPressed = this._pressedKeys.has(keyId);

if (wasPressed) {
this._pressedKeys.delete(keyId);

// Generate release event - avoid Enter, Tab, Backspace unless REPORT_ALL_KEYS is set
const reportAllKeys = flags & 8; // KITTY_FLAG_REPORT_ALL_KEYS
if (reportAllKeys || (event.key !== 'Enter' && event.key !== 'Tab' && event.key !== 'Backspace')) {
const kittySequence = this._generateKittyKeySequence(event, 3); // 3 = release
if (kittySequence) {
this.coreService.triggerDataEvent(kittySequence, true);
}
}
}
}

/**
* Generates Kitty keyboard protocol sequence for an event
*/
private _generateKittyKeySequence(event: KeyboardEvent, eventType: number): string | null {
try {
return encodeKittyKeyboardEvent(event, this.coreService.decPrivateModes.kittyKeyboardFlags, eventType);
} catch (e) {
return null;
}
}

private _isThirdLevelShift(browser: IBrowser, ev: KeyboardEvent): boolean {
const thirdLevelKey =
(browser.isMac && !this.options.macOptionIsMeta && ev.altKey && !ev.ctrlKey && !ev.metaKey) ||
Expand All @@ -1123,6 +1205,9 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
return;
}

// Handle Kitty keyboard protocol release events
this._handleKittyKeyRelease(ev);

if (!wasModifierKeyOnlyEvent(ev)) {
this.focus();
}
Expand Down
80 changes: 80 additions & 0 deletions src/common/InputHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export class InputHandler extends Disposable implements IInputHandler {
private _dirtyRowTracker: IDirtyRowTracker;
protected _windowTitleStack: string[] = [];
protected _iconNameStack: string[] = [];
protected _kittyKeyboardModeStack: number[] = [];

private _curAttrData: IAttributeData = DEFAULT_ATTR_DATA.clone();
public getAttrData(): IAttributeData { return this._curAttrData; }
Expand Down Expand Up @@ -261,6 +262,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.setKittyKeyboardMode(params));
this._parser.registerCsiHandler({ prefix: '?', final: 'u' }, params => this.queryKittyKeyboardMode());
this._parser.registerCsiHandler({ prefix: '>', final: 'u' }, params => this.pushKittyKeyboardMode(params));
this._parser.registerCsiHandler({ prefix: '<', final: 'u' }, params => this.popKittyKeyboardMode(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));
Expand Down Expand Up @@ -3425,6 +3431,80 @@ export class InputHandler extends Disposable implements IInputHandler {
public markRangeDirty(y1: number, y2: number): void {
this._dirtyRowTracker.markRangeDirty(y1, y2);
}

/**
* Kitty keyboard protocol handlers
*/

/**
* CSI = flags ; mode u Set keyboard mode flags
*/
public setKittyKeyboardMode(params: IParams): boolean {
const flags = params.length >= 1 ? params.params[0] : 0;
const mode = params.length >= 2 ? params.params[1] : 1;

if (mode === 1) {
// Set all flags to specified value (reset unspecified bits)
this._coreService.decPrivateModes.kittyKeyboardFlags = flags;
} else if (mode === 2) {
// Set specified bits, leave unset bits unchanged
this._coreService.decPrivateModes.kittyKeyboardFlags |= flags;
} else if (mode === 3) {
// Reset specified bits, leave unset bits unchanged
this._coreService.decPrivateModes.kittyKeyboardFlags &= ~flags;
}

return true;
}

/**
* CSI ? u Query keyboard mode flags
*/
public queryKittyKeyboardMode(): boolean {
this._coreService.triggerDataEvent(`${C0.ESC}[?${this._coreService.decPrivateModes.kittyKeyboardFlags}u`);
return true;
}

/**
* CSI > flags u Push keyboard mode flags (with optional default)
*/
public pushKittyKeyboardMode(params: IParams): boolean {
const defaultFlags = params.length >= 1 ? params.params[0] : 0;

// Limit stack size to prevent DoS attacks
if (this._kittyKeyboardModeStack.length >= 16) {
this._kittyKeyboardModeStack.shift();
}

// Push current mode onto stack
this._kittyKeyboardModeStack.push(this._coreService.decPrivateModes.kittyKeyboardFlags);

// Set to default flags if provided
if (params.length >= 1) {
this._coreService.decPrivateModes.kittyKeyboardFlags = defaultFlags;
}

return true;
}

/**
* CSI < number u Pop keyboard mode flags
*/
public popKittyKeyboardMode(params: IParams): boolean {
const count = params.length >= 1 ? params.params[0] : 1;

for (let i = 0; i < count && this._kittyKeyboardModeStack.length > 0; i++) {
const flags = this._kittyKeyboardModeStack.pop()!;
this._coreService.decPrivateModes.kittyKeyboardFlags = flags;
}

// If stack is empty, reset all flags
if (this._kittyKeyboardModeStack.length === 0) {
this._coreService.decPrivateModes.kittyKeyboardFlags = 0;
}

return true;
}
}

export interface IDirtyRowTracker {
Expand Down
1 change: 1 addition & 0 deletions src/common/TestUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export class MockCoreService implements ICoreService {
bracketedPasteMode: false,
cursorBlink: undefined,
cursorStyle: undefined,
kittyKeyboardFlags: 0,
origin: false,
reverseWraparound: false,
sendFocus: false,
Expand Down
1 change: 1 addition & 0 deletions src/common/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ export interface IDecPrivateModes {
bracketedPasteMode: boolean;
cursorBlink: boolean | undefined;
cursorStyle: CursorStyle | undefined;
kittyKeyboardFlags: number;
origin: boolean;
reverseWraparound: boolean;
sendFocus: boolean;
Expand Down
Loading
Loading