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
32 changes: 32 additions & 0 deletions lib/ghostty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -639,4 +639,36 @@ export class GhosttyTerminal {
this.exports.ghostty_wasm_free_u8_array(bufferPtr, maxUriLen);
}
}

// ============================================================================
// Terminal Modes
// ============================================================================

/**
* Query terminal mode state
*/
getMode(mode: number, isAnsi: boolean = false): boolean {
return this.exports.ghostty_terminal_get_mode(this.handle, mode, isAnsi ? 1 : 0) !== 0;
}

/**
* Check if bracketed paste mode is enabled
*/
hasBracketedPaste(): boolean {
return this.exports.ghostty_terminal_has_bracketed_paste(this.handle) !== 0;
}

/**
* Check if focus event reporting is enabled
*/
hasFocusEvents(): boolean {
return this.exports.ghostty_terminal_has_focus_events(this.handle) !== 0;
}

/**
* Check if mouse tracking is enabled
*/
hasMouseTracking(): boolean {
return this.exports.ghostty_terminal_has_mouse_tracking(this.handle) !== 0;
}
}
130 changes: 128 additions & 2 deletions lib/terminal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1018,8 +1018,7 @@ describe('attachCustomKeyEventHandler()', () => {
await term.open(container);

const handler = (e: KeyboardEvent) => false;
term.attachCustomKeyEventHandler(handler);
expect(() => term.attachCustomKeyEventHandler(undefined)).not.toThrow();
expect(() => term.attachCustomKeyEventHandler(handler)).not.toThrow();
term.dispose();
});
});
Expand Down Expand Up @@ -1179,3 +1178,130 @@ describe('Buffer Access API', () => {
expect(term.wasmTerm?.isRowWrapped(999)).toBe(false);
});
});

describe('Terminal Modes', () => {
test('should detect bracketed paste mode', async () => {
if (typeof document === 'undefined') return;
const term = new Terminal({ cols: 80, rows: 24 });
const container = document.createElement('div');
await term.open(container);

expect(term.hasBracketedPaste()).toBe(false);
term.write('\x1b[?2004h');
expect(term.hasBracketedPaste()).toBe(true);
term.write('\x1b[?2004l');
expect(term.hasBracketedPaste()).toBe(false);

term.dispose();
});

test('paste() should use bracketed paste when enabled', async () => {
if (typeof document === 'undefined') return;
const term = new Terminal({ cols: 80, rows: 24 });
const container = document.createElement('div');
await term.open(container);

let receivedData = '';
term.onData((data) => {
receivedData = data;
});

term.paste('test');
expect(receivedData).toBe('test');

term.write('\x1b[?2004h');
term.paste('test2');
expect(receivedData).toBe('\x1b[200~test2\x1b[201~');

term.dispose();
});

test('should query arbitrary DEC modes', async () => {
if (typeof document === 'undefined') return;
const term = new Terminal({ cols: 80, rows: 24 });
const container = document.createElement('div');
await term.open(container);

expect(term.getMode(25)).toBe(true); // Cursor visible
term.write('\x1b[?25l');
expect(term.getMode(25)).toBe(false);

term.dispose();
});

test('should detect focus event mode', async () => {
if (typeof document === 'undefined') return;
const term = new Terminal({ cols: 80, rows: 24 });
const container = document.createElement('div');
await term.open(container);

expect(term.hasFocusEvents()).toBe(false);
term.write('\x1b[?1004h');
expect(term.hasFocusEvents()).toBe(true);

term.dispose();
});

test('should detect mouse tracking modes', async () => {
if (typeof document === 'undefined') return;
const term = new Terminal({ cols: 80, rows: 24 });
const container = document.createElement('div');
await term.open(container);

expect(term.hasMouseTracking()).toBe(false);
term.write('\x1b[?1000h');
expect(term.hasMouseTracking()).toBe(true);

term.dispose();
});

test('should query ANSI modes vs DEC modes', async () => {
if (typeof document === 'undefined') return;
const term = new Terminal({ cols: 80, rows: 24 });
const container = document.createElement('div');
await term.open(container);

expect(term.getMode(4, true)).toBe(false); // Insert mode
term.write('\x1b[4h');
expect(term.getMode(4, true)).toBe(true);

term.dispose();
});

test('should handle multiple modes set simultaneously', async () => {
if (typeof document === 'undefined') return;
const term = new Terminal({ cols: 80, rows: 24 });
const container = document.createElement('div');
await term.open(container);

term.write('\x1b[?2004h\x1b[?1004h\x1b[?1000h');
expect(term.hasBracketedPaste()).toBe(true);
expect(term.hasFocusEvents()).toBe(true);
expect(term.hasMouseTracking()).toBe(true);

term.dispose();
});

test('getMode() throws when terminal not open', () => {
const term = new Terminal({ cols: 80, rows: 24 });
expect(() => term.getMode(25)).toThrow();
});

test('hasBracketedPaste() throws when terminal not open', () => {
const term = new Terminal({ cols: 80, rows: 24 });
expect(() => term.hasBracketedPaste()).toThrow();
});

test('alternate screen mode via getMode()', async () => {
if (typeof document === 'undefined') return;
const term = new Terminal({ cols: 80, rows: 24 });
const container = document.createElement('div');
await term.open(container);

expect(term.getMode(1049)).toBe(false);
term.write('\x1b[?1049h');
expect(term.getMode(1049)).toBe(true);

term.dispose();
});
});
52 changes: 48 additions & 4 deletions lib/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,10 +353,14 @@ export class Terminal implements ITerminalCore {
return;
}

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

/**
Expand Down Expand Up @@ -1074,4 +1078,44 @@ export class Terminal implements ITerminalCore {
}
}
}

// ============================================================================
// Terminal Modes
// ============================================================================

/**
* Query terminal mode state
*
* @param mode Mode number (e.g., 2004 for bracketed paste)
* @param isAnsi True for ANSI modes, false for DEC modes (default: false)
* @returns true if mode is enabled
*/
public getMode(mode: number, isAnsi: boolean = false): boolean {
this.assertOpen();
return this.wasmTerm!.getMode(mode, isAnsi);
}

/**
* Check if bracketed paste mode is enabled
*/
public hasBracketedPaste(): boolean {
this.assertOpen();
return this.wasmTerm!.hasBracketedPaste();
}

/**
* Check if focus event reporting is enabled
*/
public hasFocusEvents(): boolean {
this.assertOpen();
return this.wasmTerm!.hasFocusEvents();
}

/**
* Check if mouse tracking is enabled
*/
public hasMouseTracking(): boolean {
this.assertOpen();
return this.wasmTerm!.hasMouseTracking();
}
}
35 changes: 35 additions & 0 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,10 @@ export interface GhosttyWasmExports extends WebAssembly.Exports {
bufLen: number
): number;
ghostty_terminal_get_scrollback_length(terminal: TerminalHandle): number;
ghostty_terminal_get_mode(terminal: TerminalHandle, mode: number, isAnsi: number): number;
ghostty_terminal_has_bracketed_paste(terminal: TerminalHandle): number;
ghostty_terminal_has_focus_events(terminal: TerminalHandle): number;
ghostty_terminal_has_mouse_tracking(terminal: TerminalHandle): number;
}

// ============================================================================
Expand Down Expand Up @@ -600,3 +604,34 @@ export class EventEmitter<T> {
this.listeners = [];
}
}

/**
* Terminal mode identifiers
*
* ANSI modes (use with is_ansi = true):
* - INSERT = 4
*
* DEC modes (use with is_ansi = false):
* - CURSOR_VISIBLE = 25
* - MOUSE_TRACKING_NORMAL = 1000
* - MOUSE_TRACKING_BUTTON = 1002
* - MOUSE_TRACKING_ANY = 1003
* - FOCUS_EVENTS = 1004
* - ALT_SCREEN = 1047
* - ALT_SCREEN_WITH_CURSOR = 1049
* - BRACKETED_PASTE = 2004
*/
export enum TerminalMode {
// ANSI modes
INSERT = 4,

// DEC modes
CURSOR_VISIBLE = 25,
MOUSE_TRACKING_NORMAL = 1000,
MOUSE_TRACKING_BUTTON = 1002,
MOUSE_TRACKING_ANY = 1003,
FOCUS_EVENTS = 1004,
ALT_SCREEN = 1047,
ALT_SCREEN_WITH_CURSOR = 1049,
BRACKETED_PASTE = 2004,
}
Loading