221 lines · 7.7 KB · TypeScript
RawDownload
import ghosttyWasmUrl from "./vendor/ghostty-vt.wasm?url";import ghosttyWritePtyWasmUrl from "./vendor/ghostty-write-pty.wasm?url&no-inline"; type WasmFunction = (...args: Array<number | bigint>) => number; interface TypeField {  readonly offset: number;  readonly size: number;  readonly type: string;} interface TypeLayout {  readonly size: number;  readonly align: number;  readonly fields: Readonly<Record<string, TypeField>>;} type TypeLayouts = Readonly<Record<string, TypeLayout>>; const textDecoder = new TextDecoder(); export class GhosttyRuntime {  readonly memory: WebAssembly.Memory;  readonly layouts: TypeLayouts;  private readonly exports: WebAssembly.Exports;  private readonly ptyWriters = new Map<number, (data: string) => void>();  private nextPtyWriterId = 1;  private writePtyFunctionIndex = 0;   private constructor(instance: WebAssembly.Instance) {    this.exports = instance.exports;    const memory = instance.exports.memory;    if (!(memory instanceof WebAssembly.Memory)) {      throw new Error("libghostty-vt did not export WebAssembly memory");    }    this.memory = memory;    const jsonPointer = this.call("ghostty_type_json");    const bytes = new Uint8Array(memory.buffer);    let end = jsonPointer;    while (end < bytes.length && bytes[end] !== 0) end += 1;    this.layouts = JSON.parse(textDecoder.decode(bytes.subarray(jsonPointer, end))) as TypeLayouts;  }   static async load(): Promise<GhosttyRuntime> {    const response = await fetch(ghosttyWasmUrl);    if (!response.ok) {      throw new Error(`Unable to load libghostty-vt (${response.status})`);    }    let instance: WebAssembly.Instance | undefined;    const imports = {      env: {        log: (pointer: number, length: number) => {          if (!instance) return;          const memory = instance.exports.memory;          if (!(memory instanceof WebAssembly.Memory)) return;          const message = textDecoder.decode(new Uint8Array(memory.buffer, pointer, length));          console.debug("[libghostty-vt]", message);        },      },    };    const result = await WebAssembly.instantiate(await response.arrayBuffer(), imports);    instance = result.instance;    const runtime = new GhosttyRuntime(result.instance);    await runtime.installWritePtyTrampoline();    return runtime;  }   call(name: string, ...args: Array<number | bigint>): number {    const fn = this.exports[name];    if (typeof fn !== "function") {      throw new Error(`libghostty-vt export is unavailable: ${name}`);    }    return (fn as WasmFunction)(...args);  }   layout(name: string): TypeLayout {    const layout = this.layouts[name];    if (!layout) throw new Error(`libghostty-vt type layout is unavailable: ${name}`);    return layout;  }   alloc(size: number): number {    const pointer = this.call("ghostty_wasm_alloc_u8_array", size);    if (pointer === 0) throw new Error(`libghostty-vt failed to allocate ${size} bytes`);    new Uint8Array(this.memory.buffer, pointer, size).fill(0);    return pointer;  }   free(pointer: number, size: number): void {    if (pointer !== 0) this.call("ghostty_wasm_free_u8_array", pointer, size);  }   allocOpaque(): number {    const pointer = this.call("ghostty_wasm_alloc_opaque");    if (pointer === 0) throw new Error("libghostty-vt failed to allocate an opaque pointer");    // The slot is uninitialized until a *_new call writes it; zero it so dispose    // paths that run after a partial initialization never free a garbage pointer.    new DataView(this.memory.buffer).setUint32(pointer, 0, true);    return pointer;  }   freeOpaque(pointer: number): void {    if (pointer !== 0) this.call("ghostty_wasm_free_opaque", pointer);  }   readPointer(slot: number): number {    return new DataView(this.memory.buffer).getUint32(slot, true);  }   attachPtyWriter(terminal: number, writer: (data: string) => void): number {    if (this.writePtyFunctionIndex === 0) {      throw new Error("libghostty-vt PTY callback trampoline is unavailable");    }    const id = this.nextPtyWriterId++;    this.ptyWriters.set(id, writer);    this.call("ghostty_terminal_set", terminal, 0, id);    this.call("ghostty_terminal_set", terminal, 1, this.writePtyFunctionIndex);    return id;  }   detachPtyWriter(terminal: number, id: number): void {    this.call("ghostty_terminal_set", terminal, 1, 0);    this.call("ghostty_terminal_set", terminal, 0, 0);    this.ptyWriters.delete(id);  }   view(pointer: number, size?: number): DataView {    return new DataView(this.memory.buffer, pointer, size);  }   bytes(pointer: number, size: number): Uint8Array {    return new Uint8Array(this.memory.buffer, pointer, size);  }   setField(pointer: number, structName: string, fieldName: string, value: number): void {    const field = this.layout(structName).fields[fieldName];    if (!field) throw new Error(`libghostty-vt field is unavailable: ${structName}.${fieldName}`);    const view = this.view(pointer + field.offset, field.size);    switch (field.type) {      case "bool":      case "u8":        view.setUint8(0, value);        return;      case "u16":        view.setUint16(0, value, true);        return;      case "i32":        view.setInt32(0, value, true);        return;      case "u32":      case "enum":        view.setUint32(0, value, true);        return;      case "u64":        view.setBigUint64(0, BigInt(value), true);        return;      default:        throw new Error(`Unsupported libghostty-vt field type: ${field.type}`);    }  }   readField(pointer: number, structName: string, fieldName: string): number {    const field = this.layout(structName).fields[fieldName];    if (!field) throw new Error(`libghostty-vt field is unavailable: ${structName}.${fieldName}`);    const view = this.view(pointer + field.offset, field.size);    switch (field.type) {      case "bool":      case "u8":        return view.getUint8(0);      case "u16":        return view.getUint16(0, true);      case "i32":        return view.getInt32(0, true);      case "u32":      case "enum":        return view.getUint32(0, true);      case "u64":        return Number(view.getBigUint64(0, true));      default:        throw new Error(`Unsupported libghostty-vt field type: ${field.type}`);    }  }   private async installWritePtyTrampoline(): Promise<void> {    const response = await fetch(ghosttyWritePtyWasmUrl);    if (!response.ok) {      throw new Error(`Unable to load the libghostty-vt PTY trampoline (${response.status})`);    }    const result = await WebAssembly.instantiate(await response.arrayBuffer(), {      env: {        t3_write_pty: (_terminal: number, userdata: number, pointer: number, length: number) => {          const writer = this.ptyWriters.get(userdata);          if (!writer || length === 0) return;          writer(textDecoder.decode(new Uint8Array(this.memory.buffer, pointer, length)));        },      },    });    const trampoline = result.instance.exports.ghostty_write_pty;    const table = this.exports.__indirect_function_table;    if (typeof trampoline !== "function" || !(table instanceof WebAssembly.Table)) {      throw new Error("libghostty-vt did not expose its callback table");    }    const index = table.length;    // grow-then-set instead of grow(1, fn): WebKit stores a grow init value    // with broken type information and every later call_indirect through the    // entry traps with a signature mismatch. table.set canonicalizes correctly.    table.grow(1);    table.set(index, trampoline);    this.writePtyFunctionIndex = index;  }} let runtimePromise: Promise<GhosttyRuntime> | null = null; export function loadGhosttyRuntime(): Promise<GhosttyRuntime> {  runtimePromise ??= GhosttyRuntime.load().catch((error) => {    runtimePromise = null;    throw error;  });  return runtimePromise;}