Skip to content

Commit f3b2bc4

Browse files
authored
feat: add terminal modes API (#39)
- Add WASM C API functions for querying terminal modes - Implement getMode(), hasBracketedPaste(), hasFocusEvents(), hasMouseTracking() - Fix paste() method to automatically use bracketed paste when enabled
1 parent 98ed4f1 commit f3b2bc4

File tree

5 files changed

+368
-29
lines changed

5 files changed

+368
-29
lines changed

lib/ghostty.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,4 +639,36 @@ export class GhosttyTerminal {
639639
this.exports.ghostty_wasm_free_u8_array(bufferPtr, maxUriLen);
640640
}
641641
}
642+
643+
// ============================================================================
644+
// Terminal Modes
645+
// ============================================================================
646+
647+
/**
648+
* Query terminal mode state
649+
*/
650+
getMode(mode: number, isAnsi: boolean = false): boolean {
651+
return this.exports.ghostty_terminal_get_mode(this.handle, mode, isAnsi ? 1 : 0) !== 0;
652+
}
653+
654+
/**
655+
* Check if bracketed paste mode is enabled
656+
*/
657+
hasBracketedPaste(): boolean {
658+
return this.exports.ghostty_terminal_has_bracketed_paste(this.handle) !== 0;
659+
}
660+
661+
/**
662+
* Check if focus event reporting is enabled
663+
*/
664+
hasFocusEvents(): boolean {
665+
return this.exports.ghostty_terminal_has_focus_events(this.handle) !== 0;
666+
}
667+
668+
/**
669+
* Check if mouse tracking is enabled
670+
*/
671+
hasMouseTracking(): boolean {
672+
return this.exports.ghostty_terminal_has_mouse_tracking(this.handle) !== 0;
673+
}
642674
}

lib/terminal.test.ts

Lines changed: 128 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1018,8 +1018,7 @@ describe('attachCustomKeyEventHandler()', () => {
10181018
await term.open(container);
10191019

10201020
const handler = (e: KeyboardEvent) => false;
1021-
term.attachCustomKeyEventHandler(handler);
1022-
expect(() => term.attachCustomKeyEventHandler(undefined)).not.toThrow();
1021+
expect(() => term.attachCustomKeyEventHandler(handler)).not.toThrow();
10231022
term.dispose();
10241023
});
10251024
});
@@ -1179,3 +1178,130 @@ describe('Buffer Access API', () => {
11791178
expect(term.wasmTerm?.isRowWrapped(999)).toBe(false);
11801179
});
11811180
});
1181+
1182+
describe('Terminal Modes', () => {
1183+
test('should detect bracketed paste mode', async () => {
1184+
if (typeof document === 'undefined') return;
1185+
const term = new Terminal({ cols: 80, rows: 24 });
1186+
const container = document.createElement('div');
1187+
await term.open(container);
1188+
1189+
expect(term.hasBracketedPaste()).toBe(false);
1190+
term.write('\x1b[?2004h');
1191+
expect(term.hasBracketedPaste()).toBe(true);
1192+
term.write('\x1b[?2004l');
1193+
expect(term.hasBracketedPaste()).toBe(false);
1194+
1195+
term.dispose();
1196+
});
1197+
1198+
test('paste() should use bracketed paste when enabled', async () => {
1199+
if (typeof document === 'undefined') return;
1200+
const term = new Terminal({ cols: 80, rows: 24 });
1201+
const container = document.createElement('div');
1202+
await term.open(container);
1203+
1204+
let receivedData = '';
1205+
term.onData((data) => {
1206+
receivedData = data;
1207+
});
1208+
1209+
term.paste('test');
1210+
expect(receivedData).toBe('test');
1211+
1212+
term.write('\x1b[?2004h');
1213+
term.paste('test2');
1214+
expect(receivedData).toBe('\x1b[200~test2\x1b[201~');
1215+
1216+
term.dispose();
1217+
});
1218+
1219+
test('should query arbitrary DEC modes', async () => {
1220+
if (typeof document === 'undefined') return;
1221+
const term = new Terminal({ cols: 80, rows: 24 });
1222+
const container = document.createElement('div');
1223+
await term.open(container);
1224+
1225+
expect(term.getMode(25)).toBe(true); // Cursor visible
1226+
term.write('\x1b[?25l');
1227+
expect(term.getMode(25)).toBe(false);
1228+
1229+
term.dispose();
1230+
});
1231+
1232+
test('should detect focus event mode', async () => {
1233+
if (typeof document === 'undefined') return;
1234+
const term = new Terminal({ cols: 80, rows: 24 });
1235+
const container = document.createElement('div');
1236+
await term.open(container);
1237+
1238+
expect(term.hasFocusEvents()).toBe(false);
1239+
term.write('\x1b[?1004h');
1240+
expect(term.hasFocusEvents()).toBe(true);
1241+
1242+
term.dispose();
1243+
});
1244+
1245+
test('should detect mouse tracking modes', async () => {
1246+
if (typeof document === 'undefined') return;
1247+
const term = new Terminal({ cols: 80, rows: 24 });
1248+
const container = document.createElement('div');
1249+
await term.open(container);
1250+
1251+
expect(term.hasMouseTracking()).toBe(false);
1252+
term.write('\x1b[?1000h');
1253+
expect(term.hasMouseTracking()).toBe(true);
1254+
1255+
term.dispose();
1256+
});
1257+
1258+
test('should query ANSI modes vs DEC modes', async () => {
1259+
if (typeof document === 'undefined') return;
1260+
const term = new Terminal({ cols: 80, rows: 24 });
1261+
const container = document.createElement('div');
1262+
await term.open(container);
1263+
1264+
expect(term.getMode(4, true)).toBe(false); // Insert mode
1265+
term.write('\x1b[4h');
1266+
expect(term.getMode(4, true)).toBe(true);
1267+
1268+
term.dispose();
1269+
});
1270+
1271+
test('should handle multiple modes set simultaneously', async () => {
1272+
if (typeof document === 'undefined') return;
1273+
const term = new Terminal({ cols: 80, rows: 24 });
1274+
const container = document.createElement('div');
1275+
await term.open(container);
1276+
1277+
term.write('\x1b[?2004h\x1b[?1004h\x1b[?1000h');
1278+
expect(term.hasBracketedPaste()).toBe(true);
1279+
expect(term.hasFocusEvents()).toBe(true);
1280+
expect(term.hasMouseTracking()).toBe(true);
1281+
1282+
term.dispose();
1283+
});
1284+
1285+
test('getMode() throws when terminal not open', () => {
1286+
const term = new Terminal({ cols: 80, rows: 24 });
1287+
expect(() => term.getMode(25)).toThrow();
1288+
});
1289+
1290+
test('hasBracketedPaste() throws when terminal not open', () => {
1291+
const term = new Terminal({ cols: 80, rows: 24 });
1292+
expect(() => term.hasBracketedPaste()).toThrow();
1293+
});
1294+
1295+
test('alternate screen mode via getMode()', async () => {
1296+
if (typeof document === 'undefined') return;
1297+
const term = new Terminal({ cols: 80, rows: 24 });
1298+
const container = document.createElement('div');
1299+
await term.open(container);
1300+
1301+
expect(term.getMode(1049)).toBe(false);
1302+
term.write('\x1b[?1049h');
1303+
expect(term.getMode(1049)).toBe(true);
1304+
1305+
term.dispose();
1306+
});
1307+
});

lib/terminal.ts

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -353,10 +353,14 @@ export class Terminal implements ITerminalCore {
353353
return;
354354
}
355355

356-
// TODO: Check if terminal has bracketed paste mode enabled
357-
// For now, just send the data directly
358-
// In full implementation: wrap with \x1b[200~ and \x1b[201~
359-
this.dataEmitter.fire(data);
356+
// Check if terminal has bracketed paste mode enabled
357+
if (this.wasmTerm!.hasBracketedPaste()) {
358+
// Wrap with bracketed paste sequences (DEC mode 2004)
359+
this.dataEmitter.fire('\x1b[200~' + data + '\x1b[201~');
360+
} else {
361+
// Send data directly
362+
this.dataEmitter.fire(data);
363+
}
360364
}
361365

362366
/**
@@ -1074,4 +1078,44 @@ export class Terminal implements ITerminalCore {
10741078
}
10751079
}
10761080
}
1081+
1082+
// ============================================================================
1083+
// Terminal Modes
1084+
// ============================================================================
1085+
1086+
/**
1087+
* Query terminal mode state
1088+
*
1089+
* @param mode Mode number (e.g., 2004 for bracketed paste)
1090+
* @param isAnsi True for ANSI modes, false for DEC modes (default: false)
1091+
* @returns true if mode is enabled
1092+
*/
1093+
public getMode(mode: number, isAnsi: boolean = false): boolean {
1094+
this.assertOpen();
1095+
return this.wasmTerm!.getMode(mode, isAnsi);
1096+
}
1097+
1098+
/**
1099+
* Check if bracketed paste mode is enabled
1100+
*/
1101+
public hasBracketedPaste(): boolean {
1102+
this.assertOpen();
1103+
return this.wasmTerm!.hasBracketedPaste();
1104+
}
1105+
1106+
/**
1107+
* Check if focus event reporting is enabled
1108+
*/
1109+
public hasFocusEvents(): boolean {
1110+
this.assertOpen();
1111+
return this.wasmTerm!.hasFocusEvents();
1112+
}
1113+
1114+
/**
1115+
* Check if mouse tracking is enabled
1116+
*/
1117+
public hasMouseTracking(): boolean {
1118+
this.assertOpen();
1119+
return this.wasmTerm!.hasMouseTracking();
1120+
}
10771121
}

lib/types.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,10 @@ export interface GhosttyWasmExports extends WebAssembly.Exports {
377377
bufLen: number
378378
): number;
379379
ghostty_terminal_get_scrollback_length(terminal: TerminalHandle): number;
380+
ghostty_terminal_get_mode(terminal: TerminalHandle, mode: number, isAnsi: number): number;
381+
ghostty_terminal_has_bracketed_paste(terminal: TerminalHandle): number;
382+
ghostty_terminal_has_focus_events(terminal: TerminalHandle): number;
383+
ghostty_terminal_has_mouse_tracking(terminal: TerminalHandle): number;
380384
}
381385

382386
// ============================================================================
@@ -600,3 +604,34 @@ export class EventEmitter<T> {
600604
this.listeners = [];
601605
}
602606
}
607+
608+
/**
609+
* Terminal mode identifiers
610+
*
611+
* ANSI modes (use with is_ansi = true):
612+
* - INSERT = 4
613+
*
614+
* DEC modes (use with is_ansi = false):
615+
* - CURSOR_VISIBLE = 25
616+
* - MOUSE_TRACKING_NORMAL = 1000
617+
* - MOUSE_TRACKING_BUTTON = 1002
618+
* - MOUSE_TRACKING_ANY = 1003
619+
* - FOCUS_EVENTS = 1004
620+
* - ALT_SCREEN = 1047
621+
* - ALT_SCREEN_WITH_CURSOR = 1049
622+
* - BRACKETED_PASTE = 2004
623+
*/
624+
export enum TerminalMode {
625+
// ANSI modes
626+
INSERT = 4,
627+
628+
// DEC modes
629+
CURSOR_VISIBLE = 25,
630+
MOUSE_TRACKING_NORMAL = 1000,
631+
MOUSE_TRACKING_BUTTON = 1002,
632+
MOUSE_TRACKING_ANY = 1003,
633+
FOCUS_EVENTS = 1004,
634+
ALT_SCREEN = 1047,
635+
ALT_SCREEN_WITH_CURSOR = 1049,
636+
BRACKETED_PASTE = 2004,
637+
}

0 commit comments

Comments
 (0)