Skip to content
Merged
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
54 changes: 54 additions & 0 deletions lib/input-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,60 @@ describe('InputHandler', () => {
expect(dataReceived.length).toBe(1);
expect(dataReceived[0]).toMatch(/\x1b(\[C|OC)/);
});

test('sends CSI sequences in normal cursor mode (mode 1 off)', () => {
// Create handler with getMode callback that returns false (normal mode)
const handler = new InputHandler(
ghostty,
container as any,
(data) => dataReceived.push(data),
() => {
bellCalled = true;
},
undefined,
undefined,
(_mode: number) => false // Normal cursor mode
);

simulateKey(container, createKeyEvent('ArrowUp', 'ArrowUp'));
simulateKey(container, createKeyEvent('ArrowDown', 'ArrowDown'));
simulateKey(container, createKeyEvent('ArrowLeft', 'ArrowLeft'));
simulateKey(container, createKeyEvent('ArrowRight', 'ArrowRight'));

expect(dataReceived.length).toBe(4);
// Normal mode: CSI sequences (ESC[A, ESC[B, ESC[D, ESC[C)
expect(dataReceived[0]).toBe('\x1b[A');
expect(dataReceived[1]).toBe('\x1b[B');
expect(dataReceived[2]).toBe('\x1b[D');
expect(dataReceived[3]).toBe('\x1b[C');
});

test('sends SS3 sequences in application cursor mode (mode 1 on)', () => {
// Create handler with getMode callback that returns true for mode 1
const handler = new InputHandler(
ghostty,
container as any,
(data) => dataReceived.push(data),
() => {
bellCalled = true;
},
undefined,
undefined,
(mode: number) => mode === 1 // Application cursor mode enabled
);

simulateKey(container, createKeyEvent('ArrowUp', 'ArrowUp'));
simulateKey(container, createKeyEvent('ArrowDown', 'ArrowDown'));
simulateKey(container, createKeyEvent('ArrowLeft', 'ArrowLeft'));
simulateKey(container, createKeyEvent('ArrowRight', 'ArrowRight'));

expect(dataReceived.length).toBe(4);
// Application mode: SS3 sequences (ESCOA, ESCOB, ESCOD, ESCOC)
expect(dataReceived[0]).toBe('\x1bOA');
expect(dataReceived[1]).toBe('\x1bOB');
expect(dataReceived[2]).toBe('\x1bOD');
expect(dataReceived[3]).toBe('\x1bOC');
});
});

describe('Function Keys', () => {
Expand Down
33 changes: 16 additions & 17 deletions lib/input-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import type { Ghostty } from './ghostty';
import type { KeyEncoder } from './ghostty';
import type { IKeyEvent } from './interfaces';
import { Key, KeyAction, Mods } from './types';
import { Key, KeyAction, KeyEncoderOption, Mods } from './types';

/**
* Map KeyboardEvent.code values to USB HID Key enum values
Expand Down Expand Up @@ -135,8 +135,8 @@ const KEY_MAP: Record<string, Key> = {
NumpadDecimal: Key.KP_PERIOD,

// International
IntlBackslash: Key.NON_US_BACKSLASH,
ContextMenu: Key.APPLICATION,
IntlBackslash: Key.INTL_BACKSLASH,
ContextMenu: Key.CONTEXT_MENU,

// Additional function keys
F13: Key.F13,
Expand Down Expand Up @@ -165,6 +165,7 @@ export class InputHandler {
private onBellCallback: () => void;
private onKeyCallback?: (keyEvent: IKeyEvent) => void;
private customKeyEventHandler?: (event: KeyboardEvent) => boolean;
private getModeCallback?: (mode: number) => boolean;
private keydownListener: ((e: KeyboardEvent) => void) | null = null;
private keypressListener: ((e: KeyboardEvent) => void) | null = null;
private pasteListener: ((e: ClipboardEvent) => void) | null = null;
Expand All @@ -178,21 +179,24 @@ export class InputHandler {
* @param onBell - Callback for bell/beep event
* @param onKey - Optional callback for raw key events
* @param customKeyEventHandler - Optional custom key event handler
* @param getMode - Optional callback to query terminal mode state (for application cursor mode)
*/
constructor(
ghostty: Ghostty,
container: HTMLElement,
onData: (data: string) => void,
onBell: () => void,
onKey?: (keyEvent: IKeyEvent) => void,
customKeyEventHandler?: (event: KeyboardEvent) => boolean
customKeyEventHandler?: (event: KeyboardEvent) => boolean,
getMode?: (mode: number) => boolean
) {
this.encoder = ghostty.createKeyEncoder();
this.container = container;
this.onDataCallback = onData;
this.onBellCallback = onBell;
this.onKeyCallback = onKey;
this.customKeyEventHandler = customKeyEventHandler;
this.getModeCallback = getMode;

// Attach event listeners
this.attach();
Expand Down Expand Up @@ -347,19 +351,7 @@ export class InputHandler {
case Key.ESCAPE:
simpleOutput = '\x1B'; // ESC
break;
// Arrow keys (CSI sequences)
case Key.UP:
simpleOutput = '\x1B[A';
break;
case Key.DOWN:
simpleOutput = '\x1B[B';
break;
case Key.RIGHT:
simpleOutput = '\x1B[C';
break;
case Key.LEFT:
simpleOutput = '\x1B[D';
break;
// Arrow keys are handled by the encoder (respects application cursor mode)
// Navigation keys
case Key.HOME:
simpleOutput = '\x1B[H';
Expand Down Expand Up @@ -430,6 +422,13 @@ export class InputHandler {

// For non-printable keys or keys with modifiers, encode using Ghostty
try {
// Sync encoder options with terminal mode state
// Mode 1 (DECCKM) controls whether arrow keys send CSI or SS3 sequences
if (this.getModeCallback) {
const appCursorMode = this.getModeCallback(1);
this.encoder.setOption(KeyEncoderOption.CURSOR_KEY_APPLICATION, appCursorMode);
}

// For letter/number keys, even with modifiers, pass the base character
// This helps the encoder produce correct control sequences (e.g., Ctrl+A = 0x01)
// For special keys (Enter, Arrow keys, etc.), don't pass utf8
Expand Down
6 changes: 5 additions & 1 deletion lib/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,11 @@ export class Terminal implements ITerminalCore {
// Forward key events
this.keyEmitter.fire(keyEvent);
},
this.customKeyEventHandler
this.customKeyEventHandler,
(mode: number) => {
// Query terminal mode state (e.g., mode 1 for application cursor mode)
return this.wasmTerm?.getMode(mode, false) ?? false;
}
);

// Create selection manager (pass textarea for context menu positioning)
Expand Down
Loading