Something went wrong. Try again.
Ghostty for the web with xterm.js API compatibility
Something went wrong. Try again.
11 kB · 372 lines
TypeScript
at task2
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373/** * TypeScript wrapper for libghostty-vt WASM API * * This provides a high-level, ergonomic API around the low-level C ABI * exports from libghostty-vt.wasm */
import { type GhosttyWasmExports, type SgrAttribute, SgrAttributeTag, type RGBColor, type KeyEvent, KeyEncoderOption, type KittyKeyFlags,} from './types';
/** * Main Ghostty WASM wrapper class */export class Ghostty { private exports: GhosttyWasmExports; private memory: WebAssembly.Memory;
constructor(wasmInstance: WebAssembly.Instance) { this.exports = wasmInstance.exports as GhosttyWasmExports; this.memory = this.exports.memory; }
/** * Get current memory buffer (may change when memory grows) */ private getBuffer(): ArrayBuffer { return this.memory.buffer; }
/** * Create an SGR parser instance */ createSgrParser(): SgrParser { return new SgrParser(this.exports); }
/** * Create a key encoder instance */ createKeyEncoder(): KeyEncoder { return new KeyEncoder(this.exports); }
/** * Load Ghostty WASM from URL */ static async load(wasmUrl: string): Promise<Ghostty> { const response = await fetch(wasmUrl); const wasmBytes = await response.arrayBuffer();
const wasmModule = await WebAssembly.instantiate(wasmBytes, { env: { log: (ptr: number, len: number) => { const instance = (wasmModule as any).instance; const bytes = new Uint8Array( instance.exports.memory.buffer, ptr, len ); const text = new TextDecoder().decode(bytes); console.log('[ghostty-wasm]', text); }, }, });
return new Ghostty(wasmModule.instance); }}
/** * SGR (Select Graphic Rendition) Parser * Parses ANSI color/style sequences like "1;31" (bold red) */export class SgrParser { private exports: GhosttyWasmExports; private parser: number = 0;
constructor(exports: GhosttyWasmExports) { this.exports = exports;
// Allocate parser const parserPtrPtr = this.exports.ghostty_wasm_alloc_opaque(); const result = this.exports.ghostty_sgr_new(0, parserPtrPtr); if (result !== 0) { throw new Error(`Failed to create SGR parser: ${result}`); }
// Read the parser pointer const view = new DataView(this.exports.memory.buffer); this.parser = view.getUint32(parserPtrPtr, true); this.exports.ghostty_wasm_free_opaque(parserPtrPtr); }
/** * Parse SGR parameters (e.g., [1, 31] for bold red) * @returns Iterator of SGR attributes */ *parse(params: number[]): Generator<SgrAttribute> { if (params.length === 0) return;
// Allocate parameter array const paramsPtr = this.exports.ghostty_wasm_alloc_u16_array(params.length); const view = new DataView(this.exports.memory.buffer); // Write parameters (uint16_t array) for (let i = 0; i < params.length; i++) { view.setUint16(paramsPtr + i * 2, params[i], true); }
// Set params in parser (null for subs, we don't use subparams yet) const result = this.exports.ghostty_sgr_set_params( this.parser, paramsPtr, 0, // subsPtr - null params.length );
this.exports.ghostty_wasm_free_u16_array(paramsPtr, params.length);
if (result !== 0) { throw new Error(`Failed to set SGR params: ${result}`); }
// Iterate through attributes const attrPtr = this.exports.ghostty_wasm_alloc_sgr_attribute(); try { while (this.exports.ghostty_sgr_next(this.parser, attrPtr)) { const attr = this.readAttribute(attrPtr); if (attr) yield attr; } } finally { this.exports.ghostty_wasm_free_sgr_attribute(attrPtr); } }
private readAttribute(attrPtr: number): SgrAttribute | null { const tag = this.exports.ghostty_sgr_attribute_tag(attrPtr); const view = new DataView(this.exports.memory.buffer);
switch (tag) { case SgrAttributeTag.BOLD: return { tag: SgrAttributeTag.BOLD }; case SgrAttributeTag.RESET_BOLD: return { tag: SgrAttributeTag.RESET_BOLD }; case SgrAttributeTag.ITALIC: return { tag: SgrAttributeTag.ITALIC }; case SgrAttributeTag.RESET_ITALIC: return { tag: SgrAttributeTag.RESET_ITALIC }; case SgrAttributeTag.FAINT: return { tag: SgrAttributeTag.FAINT }; case SgrAttributeTag.RESET_FAINT: return { tag: SgrAttributeTag.RESET_FAINT }; case SgrAttributeTag.UNDERLINE: return { tag: SgrAttributeTag.UNDERLINE }; case SgrAttributeTag.RESET_UNDERLINE: return { tag: SgrAttributeTag.RESET_UNDERLINE }; case SgrAttributeTag.BLINK: return { tag: SgrAttributeTag.BLINK }; case SgrAttributeTag.RESET_BLINK: return { tag: SgrAttributeTag.RESET_BLINK }; case SgrAttributeTag.INVERSE: return { tag: SgrAttributeTag.INVERSE }; case SgrAttributeTag.RESET_INVERSE: return { tag: SgrAttributeTag.RESET_INVERSE }; case SgrAttributeTag.INVISIBLE: return { tag: SgrAttributeTag.INVISIBLE }; case SgrAttributeTag.RESET_INVISIBLE: return { tag: SgrAttributeTag.RESET_INVISIBLE }; case SgrAttributeTag.STRIKETHROUGH: return { tag: SgrAttributeTag.STRIKETHROUGH }; case SgrAttributeTag.RESET_STRIKETHROUGH: return { tag: SgrAttributeTag.RESET_STRIKETHROUGH };
case SgrAttributeTag.FG_8: case SgrAttributeTag.FG_16: case SgrAttributeTag.FG_256: { // Color value is stored after the tag (uint8_t) const color = view.getUint8(attrPtr + 4); return { tag, color }; }
case SgrAttributeTag.FG_RGB: case SgrAttributeTag.BG_RGB: case SgrAttributeTag.UNDERLINE_COLOR_RGB: { // RGB color stored after the tag (3 bytes: r, g, b) const r = view.getUint8(attrPtr + 4); const g = view.getUint8(attrPtr + 5); const b = view.getUint8(attrPtr + 6); return { tag, color: { r, g, b } }; }
case SgrAttributeTag.FG_DEFAULT: return { tag: SgrAttributeTag.FG_DEFAULT }; case SgrAttributeTag.BG_8: case SgrAttributeTag.BG_16: case SgrAttributeTag.BG_256: { const color = view.getUint8(attrPtr + 4); return { tag, color }; }
case SgrAttributeTag.BG_DEFAULT: return { tag: SgrAttributeTag.BG_DEFAULT };
case SgrAttributeTag.UNDERLINE_COLOR_DEFAULT: return { tag: SgrAttributeTag.UNDERLINE_COLOR_DEFAULT };
default: // Unknown or unhandled return null; } }
/** * Reset parser state */ reset(): void { this.exports.ghostty_sgr_reset(this.parser); }
/** * Free parser resources */ dispose(): void { if (this.parser) { this.exports.ghostty_sgr_free(this.parser); this.parser = 0; } }}
/** * Key Encoder * Converts keyboard events into terminal escape sequences */export class KeyEncoder { private exports: GhosttyWasmExports; private encoder: number = 0;
constructor(exports: GhosttyWasmExports) { this.exports = exports;
// Allocate encoder const encoderPtrPtr = this.exports.ghostty_wasm_alloc_opaque(); const result = this.exports.ghostty_key_encoder_new(0, encoderPtrPtr); if (result !== 0) { throw new Error(`Failed to create key encoder: ${result}`); }
// Read the encoder pointer const view = new DataView(this.exports.memory.buffer); this.encoder = view.getUint32(encoderPtrPtr, true); this.exports.ghostty_wasm_free_opaque(encoderPtrPtr); }
/** * Set an encoder option */ setOption(option: KeyEncoderOption, value: boolean | number): void { const valuePtr = this.exports.ghostty_wasm_alloc_u8(); const view = new DataView(this.exports.memory.buffer); if (typeof value === 'boolean') { view.setUint8(valuePtr, value ? 1 : 0); } else { view.setUint8(valuePtr, value); }
const result = this.exports.ghostty_key_encoder_setopt( this.encoder, option, valuePtr );
this.exports.ghostty_wasm_free_u8(valuePtr);
if (result !== 0) { throw new Error(`Failed to set encoder option: ${result}`); } }
/** * Enable Kitty keyboard protocol with specified flags */ setKittyFlags(flags: KittyKeyFlags): void { this.setOption(KeyEncoderOption.KITTY_KEYBOARD_FLAGS, flags); }
/** * Encode a key event to escape sequence */ encode(event: KeyEvent): Uint8Array { // Create key event structure const eventPtrPtr = this.exports.ghostty_wasm_alloc_opaque(); const createResult = this.exports.ghostty_key_event_new(0, eventPtrPtr); if (createResult !== 0) { throw new Error(`Failed to create key event: ${createResult}`); }
const view = new DataView(this.exports.memory.buffer); const eventPtr = view.getUint32(eventPtrPtr, true); this.exports.ghostty_wasm_free_opaque(eventPtrPtr);
// Set event properties this.exports.ghostty_key_event_set_action(eventPtr, event.action); this.exports.ghostty_key_event_set_key(eventPtr, event.key); this.exports.ghostty_key_event_set_mods(eventPtr, event.mods);
if (event.utf8) { const encoder = new TextEncoder(); const utf8Bytes = encoder.encode(event.utf8); const utf8Ptr = this.exports.ghostty_wasm_alloc_u8_array(utf8Bytes.length); new Uint8Array(this.exports.memory.buffer).set(utf8Bytes, utf8Ptr); this.exports.ghostty_key_event_set_utf8(eventPtr, utf8Ptr, utf8Bytes.length); this.exports.ghostty_wasm_free_u8_array(utf8Ptr, utf8Bytes.length); }
// Allocate output buffer const bufferSize = 32; const bufPtr = this.exports.ghostty_wasm_alloc_u8_array(bufferSize); const writtenPtr = this.exports.ghostty_wasm_alloc_usize();
// Encode const encodeResult = this.exports.ghostty_key_encoder_encode( this.encoder, eventPtr, bufPtr, bufferSize, writtenPtr );
if (encodeResult !== 0) { this.exports.ghostty_wasm_free_u8_array(bufPtr, bufferSize); this.exports.ghostty_wasm_free_usize(writtenPtr); this.exports.ghostty_key_event_free(eventPtr); throw new Error(`Failed to encode key: ${encodeResult}`); }
// Read result const bytesWritten = view.getUint32(writtenPtr, true); const encoded = new Uint8Array( this.exports.memory.buffer, bufPtr, bytesWritten ).slice(); // Copy the data
// Cleanup this.exports.ghostty_wasm_free_u8_array(bufPtr, bufferSize); this.exports.ghostty_wasm_free_usize(writtenPtr); this.exports.ghostty_key_event_free(eventPtr);
return encoded; }
/** * Free encoder resources */ dispose(): void { if (this.encoder) { this.exports.ghostty_key_encoder_free(this.encoder); this.encoder = 0; } }}