diff --git a/lib/interfaces.ts b/lib/interfaces.ts index 5b2017d5..e207e7b8 100644 --- a/lib/interfaces.ts +++ b/lib/interfaces.ts @@ -22,6 +22,13 @@ export interface ITerminalOptions { // Scrolling options smoothScrollDuration?: number; // Duration in ms for smooth scroll animation (default: 100, 0 = instant) + // Emit terminal-generated responses through onData (default: true) + // + // Some host applications answer terminal queries at the PTY boundary instead + // of in the renderer. Disable this to keep parser-generated replies, such as + // DSR responses, out of the same stream as user keyboard input. + emitTerminalResponses?: boolean; + // Internal: Ghostty WASM instance (optional, for test isolation) // If not provided, uses the module-level instance from init() ghostty?: Ghostty; diff --git a/lib/terminal.test.ts b/lib/terminal.test.ts index d56011ec..f941cc2e 100644 --- a/lib/terminal.test.ts +++ b/lib/terminal.test.ts @@ -172,6 +172,34 @@ describe('Terminal', () => { disposable.dispose(); }); + test('emits terminal query responses through onData by default', async () => { + const term = await createIsolatedTerminal(); + term.open(container!); + + const receivedData: string[] = []; + term.onData((data) => receivedData.push(data)); + + term.write('\x1b[5n'); + + expect(receivedData).toContain('\x1b[0n'); + + term.dispose(); + }); + + test('can keep terminal query responses out of onData', async () => { + const term = await createIsolatedTerminal({ emitTerminalResponses: false }); + term.open(container!); + + const receivedData: string[] = []; + term.onData((data) => receivedData.push(data)); + + term.write('\x1b[5n'); + + expect(receivedData).toEqual([]); + + term.dispose(); + }); + test('onResize fires when terminal is resized', async () => { const term = await createIsolatedTerminal({ cols: 80, rows: 24 }); term.open(container!); diff --git a/lib/terminal.ts b/lib/terminal.ts index eeb7acd2..d3afab42 100644 --- a/lib/terminal.ts +++ b/lib/terminal.ts @@ -151,6 +151,7 @@ export class Terminal implements ITerminalCore { convertEol: options.convertEol ?? false, disableStdin: options.disableStdin ?? false, smoothScrollDuration: options.smoothScrollDuration ?? 100, // Default: 100ms smooth scroll + emitTerminalResponses: options.emitTerminalResponses ?? true, }; // Wrap in Proxy to intercept runtime changes (xterm.js compatibility) @@ -560,9 +561,12 @@ export class Terminal implements ITerminalCore { // Write directly to WASM terminal (handles VT parsing internally) this.wasmTerm!.write(data); - // Process any responses generated by the terminal (e.g., DSR cursor position) - // These need to be sent back to the PTY via onData - this.processTerminalResponses(); + // Process any responses generated by the terminal (e.g., DSR cursor position). + // These are useful for direct browser PTY integrations, but embedders that + // answer terminal queries server-side can opt out to keep onData user-only. + if (this.options.emitTerminalResponses) { + this.processTerminalResponses(); + } // Check for bell character (BEL, \x07) // WASM doesn't expose bell events, so we detect it in the data stream