Skip to content

Commit 22f6d09

Browse files
authored
fix: support application cursor mode (DECCKM) for arrow keys (#81)
1 parent e879eef commit 22f6d09

File tree

4 files changed

+270
-148
lines changed

4 files changed

+270
-148
lines changed

lib/input-handler.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,60 @@ describe('InputHandler', () => {
492492
expect(dataReceived.length).toBe(1);
493493
expect(dataReceived[0]).toMatch(/\x1b(\[C|OC)/);
494494
});
495+
496+
test('sends CSI sequences in normal cursor mode (mode 1 off)', () => {
497+
// Create handler with getMode callback that returns false (normal mode)
498+
const handler = new InputHandler(
499+
ghostty,
500+
container as any,
501+
(data) => dataReceived.push(data),
502+
() => {
503+
bellCalled = true;
504+
},
505+
undefined,
506+
undefined,
507+
(_mode: number) => false // Normal cursor mode
508+
);
509+
510+
simulateKey(container, createKeyEvent('ArrowUp', 'ArrowUp'));
511+
simulateKey(container, createKeyEvent('ArrowDown', 'ArrowDown'));
512+
simulateKey(container, createKeyEvent('ArrowLeft', 'ArrowLeft'));
513+
simulateKey(container, createKeyEvent('ArrowRight', 'ArrowRight'));
514+
515+
expect(dataReceived.length).toBe(4);
516+
// Normal mode: CSI sequences (ESC[A, ESC[B, ESC[D, ESC[C)
517+
expect(dataReceived[0]).toBe('\x1b[A');
518+
expect(dataReceived[1]).toBe('\x1b[B');
519+
expect(dataReceived[2]).toBe('\x1b[D');
520+
expect(dataReceived[3]).toBe('\x1b[C');
521+
});
522+
523+
test('sends SS3 sequences in application cursor mode (mode 1 on)', () => {
524+
// Create handler with getMode callback that returns true for mode 1
525+
const handler = new InputHandler(
526+
ghostty,
527+
container as any,
528+
(data) => dataReceived.push(data),
529+
() => {
530+
bellCalled = true;
531+
},
532+
undefined,
533+
undefined,
534+
(mode: number) => mode === 1 // Application cursor mode enabled
535+
);
536+
537+
simulateKey(container, createKeyEvent('ArrowUp', 'ArrowUp'));
538+
simulateKey(container, createKeyEvent('ArrowDown', 'ArrowDown'));
539+
simulateKey(container, createKeyEvent('ArrowLeft', 'ArrowLeft'));
540+
simulateKey(container, createKeyEvent('ArrowRight', 'ArrowRight'));
541+
542+
expect(dataReceived.length).toBe(4);
543+
// Application mode: SS3 sequences (ESCOA, ESCOB, ESCOD, ESCOC)
544+
expect(dataReceived[0]).toBe('\x1bOA');
545+
expect(dataReceived[1]).toBe('\x1bOB');
546+
expect(dataReceived[2]).toBe('\x1bOD');
547+
expect(dataReceived[3]).toBe('\x1bOC');
548+
});
495549
});
496550

497551
describe('Function Keys', () => {

lib/input-handler.ts

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import type { Ghostty } from './ghostty';
1717
import type { KeyEncoder } from './ghostty';
1818
import type { IKeyEvent } from './interfaces';
19-
import { Key, KeyAction, Mods } from './types';
19+
import { Key, KeyAction, KeyEncoderOption, Mods } from './types';
2020

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

137137
// International
138-
IntlBackslash: Key.NON_US_BACKSLASH,
139-
ContextMenu: Key.APPLICATION,
138+
IntlBackslash: Key.INTL_BACKSLASH,
139+
ContextMenu: Key.CONTEXT_MENU,
140140

141141
// Additional function keys
142142
F13: Key.F13,
@@ -165,6 +165,7 @@ export class InputHandler {
165165
private onBellCallback: () => void;
166166
private onKeyCallback?: (keyEvent: IKeyEvent) => void;
167167
private customKeyEventHandler?: (event: KeyboardEvent) => boolean;
168+
private getModeCallback?: (mode: number) => boolean;
168169
private keydownListener: ((e: KeyboardEvent) => void) | null = null;
169170
private keypressListener: ((e: KeyboardEvent) => void) | null = null;
170171
private pasteListener: ((e: ClipboardEvent) => void) | null = null;
@@ -178,21 +179,24 @@ export class InputHandler {
178179
* @param onBell - Callback for bell/beep event
179180
* @param onKey - Optional callback for raw key events
180181
* @param customKeyEventHandler - Optional custom key event handler
182+
* @param getMode - Optional callback to query terminal mode state (for application cursor mode)
181183
*/
182184
constructor(
183185
ghostty: Ghostty,
184186
container: HTMLElement,
185187
onData: (data: string) => void,
186188
onBell: () => void,
187189
onKey?: (keyEvent: IKeyEvent) => void,
188-
customKeyEventHandler?: (event: KeyboardEvent) => boolean
190+
customKeyEventHandler?: (event: KeyboardEvent) => boolean,
191+
getMode?: (mode: number) => boolean
189192
) {
190193
this.encoder = ghostty.createKeyEncoder();
191194
this.container = container;
192195
this.onDataCallback = onData;
193196
this.onBellCallback = onBell;
194197
this.onKeyCallback = onKey;
195198
this.customKeyEventHandler = customKeyEventHandler;
199+
this.getModeCallback = getMode;
196200

197201
// Attach event listeners
198202
this.attach();
@@ -347,19 +351,7 @@ export class InputHandler {
347351
case Key.ESCAPE:
348352
simpleOutput = '\x1B'; // ESC
349353
break;
350-
// Arrow keys (CSI sequences)
351-
case Key.UP:
352-
simpleOutput = '\x1B[A';
353-
break;
354-
case Key.DOWN:
355-
simpleOutput = '\x1B[B';
356-
break;
357-
case Key.RIGHT:
358-
simpleOutput = '\x1B[C';
359-
break;
360-
case Key.LEFT:
361-
simpleOutput = '\x1B[D';
362-
break;
354+
// Arrow keys are handled by the encoder (respects application cursor mode)
363355
// Navigation keys
364356
case Key.HOME:
365357
simpleOutput = '\x1B[H';
@@ -430,6 +422,13 @@ export class InputHandler {
430422

431423
// For non-printable keys or keys with modifiers, encode using Ghostty
432424
try {
425+
// Sync encoder options with terminal mode state
426+
// Mode 1 (DECCKM) controls whether arrow keys send CSI or SS3 sequences
427+
if (this.getModeCallback) {
428+
const appCursorMode = this.getModeCallback(1);
429+
this.encoder.setOption(KeyEncoderOption.CURSOR_KEY_APPLICATION, appCursorMode);
430+
}
431+
433432
// For letter/number keys, even with modifiers, pass the base character
434433
// This helps the encoder produce correct control sequences (e.g., Ctrl+A = 0x01)
435434
// For special keys (Enter, Arrow keys, etc.), don't pass utf8

lib/terminal.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,11 @@ export class Terminal implements ITerminalCore {
441441
// Forward key events
442442
this.keyEmitter.fire(keyEvent);
443443
},
444-
this.customKeyEventHandler
444+
this.customKeyEventHandler,
445+
(mode: number) => {
446+
// Query terminal mode state (e.g., mode 1 for application cursor mode)
447+
return this.wasmTerm?.getMode(mode, false) ?? false;
448+
}
445449
);
446450

447451
// Create selection manager (pass textarea for context menu positioning)

0 commit comments

Comments
 (0)