Skip to content

Commit 2c4725e

Browse files
diegosouzapwclaude
andauthored
fix(input): enable Alt→ESC prefix and fix macOS Alt-transformed keys (#18)
Fixes two keyboard issues from upstream coder#109: 1. Shift+Tab now produces the standard backtab sequence (CSI Z / \x1b[Z) via the Ghostty encoder with SHIFT modifier on Key.TAB. 2. Alt+letter on macOS: Alt transforms event.key to Unicode (Alt+T → '†'). The encoder now receives the correct letter by deriving utf8 from event.code (KeyT → 't') when altKey is set and event.key is non-ASCII. 3. Enable ALT_ESC_PREFIX (DEC mode 1036) by default so the encoder emits ESC+letter for Alt-modified keys, matching xterm metaSendsEscape default behavior. Inspired by: coder#109 Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 828627c commit 2c4725e

2 files changed

Lines changed: 53 additions & 1 deletion

File tree

lib/input-handler.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,42 @@ describe('InputHandler', () => {
587587
expect(dataReceived[0]).toBe('\t');
588588
});
589589

590+
// https://github.com/coder/ghostty-web/issues/109
591+
test('Shift+Tab produces backtab sequence (CSI Z)', () => {
592+
const handler = new InputHandler(
593+
ghostty,
594+
container as any,
595+
(data) => dataReceived.push(data),
596+
() => {
597+
bellCalled = true;
598+
}
599+
);
600+
601+
simulateKey(container, createKeyEvent('Tab', 'Tab', { shift: true }));
602+
603+
expect(dataReceived.length).toBe(1);
604+
expect(dataReceived[0]).toBe('\x1b[Z');
605+
});
606+
607+
// https://github.com/coder/ghostty-web/issues/109
608+
test('Alt+letter uses physical key (event.code) not transformed macOS character', () => {
609+
const handler = new InputHandler(
610+
ghostty,
611+
container as any,
612+
(data) => dataReceived.push(data),
613+
() => {
614+
bellCalled = true;
615+
}
616+
);
617+
618+
// On macOS, Alt+T produces '†' in event.key; we should encode Alt+T (ESC t) instead
619+
simulateKey(container, createKeyEvent('KeyT', '†', { alt: true }));
620+
621+
expect(dataReceived.length).toBe(1);
622+
// Alt+T should produce ESC + t, NOT the raw macOS Unicode character
623+
expect(dataReceived[0]).toBe('\x1bt');
624+
});
625+
590626
test('encodes Escape', () => {
591627
const handler = new InputHandler(
592628
ghostty,

lib/input-handler.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,8 @@ export class InputHandler {
244244
mouseConfig?: MouseTrackingConfig
245245
) {
246246
this.encoder = ghostty.createKeyEncoder();
247+
// Enable Alt → ESC+letter by default (xterm metaSendsEscape / DEC mode 1036).
248+
this.encoder.setOption(KeyEncoderOption.ALT_ESC_PREFIX, true);
247249
this.container = container;
248250
this.inputElement = inputElement;
249251
this.onDataCallback = onData;
@@ -452,11 +454,25 @@ export class InputHandler {
452454
// Case is preserved intentionally: the encoder uses the utf8 byte to
453455
// pick the C0 sequence for Ctrl+letter, and needs the actual shifted
454456
// character for the text-output path.
457+
//
458+
// macOS transforms Alt+letter to a Unicode char (e.g. Alt+T → '†').
459+
// When that happens event.key is non-ASCII, so we fall back to
460+
// deriving the utf8 from event.code (KeyT → 't') so the encoder can
461+
// produce the correct ESC+letter sequence. See issue #109.
455462
let utf8: string | undefined;
456463
if (event.key.length > 0 && event.key !== 'Dead' && event.key !== 'Unidentified') {
457464
const cp = event.key.codePointAt(0);
458465
const scalarLen = cp !== undefined && cp > 0xffff ? 2 : 1;
459-
if (event.key.length === scalarLen) utf8 = event.key;
466+
if (event.key.length === scalarLen) {
467+
if (event.altKey && cp !== undefined && cp > 127) {
468+
// macOS Alt-transformed character — derive from physical key code
469+
if (event.code.startsWith('Key') && event.code.length === 4) {
470+
utf8 = event.code[3].toLowerCase();
471+
}
472+
} else {
473+
utf8 = event.key;
474+
}
475+
}
460476
}
461477

462478
// Sync encoder options with terminal mode state before every encode.

0 commit comments

Comments
 (0)