import { type GhosttyKeyboardLayoutMap, ghosttyKeyForCode, ghosttyUnshiftedCodepoint, loadGhosttyKeyboardLayoutMap,} from "./keyCodes";import { GhosttyRuntime, loadGhosttyRuntime } from "./runtime"; const GHOSTTY_SUCCESS = 0;const GHOSTTY_OUT_OF_SPACE = -3;const MAX_SCROLLBACK_ROWS = 10_000;// wasm32 C ABI layout for GhosttyTerminalSelectionFormatOptions at the// libghostty-vt revision pinned alongside this module.const SELECTION_FORMAT_OPTIONS_SIZE = 16; const RENDER_DATA = { cols: 1, rows: 2, dirty: 3, rowIterator: 4, background: 5, foreground: 6, cursor: 7, cursorHasValue: 8, cursorStyle: 10, cursorVisible: 11, cursorBlinking: 12, cursorInViewport: 14, cursorX: 15, cursorY: 16,} as const; const ROW_DATA = { dirty: 1, raw: 2, cells: 3,} as const; const CELL_DATA = { raw: 1, style: 2, graphemesLength: 3, graphemes: 4, background: 5, foreground: 6, selected: 7,} as const; const RAW_CELL_DATA = { wide: 3,} as const; export const GHOSTTY_CELL_WIDE = { narrow: 0, wide: 1, spacerTail: 2, spacerHead: 3,} as const; export interface GhosttyColor { readonly r: number; readonly g: number; readonly b: number;} export interface GhosttyTheme { readonly foreground: GhosttyColor; readonly background: GhosttyColor; readonly cursor: GhosttyColor; /** CSS color the renderer overlays on selected cells; not sent to Ghostty. */ readonly selectionBackground?: string;} export interface GhosttyCell { readonly text: string; readonly wide: number; readonly foreground: GhosttyColor; readonly background: GhosttyColor; readonly bold: boolean; readonly italic: boolean; readonly invisible: boolean; readonly strikethrough: boolean; readonly overline: boolean; readonly underline: boolean; readonly selected: boolean;} export interface GhosttyRow { readonly cells: readonly GhosttyCell[]; readonly text: string; readonly isWrapContinuation: boolean; /** Whether this row soft-wraps onto the next row. */ readonly wrapsToNext: boolean;} export interface GhosttySnapshot { readonly cols: number; readonly rows: number; readonly foreground: GhosttyColor; readonly background: GhosttyColor; readonly cursor: GhosttyColor; readonly cursorX: number; readonly cursorY: number; readonly cursorVisible: boolean; readonly cursorBlinking: boolean; readonly cursorStyle: number; readonly dirtyRows: ReadonlySet<number>; readonly rowData: readonly GhosttyRow[];} export interface GhosttySelectionRange { readonly viewport: { readonly start: { readonly x: number; readonly y: number }; readonly end: { readonly x: number; readonly y: number }; }; readonly screen: { readonly start: { readonly x: number; readonly y: number }; readonly end: { readonly x: number; readonly y: number }; };} export interface GhosttyScrollbar { readonly total: number; readonly offset: number; readonly len: number;} /** Grid position tagged with its Ghostty coordinate space: 1 viewport, 2 screen. */export interface GhosttyPointInput { readonly x: number; readonly y: number; readonly tag?: 1 | 2;} export interface GhosttyMouseInput { readonly action: "press" | "release" | "motion"; readonly button: number | null; readonly mods: number; readonly x: number; readonly y: number; readonly screenWidth: number; readonly screenHeight: number; readonly cellWidth: number; readonly cellHeight: number; readonly paddingLeft: number; readonly paddingRight: number; readonly paddingTop: number; readonly paddingBottom: number; readonly anyButtonPressed: boolean;} const decoder = new TextDecoder();const encoder = new TextEncoder(); function blend(foreground: GhosttyColor, background: GhosttyColor): GhosttyColor { const channel = (front: number, back: number) => Math.floor((front * 155 + back * 100) / 255); return { r: channel(foreground.r, background.r), g: channel(foreground.g, background.g), b: channel(foreground.b, background.b), };} function sameColor(left: GhosttyColor, right: GhosttyColor): boolean { return left.r === right.r && left.g === right.g && left.b === right.b;} export class GhosttyTerminalCore { private readonly runtime: GhosttyRuntime; private terminalSlot = 0; private terminal = 0; private renderStateSlot = 0; private renderState = 0; private rowIteratorSlot = 0; private rowCellsSlot = 0; private keyEncoderSlot = 0; private keyEncoder = 0; private keyEventSlot = 0; private keyEvent = 0; private mouseEncoderSlot = 0; private mouseEncoder = 0; private mouseEventSlot = 0; private mouseEvent = 0; private ptyWriterId = 0; private ptyWriter: ((data: string) => void) | null = null; private scratch = 0; private style = 0; private scrollbar = 0; private rows: GhosttyRow[] = []; private disposed = false; private keyboardLayoutMap: GhosttyKeyboardLayoutMap | undefined; private constructor(runtime: GhosttyRuntime) { this.runtime = runtime; void loadGhosttyKeyboardLayoutMap().then((layoutMap) => { if (!this.disposed) this.keyboardLayoutMap = layoutMap; }); } static async create( cols: number, rows: number, cellWidth: number, cellHeight: number, theme: GhosttyTheme, onPtyData: (data: string) => void, ): Promise<GhosttyTerminalCore> { const core = new GhosttyTerminalCore(await loadGhosttyRuntime()); try { core.initialize(cols, rows, cellWidth, cellHeight, theme, onPtyData); return core; } catch (error) { core.dispose(); throw error; } } private initialize( cols: number, rows: number, cellWidth: number, cellHeight: number, theme: GhosttyTheme, onPtyData: (data: string) => void, ): void { const optionsSize = this.runtime.layout("GhosttyTerminalOptions").size; const options = this.runtime.alloc(optionsSize); this.runtime.setField(options, "GhosttyTerminalOptions", "cols", cols); this.runtime.setField(options, "GhosttyTerminalOptions", "rows", rows); this.runtime.setField(options, "GhosttyTerminalOptions", "max_scrollback", MAX_SCROLLBACK_ROWS); this.terminalSlot = this.runtime.allocOpaque(); const terminalResult = this.runtime.call("ghostty_terminal_new", 0, this.terminalSlot, options); this.runtime.free(options, optionsSize); this.assertSuccess("ghostty_terminal_new", terminalResult); this.terminal = this.runtime.readPointer(this.terminalSlot); this.applyDefaultCursorBlink(); this.ptyWriter = onPtyData; this.ptyWriterId = this.runtime.attachPtyWriter(this.terminal, onPtyData); this.renderStateSlot = this.runtime.allocOpaque(); this.assertSuccess( "ghostty_render_state_new", this.runtime.call("ghostty_render_state_new", 0, this.renderStateSlot), ); this.renderState = this.runtime.readPointer(this.renderStateSlot); this.rowIteratorSlot = this.runtime.allocOpaque(); this.assertSuccess( "ghostty_render_state_row_iterator_new", this.runtime.call("ghostty_render_state_row_iterator_new", 0, this.rowIteratorSlot), ); this.rowCellsSlot = this.runtime.allocOpaque(); this.assertSuccess( "ghostty_render_state_row_cells_new", this.runtime.call("ghostty_render_state_row_cells_new", 0, this.rowCellsSlot), ); this.keyEncoderSlot = this.runtime.allocOpaque(); this.assertSuccess( "ghostty_key_encoder_new", this.runtime.call("ghostty_key_encoder_new", 0, this.keyEncoderSlot), ); this.keyEncoder = this.runtime.readPointer(this.keyEncoderSlot); this.keyEventSlot = this.runtime.allocOpaque(); this.assertSuccess( "ghostty_key_event_new", this.runtime.call("ghostty_key_event_new", 0, this.keyEventSlot), ); this.keyEvent = this.runtime.readPointer(this.keyEventSlot); this.mouseEncoderSlot = this.runtime.allocOpaque(); this.assertSuccess( "ghostty_mouse_encoder_new", this.runtime.call("ghostty_mouse_encoder_new", 0, this.mouseEncoderSlot), ); this.mouseEncoder = this.runtime.readPointer(this.mouseEncoderSlot); this.mouseEventSlot = this.runtime.allocOpaque(); this.assertSuccess( "ghostty_mouse_event_new", this.runtime.call("ghostty_mouse_event_new", 0, this.mouseEventSlot), ); this.mouseEvent = this.runtime.readPointer(this.mouseEventSlot); this.scratch = this.runtime.alloc(16); const styleSize = this.runtime.layout("GhosttyStyle").size; this.style = this.runtime.alloc(styleSize); this.runtime.setField(this.style, "GhosttyStyle", "size", styleSize); this.scrollbar = this.runtime.alloc(this.runtime.layout("GhosttyTerminalScrollbar").size); this.setTheme(theme); this.resize(cols, rows, cellWidth, cellHeight); } write(data: string | Uint8Array): void { this.ensureActive(); const bytes = typeof data === "string" ? encoder.encode(data) : data; if (bytes.length === 0) return; const pointer = this.runtime.alloc(bytes.length); this.runtime.bytes(pointer, bytes.length).set(bytes); this.runtime.call("ghostty_terminal_vt_write", this.terminal, pointer, bytes.length); this.runtime.free(pointer, bytes.length); } resetAndWrite(data: string): void { this.ensureActive(); this.runtime.call("ghostty_terminal_reset", this.terminal); // RIS returns the cursor to Ghostty's built-in steady default, so the // embedder default has to be applied again before the replay runs. this.applyDefaultCursorBlink(); this.rows = []; if (data.length === 0) return; const writer = this.ptyWriter; if (this.ptyWriterId !== 0) { this.runtime.detachPtyWriter(this.terminal, this.ptyWriterId); this.ptyWriterId = 0; } try { this.write(data); } finally { if (writer !== null && !this.disposed) { this.ptyWriterId = this.runtime.attachPtyWriter(this.terminal, writer); } } } resize(cols: number, rows: number, cellWidth: number, cellHeight: number): void { this.ensureActive(); this.assertSuccess( "ghostty_terminal_resize", this.runtime.call( "ghostty_terminal_resize", this.terminal, Math.max(1, Math.min(65_535, cols)), Math.max(1, Math.min(65_535, rows)), Math.max(1, Math.round(cellWidth)), Math.max(1, Math.round(cellHeight)), ), ); } /** * Ghostty's built-in default cursor is steady, while the xterm.js renderer * this replaced ran with `cursorBlink: true`. Option 23 is the embedder's * default blink, which is the state a session starts in and returns to on * DECSCUSR reset (CSI 0 q), so programs that ask for a specific cursor * through DECSCUSR or DEC mode 12 still win. */ private applyDefaultCursorBlink(): void { const blink = this.runtime.alloc(1); this.runtime.bytes(blink, 1)[0] = 1; this.runtime.call("ghostty_terminal_set", this.terminal, 23, blink); this.runtime.free(blink, 1); } setTheme(theme: GhosttyTheme): void { this.ensureActive(); const color = this.runtime.alloc(3); for (const [option, value] of [ [11, theme.foreground], [12, theme.background], [13, theme.cursor], ] as const) { this.runtime.bytes(color, 3).set([value.r, value.g, value.b]); this.runtime.call("ghostty_terminal_set", this.terminal, option, color); } this.runtime.free(color, 3); } scroll(deltaRows: number): void { this.ensureActive(); const layout = this.runtime.layout("GhosttyTerminalScrollViewport"); const scroll = this.runtime.alloc(layout.size); this.runtime.setField(scroll, "GhosttyTerminalScrollViewport", "tag", 2); const value = layout.fields.value!; this.runtime.view(scroll + value.offset, value.size).setInt32(0, deltaRows, true); this.runtime.call("ghostty_terminal_scroll_viewport", this.terminal, scroll); this.runtime.free(scroll, layout.size); } scrollToBottom(): void { this.ensureActive(); const layout = this.runtime.layout("GhosttyTerminalScrollViewport"); const scroll = this.runtime.alloc(layout.size); this.runtime.setField(scroll, "GhosttyTerminalScrollViewport", "tag", 1); this.runtime.call("ghostty_terminal_scroll_viewport", this.terminal, scroll); this.runtime.free(scroll, layout.size); } isViewportActive(): boolean { this.ensureActive(); this.runtime.bytes(this.scratch, 1)[0] = 0; return ( this.runtime.call("ghostty_terminal_get", this.terminal, 32, this.scratch) === GHOSTTY_SUCCESS && this.runtime.bytes(this.scratch, 1)[0] !== 0 ); } scrollbarState(): GhosttyScrollbar | null { this.ensureActive(); const layout = this.runtime.layout("GhosttyTerminalScrollbar"); this.runtime.bytes(this.scrollbar, layout.size).fill(0); if ( this.runtime.call("ghostty_terminal_get", this.terminal, 9, this.scrollbar) !== GHOSTTY_SUCCESS ) { return null; } return { total: this.runtime.readField(this.scrollbar, "GhosttyTerminalScrollbar", "total"), offset: this.runtime.readField(this.scrollbar, "GhosttyTerminalScrollbar", "offset"), len: this.runtime.readField(this.scrollbar, "GhosttyTerminalScrollbar", "len"), }; } isMouseTracking(): boolean { this.ensureActive(); this.runtime.bytes(this.scratch, 1)[0] = 0; return ( this.runtime.call("ghostty_terminal_get", this.terminal, 11, this.scratch) === GHOSTTY_SUCCESS && this.runtime.bytes(this.scratch, 1)[0] !== 0 ); } isMouseAnyEventTracking(): boolean { this.ensureActive(); this.runtime.bytes(this.scratch, 1)[0] = 0; return ( this.runtime.call("ghostty_terminal_mode_get", this.terminal, 1003, this.scratch) === GHOSTTY_SUCCESS && this.runtime.bytes(this.scratch, 1)[0] !== 0 ); } isAlternateScreen(): boolean { this.ensureActive(); this.runtime.bytes(this.scratch, 4).fill(0); return ( this.runtime.call("ghostty_terminal_get", this.terminal, 6, this.scratch) === GHOSTTY_SUCCESS && this.runtime.view(this.scratch, 4).getUint32(0, true) === 1 ); } isApplicationCursorKeys(): boolean { this.ensureActive(); this.runtime.bytes(this.scratch, 1)[0] = 0; return ( this.runtime.call("ghostty_terminal_mode_get", this.terminal, 1, this.scratch) === GHOSTTY_SUCCESS && this.runtime.bytes(this.scratch, 1)[0] !== 0 ); } encodeKey(event: KeyboardEvent, action: "press" | "release" = "press"): string { this.ensureActive(); this.runtime.call("ghostty_key_encoder_setopt_from_terminal", this.keyEncoder, this.terminal); this.runtime.call( "ghostty_key_event_set_action", this.keyEvent, action === "release" ? 0 : event.repeat ? 2 : 1, ); this.runtime.call("ghostty_key_event_set_key", this.keyEvent, ghosttyKeyForCode(event.code)); const mods = (event.shiftKey ? 1 : 0) | (event.ctrlKey ? 1 << 1 : 0) | (event.altKey ? 1 << 2 : 0) | (event.metaKey ? 1 << 3 : 0) | (event.getModifierState("CapsLock") ? 1 << 4 : 0) | (event.getModifierState("NumLock") ? 1 << 5 : 0); this.runtime.call("ghostty_key_event_set_mods", this.keyEvent, mods); this.runtime.call("ghostty_key_event_set_consumed_mods", this.keyEvent, 0); this.runtime.call("ghostty_key_event_set_composing", this.keyEvent, event.isComposing ? 1 : 0); this.runtime.call( "ghostty_key_event_set_unshifted_codepoint", this.keyEvent, ghosttyUnshiftedCodepoint(event, this.keyboardLayoutMap), ); const text = event.key.length === 1 ? event.key : ""; const textBytes = encoder.encode(text); const textPointer = textBytes.length === 0 ? 0 : this.runtime.alloc(textBytes.length); if (textPointer !== 0) this.runtime.bytes(textPointer, textBytes.length).set(textBytes); this.runtime.call("ghostty_key_event_set_utf8", this.keyEvent, textPointer, textBytes.length); const written = this.runtime.call("ghostty_wasm_alloc_usize"); const encoded = this.encodeOutput(written, (output, outputSize) => this.runtime.call( "ghostty_key_encoder_encode", this.keyEncoder, this.keyEvent, output, outputSize, written, ), ); this.runtime.call("ghostty_wasm_free_usize", written); if (textPointer !== 0) this.runtime.free(textPointer, textBytes.length); return encoded; } encodePaste(data: string): string { this.ensureActive(); const input = encoder.encode(data); if (input.length === 0) return "";Lines 1–500 of 1207. Load more
import { type GhosttyKeyboardLayoutMap, ghosttyKeyForCode, ghosttyUnshiftedCodepoint, loadGhosttyKeyboardLayoutMap, } from "./keyCodes"; import { GhosttyRuntime, loadGhosttyRuntime } from "./runtime"; const GHOSTTY_SUCCESS = 0; const GHOSTTY_OUT_OF_SPACE = -3; const MAX_SCROLLBACK_ROWS = 10_000; // wasm32 C ABI layout for GhosttyTerminalSelectionFormatOptions at the // libghostty-vt revision pinned alongside this module. const SELECTION_FORMAT_OPTIONS_SIZE = 16; const RENDER_DATA = { cols: 1, rows: 2, dirty: 3, rowIterator: 4, background: 5, foreground: 6, cursor: 7, cursorHasValue: 8, cursorStyle: 10, cursorVisible: 11, cursorBlinking: 12, cursorInViewport: 14, cursorX: 15, cursorY: 16, } as const; const ROW_DATA = { dirty: 1, raw: 2, cells: 3, } as const; const CELL_DATA = { raw: 1, style: 2, graphemesLength: 3, graphemes: 4, background: 5, foreground: 6, selected: 7, } as const; const RAW_CELL_DATA = { wide: 3, } as const; export const GHOSTTY_CELL_WIDE = { narrow: 0, wide: 1, spacerTail: 2, spacerHead: 3, } as const; export interface GhosttyColor { readonly r: number; readonly g: number; readonly b: number; } export interface GhosttyTheme { readonly foreground: GhosttyColor; readonly background: GhosttyColor; readonly cursor: GhosttyColor; /** CSS color the renderer overlays on selected cells; not sent to Ghostty. */ readonly selectionBackground?: string; } export interface GhosttyCell { readonly text: string; readonly wide: number; readonly foreground: GhosttyColor; readonly background: GhosttyColor; readonly bold: boolean; readonly italic: boolean; readonly invisible: boolean; readonly strikethrough: boolean; readonly overline: boolean; readonly underline: boolean; readonly selected: boolean; } export interface GhosttyRow { readonly cells: readonly GhosttyCell[]; readonly text: string; readonly isWrapContinuation: boolean; /** Whether this row soft-wraps onto the next row. */ readonly wrapsToNext: boolean; } export interface GhosttySnapshot { readonly cols: number; readonly rows: number; readonly foreground: GhosttyColor; readonly background: GhosttyColor; readonly cursor: GhosttyColor; readonly cursorX: number; readonly cursorY: number; readonly cursorVisible: boolean; readonly cursorBlinking: boolean; readonly cursorStyle: number; readonly dirtyRows: ReadonlySet<number>; readonly rowData: readonly GhosttyRow[]; } export interface GhosttySelectionRange { readonly viewport: { readonly start: { readonly x: number; readonly y: number }; readonly end: { readonly x: number; readonly y: number }; }; readonly screen: { readonly start: { readonly x: number; readonly y: number }; readonly end: { readonly x: number; readonly y: number }; }; } export interface GhosttyScrollbar { readonly total: number; readonly offset: number; readonly len: number; } /** Grid position tagged with its Ghostty coordinate space: 1 viewport, 2 screen. */ export interface GhosttyPointInput { readonly x: number; readonly y: number; readonly tag?: 1 | 2; } export interface GhosttyMouseInput { readonly action: "press" | "release" | "motion"; readonly button: number | null; readonly mods: number; readonly x: number; readonly y: number; readonly screenWidth: number; readonly screenHeight: number; readonly cellWidth: number; readonly cellHeight: number; readonly paddingLeft: number; readonly paddingRight: number; readonly paddingTop: number; readonly paddingBottom: number; readonly anyButtonPressed: boolean; } const decoder = new TextDecoder(); const encoder = new TextEncoder(); function blend(foreground: GhosttyColor, background: GhosttyColor): GhosttyColor { const channel = (front: number, back: number) => Math.floor((front * 155 + back * 100) / 255); return { r: channel(foreground.r, background.r), g: channel(foreground.g, background.g), b: channel(foreground.b, background.b), }; } function sameColor(left: GhosttyColor, right: GhosttyColor): boolean { return left.r === right.r && left.g === right.g && left.b === right.b; } export class GhosttyTerminalCore { private readonly runtime: GhosttyRuntime; private terminalSlot = 0; private terminal = 0; private renderStateSlot = 0; private renderState = 0; private rowIteratorSlot = 0; private rowCellsSlot = 0; private keyEncoderSlot = 0; private keyEncoder = 0; private keyEventSlot = 0; private keyEvent = 0; private mouseEncoderSlot = 0; private mouseEncoder = 0; private mouseEventSlot = 0; private mouseEvent = 0; private ptyWriterId = 0; private ptyWriter: ((data: string) => void) | null = null; private scratch = 0; private style = 0; private scrollbar = 0; private rows: GhosttyRow[] = []; private disposed = false; private keyboardLayoutMap: GhosttyKeyboardLayoutMap | undefined; private constructor(runtime: GhosttyRuntime) { this.runtime = runtime; void loadGhosttyKeyboardLayoutMap().then((layoutMap) => { if (!this.disposed) this.keyboardLayoutMap = layoutMap; }); } static async create( cols: number, rows: number, cellWidth: number, cellHeight: number, theme: GhosttyTheme, onPtyData: (data: string) => void, ): Promise<GhosttyTerminalCore> { const core = new GhosttyTerminalCore(await loadGhosttyRuntime()); try { core.initialize(cols, rows, cellWidth, cellHeight, theme, onPtyData); return core; } catch (error) { core.dispose(); throw error; } } private initialize( cols: number, rows: number, cellWidth: number, cellHeight: number, theme: GhosttyTheme, onPtyData: (data: string) => void, ): void { const optionsSize = this.runtime.layout("GhosttyTerminalOptions").size; const options = this.runtime.alloc(optionsSize); this.runtime.setField(options, "GhosttyTerminalOptions", "cols", cols); this.runtime.setField(options, "GhosttyTerminalOptions", "rows", rows); this.runtime.setField(options, "GhosttyTerminalOptions", "max_scrollback", MAX_SCROLLBACK_ROWS); this.terminalSlot = this.runtime.allocOpaque(); const terminalResult = this.runtime.call("ghostty_terminal_new", 0, this.terminalSlot, options); this.runtime.free(options, optionsSize); this.assertSuccess("ghostty_terminal_new", terminalResult); this.terminal = this.runtime.readPointer(this.terminalSlot); this.applyDefaultCursorBlink(); this.ptyWriter = onPtyData; this.ptyWriterId = this.runtime.attachPtyWriter(this.terminal, onPtyData); this.renderStateSlot = this.runtime.allocOpaque(); this.assertSuccess( "ghostty_render_state_new", this.runtime.call("ghostty_render_state_new", 0, this.renderStateSlot), ); this.renderState = this.runtime.readPointer(this.renderStateSlot); this.rowIteratorSlot = this.runtime.allocOpaque(); this.assertSuccess( "ghostty_render_state_row_iterator_new", this.runtime.call("ghostty_render_state_row_iterator_new", 0, this.rowIteratorSlot), ); this.rowCellsSlot = this.runtime.allocOpaque(); this.assertSuccess( "ghostty_render_state_row_cells_new", this.runtime.call("ghostty_render_state_row_cells_new", 0, this.rowCellsSlot), ); this.keyEncoderSlot = this.runtime.allocOpaque(); this.assertSuccess( "ghostty_key_encoder_new", this.runtime.call("ghostty_key_encoder_new", 0, this.keyEncoderSlot), ); this.keyEncoder = this.runtime.readPointer(this.keyEncoderSlot); this.keyEventSlot = this.runtime.allocOpaque(); this.assertSuccess( "ghostty_key_event_new", this.runtime.call("ghostty_key_event_new", 0, this.keyEventSlot), ); this.keyEvent = this.runtime.readPointer(this.keyEventSlot); this.mouseEncoderSlot = this.runtime.allocOpaque(); this.assertSuccess( "ghostty_mouse_encoder_new", this.runtime.call("ghostty_mouse_encoder_new", 0, this.mouseEncoderSlot), ); this.mouseEncoder = this.runtime.readPointer(this.mouseEncoderSlot); this.mouseEventSlot = this.runtime.allocOpaque(); this.assertSuccess( "ghostty_mouse_event_new", this.runtime.call("ghostty_mouse_event_new", 0, this.mouseEventSlot), ); this.mouseEvent = this.runtime.readPointer(this.mouseEventSlot); this.scratch = this.runtime.alloc(16); const styleSize = this.runtime.layout("GhosttyStyle").size; this.style = this.runtime.alloc(styleSize); this.runtime.setField(this.style, "GhosttyStyle", "size", styleSize); this.scrollbar = this.runtime.alloc(this.runtime.layout("GhosttyTerminalScrollbar").size); this.setTheme(theme); this.resize(cols, rows, cellWidth, cellHeight); } write(data: string | Uint8Array): void { this.ensureActive(); const bytes = typeof data === "string" ? encoder.encode(data) : data; if (bytes.length === 0) return; const pointer = this.runtime.alloc(bytes.length); this.runtime.bytes(pointer, bytes.length).set(bytes); this.runtime.call("ghostty_terminal_vt_write", this.terminal, pointer, bytes.length); this.runtime.free(pointer, bytes.length); } resetAndWrite(data: string): void { this.ensureActive(); this.runtime.call("ghostty_terminal_reset", this.terminal); // RIS returns the cursor to Ghostty's built-in steady default, so the // embedder default has to be applied again before the replay runs. this.applyDefaultCursorBlink(); this.rows = []; if (data.length === 0) return; const writer = this.ptyWriter; if (this.ptyWriterId !== 0) { this.runtime.detachPtyWriter(this.terminal, this.ptyWriterId); this.ptyWriterId = 0; } try { this.write(data); } finally { if (writer !== null && !this.disposed) { this.ptyWriterId = this.runtime.attachPtyWriter(this.terminal, writer); } } } resize(cols: number, rows: number, cellWidth: number, cellHeight: number): void { this.ensureActive(); this.assertSuccess( "ghostty_terminal_resize", this.runtime.call( "ghostty_terminal_resize", this.terminal, Math.max(1, Math.min(65_535, cols)), Math.max(1, Math.min(65_535, rows)), Math.max(1, Math.round(cellWidth)), Math.max(1, Math.round(cellHeight)), ), ); } /** * Ghostty's built-in default cursor is steady, while the xterm.js renderer * this replaced ran with `cursorBlink: true`. Option 23 is the embedder's * default blink, which is the state a session starts in and returns to on * DECSCUSR reset (CSI 0 q), so programs that ask for a specific cursor * through DECSCUSR or DEC mode 12 still win. */ private applyDefaultCursorBlink(): void { const blink = this.runtime.alloc(1); this.runtime.bytes(blink, 1)[0] = 1; this.runtime.call("ghostty_terminal_set", this.terminal, 23, blink); this.runtime.free(blink, 1); } setTheme(theme: GhosttyTheme): void { this.ensureActive(); const color = this.runtime.alloc(3); for (const [option, value] of [ [11, theme.foreground], [12, theme.background], [13, theme.cursor], ] as const) { this.runtime.bytes(color, 3).set([value.r, value.g, value.b]); this.runtime.call("ghostty_terminal_set", this.terminal, option, color); } this.runtime.free(color, 3); } scroll(deltaRows: number): void { this.ensureActive(); const layout = this.runtime.layout("GhosttyTerminalScrollViewport"); const scroll = this.runtime.alloc(layout.size); this.runtime.setField(scroll, "GhosttyTerminalScrollViewport", "tag", 2); const value = layout.fields.value!; this.runtime.view(scroll + value.offset, value.size).setInt32(0, deltaRows, true); this.runtime.call("ghostty_terminal_scroll_viewport", this.terminal, scroll); this.runtime.free(scroll, layout.size); } scrollToBottom(): void { this.ensureActive(); const layout = this.runtime.layout("GhosttyTerminalScrollViewport"); const scroll = this.runtime.alloc(layout.size); this.runtime.setField(scroll, "GhosttyTerminalScrollViewport", "tag", 1); this.runtime.call("ghostty_terminal_scroll_viewport", this.terminal, scroll); this.runtime.free(scroll, layout.size); } isViewportActive(): boolean { this.ensureActive(); this.runtime.bytes(this.scratch, 1)[0] = 0; return ( this.runtime.call("ghostty_terminal_get", this.terminal, 32, this.scratch) === GHOSTTY_SUCCESS && this.runtime.bytes(this.scratch, 1)[0] !== 0 ); } scrollbarState(): GhosttyScrollbar | null { this.ensureActive(); const layout = this.runtime.layout("GhosttyTerminalScrollbar"); this.runtime.bytes(this.scrollbar, layout.size).fill(0); if ( this.runtime.call("ghostty_terminal_get", this.terminal, 9, this.scrollbar) !== GHOSTTY_SUCCESS ) { return null; } return { total: this.runtime.readField(this.scrollbar, "GhosttyTerminalScrollbar", "total"), offset: this.runtime.readField(this.scrollbar, "GhosttyTerminalScrollbar", "offset"), len: this.runtime.readField(this.scrollbar, "GhosttyTerminalScrollbar", "len"), }; } isMouseTracking(): boolean { this.ensureActive(); this.runtime.bytes(this.scratch, 1)[0] = 0; return ( this.runtime.call("ghostty_terminal_get", this.terminal, 11, this.scratch) === GHOSTTY_SUCCESS && this.runtime.bytes(this.scratch, 1)[0] !== 0 ); } isMouseAnyEventTracking(): boolean { this.ensureActive(); this.runtime.bytes(this.scratch, 1)[0] = 0; return ( this.runtime.call("ghostty_terminal_mode_get", this.terminal, 1003, this.scratch) === GHOSTTY_SUCCESS && this.runtime.bytes(this.scratch, 1)[0] !== 0 ); } isAlternateScreen(): boolean { this.ensureActive(); this.runtime.bytes(this.scratch, 4).fill(0); return ( this.runtime.call("ghostty_terminal_get", this.terminal, 6, this.scratch) === GHOSTTY_SUCCESS && this.runtime.view(this.scratch, 4).getUint32(0, true) === 1 ); } isApplicationCursorKeys(): boolean { this.ensureActive(); this.runtime.bytes(this.scratch, 1)[0] = 0; return ( this.runtime.call("ghostty_terminal_mode_get", this.terminal, 1, this.scratch) === GHOSTTY_SUCCESS && this.runtime.bytes(this.scratch, 1)[0] !== 0 ); } encodeKey(event: KeyboardEvent, action: "press" | "release" = "press"): string { this.ensureActive(); this.runtime.call("ghostty_key_encoder_setopt_from_terminal", this.keyEncoder, this.terminal); this.runtime.call( "ghostty_key_event_set_action", this.keyEvent, action === "release" ? 0 : event.repeat ? 2 : 1, ); this.runtime.call("ghostty_key_event_set_key", this.keyEvent, ghosttyKeyForCode(event.code)); const mods = (event.shiftKey ? 1 : 0) | (event.ctrlKey ? 1 << 1 : 0) | (event.altKey ? 1 << 2 : 0) | (event.metaKey ? 1 << 3 : 0) | (event.getModifierState("CapsLock") ? 1 << 4 : 0) | (event.getModifierState("NumLock") ? 1 << 5 : 0); this.runtime.call("ghostty_key_event_set_mods", this.keyEvent, mods); this.runtime.call("ghostty_key_event_set_consumed_mods", this.keyEvent, 0); this.runtime.call("ghostty_key_event_set_composing", this.keyEvent, event.isComposing ? 1 : 0); this.runtime.call( "ghostty_key_event_set_unshifted_codepoint", this.keyEvent, ghosttyUnshiftedCodepoint(event, this.keyboardLayoutMap), ); const text = event.key.length === 1 ? event.key : ""; const textBytes = encoder.encode(text); const textPointer = textBytes.length === 0 ? 0 : this.runtime.alloc(textBytes.length); if (textPointer !== 0) this.runtime.bytes(textPointer, textBytes.length).set(textBytes); this.runtime.call("ghostty_key_event_set_utf8", this.keyEvent, textPointer, textBytes.length); const written = this.runtime.call("ghostty_wasm_alloc_usize"); const encoded = this.encodeOutput(written, (output, outputSize) => this.runtime.call( "ghostty_key_encoder_encode", this.keyEncoder, this.keyEvent, output, outputSize, written, ), ); this.runtime.call("ghostty_wasm_free_usize", written); if (textPointer !== 0) this.runtime.free(textPointer, textBytes.length); return encoded; } encodePaste(data: string): string { this.ensureActive(); const input = encoder.encode(data); if (input.length === 0) return "";