import { describe, expect, it } from "vite-plus/test"; import wasmDataUrl from "./vendor/ghostty-vt.wasm?inline";import writePtyWasmDataUrl from "./vendor/ghostty-write-pty.wasm?inline";import pinnedVersion from "../../../../../native/libghostty-vt/VERSION?raw";import { ghosttyKeyForCode } from "./keyCodes"; type WasmFunction = (...args: number[]) => number; function decodeWasmDataUrl(dataUrl: string): Uint8Array { const encoded = dataUrl.split(",", 2)[1]; if (!encoded) throw new Error("The vendored Ghostty WASM data URL is invalid"); return Uint8Array.from(atob(encoded), (character) => character.charCodeAt(0));} describe("vendored libghostty-vt WebAssembly", () => { it("stays pinned to the canonical revision and size budget", async () => { const wasm = decodeWasmDataUrl(wasmDataUrl); expect(wasm.byteLength).toBeLessThan(750_000); // The artifact carries its own provenance: the build embeds the pinned // revision as semver build metadata, so the repository's canonical VERSION // file is the single source of truth and drift is caught here without a copy. const result = await WebAssembly.instantiate(wasm.buffer as ArrayBuffer, { env: { log: () => {} }, }); const instance = result instanceof WebAssembly.Instance ? result : result.instance; const memory = instance.exports.memory as WebAssembly.Memory; const call = (name: string, ...args: number[]) => (instance.exports[name] as WasmFunction)(...args); const out = call("ghostty_wasm_alloc_u8_array", 8); expect(call("ghostty_build_info", 10, out)).toBe(0); const view = new DataView(memory.buffer, out, 8); const embeddedRevision = new TextDecoder().decode( new Uint8Array(memory.buffer, view.getUint32(0, true), view.getUint32(4, true)), ); call("ghostty_wasm_free_u8_array", out, 8); expect(embeddedRevision).toBe(pinnedVersion.trim()); }); it("creates, writes multi-codepoint graphemes, and frees repeated terminals", async () => { const bytes = decodeWasmDataUrl(wasmDataUrl); let memory: WebAssembly.Memory | null = null; const instantiated = await WebAssembly.instantiate(bytes.buffer as ArrayBuffer, { env: { log: () => {}, }, }); const instance = instantiated instanceof WebAssembly.Instance ? instantiated : instantiated.instance; const exports = instance.exports; memory = exports.memory as WebAssembly.Memory; const call = (name: string, ...args: number[]) => (exports[name] as WasmFunction)(...args); const jsonPointer = call("ghostty_type_json"); const jsonBytes = new Uint8Array(memory.buffer, jsonPointer); const jsonEnd = jsonBytes.indexOf(0); const layouts = JSON.parse(new TextDecoder().decode(jsonBytes.subarray(0, jsonEnd))) as Record< string, { size: number } >; const optionsSize = layouts.GhosttyTerminalOptions?.size; expect(optionsSize).toBe(8); if (optionsSize === undefined) throw new Error("GhosttyTerminalOptions layout is missing"); for (let iteration = 0; iteration < 25; iteration += 1) { const options = call("ghostty_wasm_alloc_u8_array", optionsSize); const optionsView = new DataView(memory.buffer, options, optionsSize); optionsView.setUint16(0, 80, true); optionsView.setUint16(2, 24, true); optionsView.setUint32(4, 5_000, true); const terminalSlot = call("ghostty_wasm_alloc_opaque"); expect(call("ghostty_terminal_new", 0, terminalSlot, options)).toBe(0); const terminal = new DataView(memory.buffer).getUint32(terminalSlot, true); const input = new TextEncoder().encode("e\u0301 👨👩👧👦 العربية\r\n"); const inputPointer = call("ghostty_wasm_alloc_u8_array", input.length); new Uint8Array(memory.buffer, inputPointer, input.length).set(input); call("ghostty_terminal_vt_write", terminal, inputPointer, input.length); call("ghostty_wasm_free_u8_array", inputPointer, input.length); call("ghostty_terminal_free", terminal); call("ghostty_wasm_free_opaque", terminalSlot); call("ghostty_wasm_free_u8_array", options, optionsSize); } }); it("blinks the default cursor until a program asks for a steady one", async () => { const result = await WebAssembly.instantiate( decodeWasmDataUrl(wasmDataUrl).buffer as ArrayBuffer, { env: { log: () => {} } }, ); const instance = result instanceof WebAssembly.Instance ? result : result.instance; const memory = instance.exports.memory as WebAssembly.Memory; const call = (name: string, ...args: number[]) => (instance.exports[name] as WasmFunction)(...args); const alloc = (size: number) => call("ghostty_wasm_alloc_u8_array", size); const options = alloc(8); const optionsView = new DataView(memory.buffer, options, 8); optionsView.setUint16(0, 80, true); optionsView.setUint16(2, 24, true); const terminalSlot = call("ghostty_wasm_alloc_opaque"); expect(call("ghostty_terminal_new", 0, terminalSlot, options)).toBe(0); const terminal = new DataView(memory.buffer).getUint32(terminalSlot, true); const renderStateSlot = call("ghostty_wasm_alloc_opaque"); expect(call("ghostty_render_state_new", 0, renderStateSlot)).toBe(0); const renderState = new DataView(memory.buffer).getUint32(renderStateSlot, true); const scratch = alloc(4); const blinking = () => { expect(call("ghostty_render_state_update", renderState, terminal)).toBe(0); expect(call("ghostty_render_state_get", renderState, 12, scratch)).toBe(0); return new DataView(memory.buffer, scratch, 4).getUint8(0) !== 0; }; const write = (data: string) => { const bytes = new TextEncoder().encode(data); const pointer = alloc(bytes.length); new Uint8Array(memory.buffer, pointer, bytes.length).set(bytes); call("ghostty_terminal_vt_write", terminal, pointer, bytes.length); call("ghostty_wasm_free_u8_array", pointer, bytes.length); }; const setDefaultCursorBlink = (blink: boolean) => { const value = alloc(1); new Uint8Array(memory.buffer, value, 1)[0] = blink ? 1 : 0; expect(call("ghostty_terminal_set", terminal, 23, value)).toBe(0); call("ghostty_wasm_free_u8_array", value, 1); }; // Ghostty's own default is a steady cursor, so the blink the web terminal // inherited from xterm.js only exists because option 23 asks for it. expect(blinking()).toBe(false); setDefaultCursorBlink(true); expect(blinking()).toBe(true); // Programs still own the cursor: DECSCUSR steady block and DEC mode 12 both // stop the blink, and DECSCUSR reset returns to the embedder default. write("\u001b[2 q"); expect(blinking()).toBe(false); write("\u001b[0 q"); expect(blinking()).toBe(true); write("\u001b[?12l"); expect(blinking()).toBe(false); write("\u001b[?12h"); expect(blinking()).toBe(true); // RIS restores Ghostty's built-in steady default rather than the embedder's, // which is why the core reapplies the option around a session replay. call("ghostty_terminal_reset", terminal); expect(blinking()).toBe(false); setDefaultCursorBlink(true); expect(blinking()).toBe(true); call("ghostty_wasm_free_u8_array", scratch, 4); call("ghostty_render_state_free", renderState); call("ghostty_wasm_free_opaque", renderStateSlot); call("ghostty_terminal_free", terminal); call("ghostty_wasm_free_opaque", terminalSlot); call("ghostty_wasm_free_u8_array", options, 8); }); it("reports and scrolls the viewport with Ghostty's scrollbar state", async () => { const result = await WebAssembly.instantiate( decodeWasmDataUrl(wasmDataUrl).buffer as ArrayBuffer, { env: { log: () => {} } }, ); const instance = result instanceof WebAssembly.Instance ? result : result.instance; const memory = instance.exports.memory as WebAssembly.Memory; const call = (name: string, ...args: number[]) => (instance.exports[name] as WasmFunction)(...args); const alloc = (size: number) => call("ghostty_wasm_alloc_u8_array", size); const options = alloc(8); const optionsView = new DataView(memory.buffer, options, 8); optionsView.setUint16(0, 80, true); optionsView.setUint16(2, 10, true); optionsView.setUint32(4, 1_000, true); const terminalSlot = call("ghostty_wasm_alloc_opaque"); expect(call("ghostty_terminal_new", 0, terminalSlot, options)).toBe(0); const terminal = new DataView(memory.buffer).getUint32(terminalSlot, true); const input = new TextEncoder().encode( Array.from({ length: 50 }, (_, index) => `${index + 1}\r\n`).join(""), ); const inputPointer = alloc(input.length); new Uint8Array(memory.buffer, inputPointer, input.length).set(input); call("ghostty_terminal_vt_write", terminal, inputPointer, input.length); const scrollbar = alloc(24); const scrollbarView = new DataView(memory.buffer, scrollbar, 24); expect(call("ghostty_terminal_get", terminal, 9, scrollbar)).toBe(0); expect([0, 8, 16].map((offset) => Number(scrollbarView.getBigUint64(offset, true)))).toEqual([ 51, 41, 10, ]); const scroll = alloc(24); const scrollView = new DataView(memory.buffer, scroll, 24); scrollView.setUint32(0, 2, true); scrollView.setInt32(8, -5, true); call("ghostty_terminal_scroll_viewport", terminal, scroll); expect(call("ghostty_terminal_get", terminal, 9, scrollbar)).toBe(0); expect(Number(scrollbarView.getBigUint64(8, true))).toBe(36); call("ghostty_wasm_free_u8_array", scroll, 24); call("ghostty_wasm_free_u8_array", scrollbar, 24); call("ghostty_wasm_free_u8_array", inputPointer, input.length); call("ghostty_terminal_free", terminal); call("ghostty_wasm_free_opaque", terminalSlot); call("ghostty_wasm_free_u8_array", options, 8); }); it("routes terminal-generated replies through the shared callback table", async () => { const mainResult = await WebAssembly.instantiate( decodeWasmDataUrl(wasmDataUrl).buffer as ArrayBuffer, { env: { log: () => {} } }, ); const main = mainResult instanceof WebAssembly.Instance ? mainResult : mainResult.instance; const memory = main.exports.memory as WebAssembly.Memory; let reply = ""; const trampolineResult = await WebAssembly.instantiate( decodeWasmDataUrl(writePtyWasmDataUrl).buffer as ArrayBuffer, { env: { t3_write_pty: (_terminal: number, _userdata: number, pointer: number, length: number) => { reply += new TextDecoder().decode(new Uint8Array(memory.buffer, pointer, length)); }, }, }, ); const trampoline = trampolineResult instanceof WebAssembly.Instance ? trampolineResult : trampolineResult.instance; const table = main.exports.__indirect_function_table as WebAssembly.Table; const callbackIndex = table.length; table.grow(1, trampoline.exports.ghostty_write_pty as CallableFunction); const call = (name: string, ...args: number[]) => (main.exports[name] as WasmFunction)(...args); const options = call("ghostty_wasm_alloc_u8_array", 8); const optionsView = new DataView(memory.buffer, options, 8); optionsView.setUint16(0, 80, true); optionsView.setUint16(2, 24, true); const terminalSlot = call("ghostty_wasm_alloc_opaque"); expect(call("ghostty_terminal_new", 0, terminalSlot, options)).toBe(0); const terminal = new DataView(memory.buffer).getUint32(terminalSlot, true); call("ghostty_terminal_set", terminal, 0, 1); call("ghostty_terminal_set", terminal, 1, callbackIndex); const query = new TextEncoder().encode("\u001b[5n"); const queryPointer = call("ghostty_wasm_alloc_u8_array", query.length); new Uint8Array(memory.buffer, queryPointer, query.length).set(query); call("ghostty_terminal_vt_write", terminal, queryPointer, query.length); expect(reply).toBe("\u001b[0n"); reply = ""; call("ghostty_terminal_set", terminal, 1, 0); call("ghostty_terminal_set", terminal, 0, 0); call("ghostty_terminal_vt_write", terminal, queryPointer, query.length); expect(reply).toBe(""); call("ghostty_terminal_set", terminal, 0, 1); call("ghostty_terminal_set", terminal, 1, callbackIndex); call("ghostty_terminal_vt_write", terminal, queryPointer, query.length); expect(reply).toBe("\u001b[0n"); call("ghostty_wasm_free_u8_array", queryPointer, query.length); call("ghostty_terminal_free", terminal); call("ghostty_wasm_free_opaque", terminalSlot); call("ghostty_wasm_free_u8_array", options, 8); }); it("formats the active selection with Ghostty's copy semantics", async () => { const result = await WebAssembly.instantiate( decodeWasmDataUrl(wasmDataUrl).buffer as ArrayBuffer, { env: { log: () => {} } }, ); const instance = result instanceof WebAssembly.Instance ? result : result.instance; const memory = instance.exports.memory as WebAssembly.Memory; const call = (name: string, ...args: number[]) => (instance.exports[name] as WasmFunction)(...args); const options = call("ghostty_wasm_alloc_u8_array", 8); const optionsView = new DataView(memory.buffer, options, 8); optionsView.setUint16(0, 80, true); optionsView.setUint16(2, 24, true); const terminalSlot = call("ghostty_wasm_alloc_opaque"); expect(call("ghostty_terminal_new", 0, terminalSlot, options)).toBe(0); const terminal = new DataView(memory.buffer).getUint32(terminalSlot, true); const input = new TextEncoder().encode("a\r\n\r\nb"); const inputPointer = call("ghostty_wasm_alloc_u8_array", input.length); new Uint8Array(memory.buffer, inputPointer, input.length).set(input); call("ghostty_terminal_vt_write", terminal, inputPointer, input.length); const selection = call("ghostty_wasm_alloc_u8_array", 32); new DataView(memory.buffer, selection, 32).setUint32(0, 32, true); expect(call("ghostty_terminal_select_all", terminal, selection)).toBe(0); expect(call("ghostty_terminal_set", terminal, 21, selection)).toBe(0); const formatOptions = call("ghostty_wasm_alloc_u8_array", 16); const formatView = new DataView(memory.buffer, formatOptions, 16); formatView.setUint32(0, 16, true); formatView.setUint8(8, 1); formatView.setUint8(9, 1); const written = call("ghostty_wasm_alloc_usize"); expect( call("ghostty_terminal_selection_format_buf", terminal, formatOptions, 0, 0, written), ).toBe(-3); const outputSize = new DataView(memory.buffer, written, 4).getUint32(0, true); const output = call("ghostty_wasm_alloc_u8_array", outputSize); expect( call( "ghostty_terminal_selection_format_buf", terminal, formatOptions, output, outputSize, written, ), ).toBe(0); expect(new TextDecoder().decode(new Uint8Array(memory.buffer, output, outputSize))).toBe( "a\n\nb", ); call("ghostty_wasm_free_u8_array", output, outputSize); call("ghostty_wasm_free_usize", written); call("ghostty_wasm_free_u8_array", formatOptions, 16); call("ghostty_wasm_free_u8_array", selection, 32); call("ghostty_wasm_free_u8_array", inputPointer, input.length); call("ghostty_terminal_free", terminal); call("ghostty_wasm_free_opaque", terminalSlot); call("ghostty_wasm_free_u8_array", options, 8); }); it("uses Ghostty for mouse encoding, word selection, and OSC 8 hit testing", async () => { const result = await WebAssembly.instantiate( decodeWasmDataUrl(wasmDataUrl).buffer as ArrayBuffer, { env: { log: () => {} } }, ); const instance = result instanceof WebAssembly.Instance ? result : result.instance; const memory = instance.exports.memory as WebAssembly.Memory; const call = (name: string, ...args: number[]) => (instance.exports[name] as WasmFunction)(...args); const alloc = (size: number) => call("ghostty_wasm_alloc_u8_array", size); const free = (pointer: number, size: number) => call("ghostty_wasm_free_u8_array", pointer, size); const terminalOptions = alloc(8); const terminalOptionsView = new DataView(memory.buffer, terminalOptions, 8); terminalOptionsView.setUint16(0, 80, true); terminalOptionsView.setUint16(2, 24, true); const terminalSlot = call("ghostty_wasm_alloc_opaque"); expect(call("ghostty_terminal_new", 0, terminalSlot, terminalOptions)).toBe(0); const terminal = new DataView(memory.buffer).getUint32(terminalSlot, true); const input = new TextEncoder().encode( "\u001b[?1000h\u001b[?1006h\u001b]8;;https://t3.codes/docs\u001b\\linked\u001b]8;;\u001b\\ plain", ); const inputPointer = alloc(input.length); new Uint8Array(memory.buffer, inputPointer, input.length).set(input); call("ghostty_terminal_vt_write", terminal, inputPointer, input.length); const modeFlag = alloc(1); new Uint8Array(memory.buffer, modeFlag, 1)[0] = 0; expect(call("ghostty_terminal_get", terminal, 11, modeFlag)).toBe(0); expect(new Uint8Array(memory.buffer, modeFlag, 1)[0]).toBe(1); new Uint8Array(memory.buffer, modeFlag, 1)[0] = 1; expect(call("ghostty_terminal_mode_get", terminal, 1003, modeFlag)).toBe(0); expect(new Uint8Array(memory.buffer, modeFlag, 1)[0]).toBe(0); const anyEventInput = new TextEncoder().encode("\u001b[?1003h"); const anyEventPointer = alloc(anyEventInput.length); new Uint8Array(memory.buffer, anyEventPointer, anyEventInput.length).set(anyEventInput); call("ghostty_terminal_vt_write", terminal, anyEventPointer, anyEventInput.length); expect(call("ghostty_terminal_mode_get", terminal, 1003, modeFlag)).toBe(0); expect(new Uint8Array(memory.buffer, modeFlag, 1)[0]).toBe(1); const anyEventReset = new TextEncoder().encode("\u001b[?1003l\u001b[?1000h"); const anyEventResetPointer = alloc(anyEventReset.length); new Uint8Array(memory.buffer, anyEventResetPointer, anyEventReset.length).set(anyEventReset); call("ghostty_terminal_vt_write", terminal, anyEventResetPointer, anyEventReset.length); expect(call("ghostty_terminal_mode_get", terminal, 1003, modeFlag)).toBe(0); expect(new Uint8Array(memory.buffer, modeFlag, 1)[0]).toBe(0); free(anyEventResetPointer, anyEventReset.length); free(anyEventPointer, anyEventInput.length); free(modeFlag, 1); const point = alloc(24); const pointView = new DataView(memory.buffer, point, 24); pointView.setUint32(0, 1, true); pointView.setUint16(8, 1, true); pointView.setUint32(12, 0, true); const gridRef = alloc(12); new DataView(memory.buffer, gridRef, 12).setUint32(0, 12, true); expect(call("ghostty_terminal_grid_ref", terminal, point, gridRef)).toBe(0); const written = call("ghostty_wasm_alloc_usize"); expect(call("ghostty_grid_ref_hyperlink_uri", gridRef, 0, 0, written)).toBe(-3); const hyperlinkSize = new DataView(memory.buffer, written, 4).getUint32(0, true); const hyperlink = alloc(hyperlinkSize); expect(call("ghostty_grid_ref_hyperlink_uri", gridRef, hyperlink, hyperlinkSize, written)).toBe( 0, ); expect(new TextDecoder().decode(new Uint8Array(memory.buffer, hyperlink, hyperlinkSize))).toBe( "https://t3.codes/docs", ); const wordOptions = alloc(24); const wordOptionsView = new DataView(memory.buffer, wordOptions, 24); wordOptionsView.setUint32(0, 24, true); new Uint8Array(memory.buffer, wordOptions + 4, 12).set( new Uint8Array(memory.buffer, gridRef, 12), ); const selection = alloc(32); new DataView(memory.buffer, selection, 32).setUint32(0, 32, true); expect(call("ghostty_terminal_select_word", terminal, wordOptions, selection)).toBe(0); expect(call("ghostty_terminal_set", terminal, 21, selection)).toBe(0); const formatOptions = alloc(16); const formatView = new DataView(memory.buffer, formatOptions, 16); formatView.setUint32(0, 16, true); formatView.setUint8(8, 1); formatView.setUint8(9, 1); expect( call("ghostty_terminal_selection_format_buf", terminal, formatOptions, 0, 0, written), ).toBe(-3); const selectionSize = new DataView(memory.buffer, written, 4).getUint32(0, true); const selectionText = alloc(selectionSize); expect( call( "ghostty_terminal_selection_format_buf", terminal, formatOptions, selectionText, selectionSize, written, ), ).toBe(0); expect( new TextDecoder().decode(new Uint8Array(memory.buffer, selectionText, selectionSize)), ).toBe("linked"); const lineOptions = alloc(28); const lineOptionsView = new DataView(memory.buffer, lineOptions, 28); lineOptionsView.setUint32(0, 28, true); new Uint8Array(memory.buffer, lineOptions + 4, 12).set( new Uint8Array(memory.buffer, gridRef, 12), ); expect(call("ghostty_terminal_select_line", terminal, lineOptions, selection)).toBe(0); expect(call("ghostty_terminal_set", terminal, 21, selection)).toBe(0); expect( call("ghostty_terminal_selection_format_buf", terminal, formatOptions, 0, 0, written), ).toBe(-3); const lineSelectionSize = new DataView(memory.buffer, written, 4).getUint32(0, true); const lineSelectionText = alloc(lineSelectionSize); expect( call( "ghostty_terminal_selection_format_buf", terminal, formatOptions, lineSelectionText, lineSelectionSize, written, ), ).toBe(0); expect( new TextDecoder().decode(new Uint8Array(memory.buffer, lineSelectionText, lineSelectionSize)), ).toBe("linked plain"); const mouseEncoderSlot = call("ghostty_wasm_alloc_opaque"); const mouseEventSlot = call("ghostty_wasm_alloc_opaque"); expect(call("ghostty_mouse_encoder_new", 0, mouseEncoderSlot)).toBe(0); expect(call("ghostty_mouse_event_new", 0, mouseEventSlot)).toBe(0); const view = new DataView(memory.buffer); const mouseEncoder = view.getUint32(mouseEncoderSlot, true); const mouseEvent = view.getUint32(mouseEventSlot, true); call("ghostty_mouse_encoder_setopt_from_terminal", mouseEncoder, terminal); const mouseSize = alloc(36); const mouseSizeView = new DataView(memory.buffer, mouseSize, 36); for (const [offset, value] of [ [0, 36], [4, 800], [8, 480], [12, 10], [16, 20], ] as const) { mouseSizeView.setUint32(offset, value, true); } call("ghostty_mouse_encoder_setopt", mouseEncoder, 2, mouseSize); call("ghostty_mouse_event_set_action", mouseEvent, 0); call("ghostty_mouse_event_set_button", mouseEvent, 1); const mousePosition = new DataView(new ArrayBuffer(8)); mousePosition.setFloat32(0, 15, true); mousePosition.setFloat32(4, 25, true); const mousePositionPointer = alloc(8); new Uint8Array(memory.buffer, mousePositionPointer, 8).set( new Uint8Array(mousePosition.buffer), ); call("ghostty_mouse_event_set_position", mouseEvent, mousePositionPointer); const mouseOutput = alloc(128); expect( call("ghostty_mouse_encoder_encode", mouseEncoder, mouseEvent, mouseOutput, 128, written), ).toBe(0); const mouseOutputSize = new DataView(memory.buffer, written, 4).getUint32(0, true); expect( new TextDecoder().decode(new Uint8Array(memory.buffer, mouseOutput, mouseOutputSize)), ).toBe("\u001b[<0;2;2M"); call("ghostty_mouse_event_free", mouseEvent); call("ghostty_mouse_encoder_free", mouseEncoder); call("ghostty_wasm_free_opaque", mouseEventSlot); call("ghostty_wasm_free_opaque", mouseEncoderSlot); free(mousePositionPointer, 8); free(mouseOutput, 128); free(mouseSize, 36); free(lineSelectionText, lineSelectionSize);Lines 1–500 of 634. Load more
import { describe, expect, it } from "vite-plus/test"; import wasmDataUrl from "./vendor/ghostty-vt.wasm?inline"; import writePtyWasmDataUrl from "./vendor/ghostty-write-pty.wasm?inline"; import pinnedVersion from "../../../../../native/libghostty-vt/VERSION?raw"; import { ghosttyKeyForCode } from "./keyCodes"; type WasmFunction = (...args: number[]) => number; function decodeWasmDataUrl(dataUrl: string): Uint8Array { const encoded = dataUrl.split(",", 2)[1]; if (!encoded) throw new Error("The vendored Ghostty WASM data URL is invalid"); return Uint8Array.from(atob(encoded), (character) => character.charCodeAt(0)); } describe("vendored libghostty-vt WebAssembly", () => { it("stays pinned to the canonical revision and size budget", async () => { const wasm = decodeWasmDataUrl(wasmDataUrl); expect(wasm.byteLength).toBeLessThan(750_000); // The artifact carries its own provenance: the build embeds the pinned // revision as semver build metadata, so the repository's canonical VERSION // file is the single source of truth and drift is caught here without a copy. const result = await WebAssembly.instantiate(wasm.buffer as ArrayBuffer, { env: { log: () => {} }, }); const instance = result instanceof WebAssembly.Instance ? result : result.instance; const memory = instance.exports.memory as WebAssembly.Memory; const call = (name: string, ...args: number[]) => (instance.exports[name] as WasmFunction)(...args); const out = call("ghostty_wasm_alloc_u8_array", 8); expect(call("ghostty_build_info", 10, out)).toBe(0); const view = new DataView(memory.buffer, out, 8); const embeddedRevision = new TextDecoder().decode( new Uint8Array(memory.buffer, view.getUint32(0, true), view.getUint32(4, true)), ); call("ghostty_wasm_free_u8_array", out, 8); expect(embeddedRevision).toBe(pinnedVersion.trim()); }); it("creates, writes multi-codepoint graphemes, and frees repeated terminals", async () => { const bytes = decodeWasmDataUrl(wasmDataUrl); let memory: WebAssembly.Memory | null = null; const instantiated = await WebAssembly.instantiate(bytes.buffer as ArrayBuffer, { env: { log: () => {}, }, }); const instance = instantiated instanceof WebAssembly.Instance ? instantiated : instantiated.instance; const exports = instance.exports; memory = exports.memory as WebAssembly.Memory; const call = (name: string, ...args: number[]) => (exports[name] as WasmFunction)(...args); const jsonPointer = call("ghostty_type_json"); const jsonBytes = new Uint8Array(memory.buffer, jsonPointer); const jsonEnd = jsonBytes.indexOf(0); const layouts = JSON.parse(new TextDecoder().decode(jsonBytes.subarray(0, jsonEnd))) as Record< string, { size: number } >; const optionsSize = layouts.GhosttyTerminalOptions?.size; expect(optionsSize).toBe(8); if (optionsSize === undefined) throw new Error("GhosttyTerminalOptions layout is missing"); for (let iteration = 0; iteration < 25; iteration += 1) { const options = call("ghostty_wasm_alloc_u8_array", optionsSize); const optionsView = new DataView(memory.buffer, options, optionsSize); optionsView.setUint16(0, 80, true); optionsView.setUint16(2, 24, true); optionsView.setUint32(4, 5_000, true); const terminalSlot = call("ghostty_wasm_alloc_opaque"); expect(call("ghostty_terminal_new", 0, terminalSlot, options)).toBe(0); const terminal = new DataView(memory.buffer).getUint32(terminalSlot, true); const input = new TextEncoder().encode("e\u0301 👨👩👧👦 العربية\r\n"); const inputPointer = call("ghostty_wasm_alloc_u8_array", input.length); new Uint8Array(memory.buffer, inputPointer, input.length).set(input); call("ghostty_terminal_vt_write", terminal, inputPointer, input.length); call("ghostty_wasm_free_u8_array", inputPointer, input.length); call("ghostty_terminal_free", terminal); call("ghostty_wasm_free_opaque", terminalSlot); call("ghostty_wasm_free_u8_array", options, optionsSize); } }); it("blinks the default cursor until a program asks for a steady one", async () => { const result = await WebAssembly.instantiate( decodeWasmDataUrl(wasmDataUrl).buffer as ArrayBuffer, { env: { log: () => {} } }, ); const instance = result instanceof WebAssembly.Instance ? result : result.instance; const memory = instance.exports.memory as WebAssembly.Memory; const call = (name: string, ...args: number[]) => (instance.exports[name] as WasmFunction)(...args); const alloc = (size: number) => call("ghostty_wasm_alloc_u8_array", size); const options = alloc(8); const optionsView = new DataView(memory.buffer, options, 8); optionsView.setUint16(0, 80, true); optionsView.setUint16(2, 24, true); const terminalSlot = call("ghostty_wasm_alloc_opaque"); expect(call("ghostty_terminal_new", 0, terminalSlot, options)).toBe(0); const terminal = new DataView(memory.buffer).getUint32(terminalSlot, true); const renderStateSlot = call("ghostty_wasm_alloc_opaque"); expect(call("ghostty_render_state_new", 0, renderStateSlot)).toBe(0); const renderState = new DataView(memory.buffer).getUint32(renderStateSlot, true); const scratch = alloc(4); const blinking = () => { expect(call("ghostty_render_state_update", renderState, terminal)).toBe(0); expect(call("ghostty_render_state_get", renderState, 12, scratch)).toBe(0); return new DataView(memory.buffer, scratch, 4).getUint8(0) !== 0; }; const write = (data: string) => { const bytes = new TextEncoder().encode(data); const pointer = alloc(bytes.length); new Uint8Array(memory.buffer, pointer, bytes.length).set(bytes); call("ghostty_terminal_vt_write", terminal, pointer, bytes.length); call("ghostty_wasm_free_u8_array", pointer, bytes.length); }; const setDefaultCursorBlink = (blink: boolean) => { const value = alloc(1); new Uint8Array(memory.buffer, value, 1)[0] = blink ? 1 : 0; expect(call("ghostty_terminal_set", terminal, 23, value)).toBe(0); call("ghostty_wasm_free_u8_array", value, 1); }; // Ghostty's own default is a steady cursor, so the blink the web terminal // inherited from xterm.js only exists because option 23 asks for it. expect(blinking()).toBe(false); setDefaultCursorBlink(true); expect(blinking()).toBe(true); // Programs still own the cursor: DECSCUSR steady block and DEC mode 12 both // stop the blink, and DECSCUSR reset returns to the embedder default. write("\u001b[2 q"); expect(blinking()).toBe(false); write("\u001b[0 q"); expect(blinking()).toBe(true); write("\u001b[?12l"); expect(blinking()).toBe(false); write("\u001b[?12h"); expect(blinking()).toBe(true); // RIS restores Ghostty's built-in steady default rather than the embedder's, // which is why the core reapplies the option around a session replay. call("ghostty_terminal_reset", terminal); expect(blinking()).toBe(false); setDefaultCursorBlink(true); expect(blinking()).toBe(true); call("ghostty_wasm_free_u8_array", scratch, 4); call("ghostty_render_state_free", renderState); call("ghostty_wasm_free_opaque", renderStateSlot); call("ghostty_terminal_free", terminal); call("ghostty_wasm_free_opaque", terminalSlot); call("ghostty_wasm_free_u8_array", options, 8); }); it("reports and scrolls the viewport with Ghostty's scrollbar state", async () => { const result = await WebAssembly.instantiate( decodeWasmDataUrl(wasmDataUrl).buffer as ArrayBuffer, { env: { log: () => {} } }, ); const instance = result instanceof WebAssembly.Instance ? result : result.instance; const memory = instance.exports.memory as WebAssembly.Memory; const call = (name: string, ...args: number[]) => (instance.exports[name] as WasmFunction)(...args); const alloc = (size: number) => call("ghostty_wasm_alloc_u8_array", size); const options = alloc(8); const optionsView = new DataView(memory.buffer, options, 8); optionsView.setUint16(0, 80, true); optionsView.setUint16(2, 10, true); optionsView.setUint32(4, 1_000, true); const terminalSlot = call("ghostty_wasm_alloc_opaque"); expect(call("ghostty_terminal_new", 0, terminalSlot, options)).toBe(0); const terminal = new DataView(memory.buffer).getUint32(terminalSlot, true); const input = new TextEncoder().encode( Array.from({ length: 50 }, (_, index) => `${index + 1}\r\n`).join(""), ); const inputPointer = alloc(input.length); new Uint8Array(memory.buffer, inputPointer, input.length).set(input); call("ghostty_terminal_vt_write", terminal, inputPointer, input.length); const scrollbar = alloc(24); const scrollbarView = new DataView(memory.buffer, scrollbar, 24); expect(call("ghostty_terminal_get", terminal, 9, scrollbar)).toBe(0); expect([0, 8, 16].map((offset) => Number(scrollbarView.getBigUint64(offset, true)))).toEqual([ 51, 41, 10, ]); const scroll = alloc(24); const scrollView = new DataView(memory.buffer, scroll, 24); scrollView.setUint32(0, 2, true); scrollView.setInt32(8, -5, true); call("ghostty_terminal_scroll_viewport", terminal, scroll); expect(call("ghostty_terminal_get", terminal, 9, scrollbar)).toBe(0); expect(Number(scrollbarView.getBigUint64(8, true))).toBe(36); call("ghostty_wasm_free_u8_array", scroll, 24); call("ghostty_wasm_free_u8_array", scrollbar, 24); call("ghostty_wasm_free_u8_array", inputPointer, input.length); call("ghostty_terminal_free", terminal); call("ghostty_wasm_free_opaque", terminalSlot); call("ghostty_wasm_free_u8_array", options, 8); }); it("routes terminal-generated replies through the shared callback table", async () => { const mainResult = await WebAssembly.instantiate( decodeWasmDataUrl(wasmDataUrl).buffer as ArrayBuffer, { env: { log: () => {} } }, ); const main = mainResult instanceof WebAssembly.Instance ? mainResult : mainResult.instance; const memory = main.exports.memory as WebAssembly.Memory; let reply = ""; const trampolineResult = await WebAssembly.instantiate( decodeWasmDataUrl(writePtyWasmDataUrl).buffer as ArrayBuffer, { env: { t3_write_pty: (_terminal: number, _userdata: number, pointer: number, length: number) => { reply += new TextDecoder().decode(new Uint8Array(memory.buffer, pointer, length)); }, }, }, ); const trampoline = trampolineResult instanceof WebAssembly.Instance ? trampolineResult : trampolineResult.instance; const table = main.exports.__indirect_function_table as WebAssembly.Table; const callbackIndex = table.length; table.grow(1, trampoline.exports.ghostty_write_pty as CallableFunction); const call = (name: string, ...args: number[]) => (main.exports[name] as WasmFunction)(...args); const options = call("ghostty_wasm_alloc_u8_array", 8); const optionsView = new DataView(memory.buffer, options, 8); optionsView.setUint16(0, 80, true); optionsView.setUint16(2, 24, true); const terminalSlot = call("ghostty_wasm_alloc_opaque"); expect(call("ghostty_terminal_new", 0, terminalSlot, options)).toBe(0); const terminal = new DataView(memory.buffer).getUint32(terminalSlot, true); call("ghostty_terminal_set", terminal, 0, 1); call("ghostty_terminal_set", terminal, 1, callbackIndex); const query = new TextEncoder().encode("\u001b[5n"); const queryPointer = call("ghostty_wasm_alloc_u8_array", query.length); new Uint8Array(memory.buffer, queryPointer, query.length).set(query); call("ghostty_terminal_vt_write", terminal, queryPointer, query.length); expect(reply).toBe("\u001b[0n"); reply = ""; call("ghostty_terminal_set", terminal, 1, 0); call("ghostty_terminal_set", terminal, 0, 0); call("ghostty_terminal_vt_write", terminal, queryPointer, query.length); expect(reply).toBe(""); call("ghostty_terminal_set", terminal, 0, 1); call("ghostty_terminal_set", terminal, 1, callbackIndex); call("ghostty_terminal_vt_write", terminal, queryPointer, query.length); expect(reply).toBe("\u001b[0n"); call("ghostty_wasm_free_u8_array", queryPointer, query.length); call("ghostty_terminal_free", terminal); call("ghostty_wasm_free_opaque", terminalSlot); call("ghostty_wasm_free_u8_array", options, 8); }); it("formats the active selection with Ghostty's copy semantics", async () => { const result = await WebAssembly.instantiate( decodeWasmDataUrl(wasmDataUrl).buffer as ArrayBuffer, { env: { log: () => {} } }, ); const instance = result instanceof WebAssembly.Instance ? result : result.instance; const memory = instance.exports.memory as WebAssembly.Memory; const call = (name: string, ...args: number[]) => (instance.exports[name] as WasmFunction)(...args); const options = call("ghostty_wasm_alloc_u8_array", 8); const optionsView = new DataView(memory.buffer, options, 8); optionsView.setUint16(0, 80, true); optionsView.setUint16(2, 24, true); const terminalSlot = call("ghostty_wasm_alloc_opaque"); expect(call("ghostty_terminal_new", 0, terminalSlot, options)).toBe(0); const terminal = new DataView(memory.buffer).getUint32(terminalSlot, true); const input = new TextEncoder().encode("a\r\n\r\nb"); const inputPointer = call("ghostty_wasm_alloc_u8_array", input.length); new Uint8Array(memory.buffer, inputPointer, input.length).set(input); call("ghostty_terminal_vt_write", terminal, inputPointer, input.length); const selection = call("ghostty_wasm_alloc_u8_array", 32); new DataView(memory.buffer, selection, 32).setUint32(0, 32, true); expect(call("ghostty_terminal_select_all", terminal, selection)).toBe(0); expect(call("ghostty_terminal_set", terminal, 21, selection)).toBe(0); const formatOptions = call("ghostty_wasm_alloc_u8_array", 16); const formatView = new DataView(memory.buffer, formatOptions, 16); formatView.setUint32(0, 16, true); formatView.setUint8(8, 1); formatView.setUint8(9, 1); const written = call("ghostty_wasm_alloc_usize"); expect( call("ghostty_terminal_selection_format_buf", terminal, formatOptions, 0, 0, written), ).toBe(-3); const outputSize = new DataView(memory.buffer, written, 4).getUint32(0, true); const output = call("ghostty_wasm_alloc_u8_array", outputSize); expect( call( "ghostty_terminal_selection_format_buf", terminal, formatOptions, output, outputSize, written, ), ).toBe(0); expect(new TextDecoder().decode(new Uint8Array(memory.buffer, output, outputSize))).toBe( "a\n\nb", ); call("ghostty_wasm_free_u8_array", output, outputSize); call("ghostty_wasm_free_usize", written); call("ghostty_wasm_free_u8_array", formatOptions, 16); call("ghostty_wasm_free_u8_array", selection, 32); call("ghostty_wasm_free_u8_array", inputPointer, input.length); call("ghostty_terminal_free", terminal); call("ghostty_wasm_free_opaque", terminalSlot); call("ghostty_wasm_free_u8_array", options, 8); }); it("uses Ghostty for mouse encoding, word selection, and OSC 8 hit testing", async () => { const result = await WebAssembly.instantiate( decodeWasmDataUrl(wasmDataUrl).buffer as ArrayBuffer, { env: { log: () => {} } }, ); const instance = result instanceof WebAssembly.Instance ? result : result.instance; const memory = instance.exports.memory as WebAssembly.Memory; const call = (name: string, ...args: number[]) => (instance.exports[name] as WasmFunction)(...args); const alloc = (size: number) => call("ghostty_wasm_alloc_u8_array", size); const free = (pointer: number, size: number) => call("ghostty_wasm_free_u8_array", pointer, size); const terminalOptions = alloc(8); const terminalOptionsView = new DataView(memory.buffer, terminalOptions, 8); terminalOptionsView.setUint16(0, 80, true); terminalOptionsView.setUint16(2, 24, true); const terminalSlot = call("ghostty_wasm_alloc_opaque"); expect(call("ghostty_terminal_new", 0, terminalSlot, terminalOptions)).toBe(0); const terminal = new DataView(memory.buffer).getUint32(terminalSlot, true); const input = new TextEncoder().encode( "\u001b[?1000h\u001b[?1006h\u001b]8;;https://t3.codes/docs\u001b\\linked\u001b]8;;\u001b\\ plain", ); const inputPointer = alloc(input.length); new Uint8Array(memory.buffer, inputPointer, input.length).set(input); call("ghostty_terminal_vt_write", terminal, inputPointer, input.length); const modeFlag = alloc(1); new Uint8Array(memory.buffer, modeFlag, 1)[0] = 0; expect(call("ghostty_terminal_get", terminal, 11, modeFlag)).toBe(0); expect(new Uint8Array(memory.buffer, modeFlag, 1)[0]).toBe(1); new Uint8Array(memory.buffer, modeFlag, 1)[0] = 1; expect(call("ghostty_terminal_mode_get", terminal, 1003, modeFlag)).toBe(0); expect(new Uint8Array(memory.buffer, modeFlag, 1)[0]).toBe(0); const anyEventInput = new TextEncoder().encode("\u001b[?1003h"); const anyEventPointer = alloc(anyEventInput.length); new Uint8Array(memory.buffer, anyEventPointer, anyEventInput.length).set(anyEventInput); call("ghostty_terminal_vt_write", terminal, anyEventPointer, anyEventInput.length); expect(call("ghostty_terminal_mode_get", terminal, 1003, modeFlag)).toBe(0); expect(new Uint8Array(memory.buffer, modeFlag, 1)[0]).toBe(1); const anyEventReset = new TextEncoder().encode("\u001b[?1003l\u001b[?1000h"); const anyEventResetPointer = alloc(anyEventReset.length); new Uint8Array(memory.buffer, anyEventResetPointer, anyEventReset.length).set(anyEventReset); call("ghostty_terminal_vt_write", terminal, anyEventResetPointer, anyEventReset.length); expect(call("ghostty_terminal_mode_get", terminal, 1003, modeFlag)).toBe(0); expect(new Uint8Array(memory.buffer, modeFlag, 1)[0]).toBe(0); free(anyEventResetPointer, anyEventReset.length); free(anyEventPointer, anyEventInput.length); free(modeFlag, 1); const point = alloc(24); const pointView = new DataView(memory.buffer, point, 24); pointView.setUint32(0, 1, true); pointView.setUint16(8, 1, true); pointView.setUint32(12, 0, true); const gridRef = alloc(12); new DataView(memory.buffer, gridRef, 12).setUint32(0, 12, true); expect(call("ghostty_terminal_grid_ref", terminal, point, gridRef)).toBe(0); const written = call("ghostty_wasm_alloc_usize"); expect(call("ghostty_grid_ref_hyperlink_uri", gridRef, 0, 0, written)).toBe(-3); const hyperlinkSize = new DataView(memory.buffer, written, 4).getUint32(0, true); const hyperlink = alloc(hyperlinkSize); expect(call("ghostty_grid_ref_hyperlink_uri", gridRef, hyperlink, hyperlinkSize, written)).toBe( 0, ); expect(new TextDecoder().decode(new Uint8Array(memory.buffer, hyperlink, hyperlinkSize))).toBe( "https://t3.codes/docs", ); const wordOptions = alloc(24); const wordOptionsView = new DataView(memory.buffer, wordOptions, 24); wordOptionsView.setUint32(0, 24, true); new Uint8Array(memory.buffer, wordOptions + 4, 12).set( new Uint8Array(memory.buffer, gridRef, 12), ); const selection = alloc(32); new DataView(memory.buffer, selection, 32).setUint32(0, 32, true); expect(call("ghostty_terminal_select_word", terminal, wordOptions, selection)).toBe(0); expect(call("ghostty_terminal_set", terminal, 21, selection)).toBe(0); const formatOptions = alloc(16); const formatView = new DataView(memory.buffer, formatOptions, 16); formatView.setUint32(0, 16, true); formatView.setUint8(8, 1); formatView.setUint8(9, 1); expect( call("ghostty_terminal_selection_format_buf", terminal, formatOptions, 0, 0, written), ).toBe(-3); const selectionSize = new DataView(memory.buffer, written, 4).getUint32(0, true); const selectionText = alloc(selectionSize); expect( call( "ghostty_terminal_selection_format_buf", terminal, formatOptions, selectionText, selectionSize, written, ), ).toBe(0); expect( new TextDecoder().decode(new Uint8Array(memory.buffer, selectionText, selectionSize)), ).toBe("linked"); const lineOptions = alloc(28); const lineOptionsView = new DataView(memory.buffer, lineOptions, 28); lineOptionsView.setUint32(0, 28, true); new Uint8Array(memory.buffer, lineOptions + 4, 12).set( new Uint8Array(memory.buffer, gridRef, 12), ); expect(call("ghostty_terminal_select_line", terminal, lineOptions, selection)).toBe(0); expect(call("ghostty_terminal_set", terminal, 21, selection)).toBe(0); expect( call("ghostty_terminal_selection_format_buf", terminal, formatOptions, 0, 0, written), ).toBe(-3); const lineSelectionSize = new DataView(memory.buffer, written, 4).getUint32(0, true); const lineSelectionText = alloc(lineSelectionSize); expect( call( "ghostty_terminal_selection_format_buf", terminal, formatOptions, lineSelectionText, lineSelectionSize, written, ), ).toBe(0); expect( new TextDecoder().decode(new Uint8Array(memory.buffer, lineSelectionText, lineSelectionSize)), ).toBe("linked plain"); const mouseEncoderSlot = call("ghostty_wasm_alloc_opaque"); const mouseEventSlot = call("ghostty_wasm_alloc_opaque"); expect(call("ghostty_mouse_encoder_new", 0, mouseEncoderSlot)).toBe(0); expect(call("ghostty_mouse_event_new", 0, mouseEventSlot)).toBe(0); const view = new DataView(memory.buffer); const mouseEncoder = view.getUint32(mouseEncoderSlot, true); const mouseEvent = view.getUint32(mouseEventSlot, true); call("ghostty_mouse_encoder_setopt_from_terminal", mouseEncoder, terminal); const mouseSize = alloc(36); const mouseSizeView = new DataView(memory.buffer, mouseSize, 36); for (const [offset, value] of [ [0, 36], [4, 800], [8, 480], [12, 10], [16, 20], ] as const) { mouseSizeView.setUint32(offset, value, true); } call("ghostty_mouse_encoder_setopt", mouseEncoder, 2, mouseSize); call("ghostty_mouse_event_set_action", mouseEvent, 0); call("ghostty_mouse_event_set_button", mouseEvent, 1); const mousePosition = new DataView(new ArrayBuffer(8)); mousePosition.setFloat32(0, 15, true); mousePosition.setFloat32(4, 25, true); const mousePositionPointer = alloc(8); new Uint8Array(memory.buffer, mousePositionPointer, 8).set( new Uint8Array(mousePosition.buffer), ); call("ghostty_mouse_event_set_position", mouseEvent, mousePositionPointer); const mouseOutput = alloc(128); expect( call("ghostty_mouse_encoder_encode", mouseEncoder, mouseEvent, mouseOutput, 128, written), ).toBe(0); const mouseOutputSize = new DataView(memory.buffer, written, 4).getUint32(0, true); expect( new TextDecoder().decode(new Uint8Array(memory.buffer, mouseOutput, mouseOutputSize)), ).toBe("\u001b[<0;2;2M"); call("ghostty_mouse_event_free", mouseEvent); call("ghostty_mouse_encoder_free", mouseEncoder); call("ghostty_wasm_free_opaque", mouseEventSlot); call("ghostty_wasm_free_opaque", mouseEncoderSlot); free(mousePositionPointer, 8); free(mouseOutput, 128); free(mouseSize, 36); free(lineSelectionText, lineSelectionSize);