diff --git a/demo/bin/demo.js b/demo/bin/demo.js index e4f2403..bc29333 100644 --- a/demo/bin/demo.js +++ b/demo/bin/demo.js @@ -215,6 +215,10 @@ const HTML_TEMPLATE = ` rows: 24, fontFamily: 'JetBrains Mono, Menlo, Monaco, monospace', fontSize: 14, + theme: { + background: '#1e1e1e', + foreground: '#d4d4d4', + }, }); const fitAddon = new FitAddon(); diff --git a/lib/ghostty.ts b/lib/ghostty.ts index 281d5d4..b8fb45a 100644 --- a/lib/ghostty.ts +++ b/lib/ghostty.ts @@ -8,7 +8,9 @@ import { CellFlags, type Cursor, + GHOSTTY_CONFIG_SIZE, type GhosttyCell, + type GhosttyTerminalConfig, type GhosttyWasmExports, KeyEncoderOption, type KeyEvent, @@ -49,8 +51,12 @@ export class Ghostty { /** * Create a terminal emulator instance */ - createTerminal(cols: number = 80, rows: number = 24): GhosttyTerminal { - return new GhosttyTerminal(this.exports, this.memory, cols, rows); + createTerminal( + cols: number = 80, + rows: number = 24, + config?: GhosttyTerminalConfig + ): GhosttyTerminal { + return new GhosttyTerminal(this.exports, this.memory, cols, rows, config); } /** @@ -286,20 +292,67 @@ export class GhosttyTerminal { * @param memory WASM memory * @param cols Number of columns (default: 80) * @param rows Number of rows (default: 24) + * @param config Optional terminal configuration (colors, scrollback) * @throws Error if allocation fails */ constructor( exports: GhosttyWasmExports, memory: WebAssembly.Memory, cols: number = 80, - rows: number = 24 + rows: number = 24, + config?: GhosttyTerminalConfig ) { this.exports = exports; this.memory = memory; this._cols = cols; this._rows = rows; - const handle = this.exports.ghostty_terminal_new(cols, rows); + let handle: TerminalHandle; + + if (config) { + // Allocate config struct in WASM memory + const configPtr = this.exports.ghostty_wasm_alloc_u8_array(GHOSTTY_CONFIG_SIZE); + if (configPtr === 0) { + throw new Error('Failed to allocate config (out of memory)'); + } + + try { + // Write config to WASM memory + const view = new DataView(this.memory.buffer); + let offset = configPtr; + + // scrollback_limit (u32) + view.setUint32(offset, config.scrollbackLimit ?? 10000, true); + offset += 4; + + // fg_color (u32) + view.setUint32(offset, config.fgColor ?? 0, true); + offset += 4; + + // bg_color (u32) + view.setUint32(offset, config.bgColor ?? 0, true); + offset += 4; + + // cursor_color (u32) + view.setUint32(offset, config.cursorColor ?? 0, true); + offset += 4; + + // palette[16] (u32 * 16) + for (let i = 0; i < 16; i++) { + const color = config.palette?.[i] ?? 0; + view.setUint32(offset, color, true); + offset += 4; + } + + handle = this.exports.ghostty_terminal_new_with_config(cols, rows, configPtr); + } finally { + // Free config memory + this.exports.ghostty_wasm_free_u8_array(configPtr, GHOSTTY_CONFIG_SIZE); + } + } else { + handle = this.exports.ghostty_terminal_new(cols, rows); + } + if (handle === 0) { throw new Error('Failed to allocate terminal (out of memory)'); } diff --git a/lib/terminal.ts b/lib/terminal.ts index d3bee14..3853b7f 100644 --- a/lib/terminal.ts +++ b/lib/terminal.ts @@ -36,6 +36,7 @@ import { OSC8LinkProvider } from './providers/osc8-link-provider'; import { UrlRegexProvider } from './providers/url-regex-provider'; import { CanvasRenderer } from './renderer'; import { SelectionManager } from './selection-manager'; +import type { GhosttyTerminalConfig } from './types'; import type { ILink, ILinkProvider } from './types'; // ============================================================================ @@ -174,6 +175,82 @@ export class Terminal implements ITerminalCore { this.buffer = new BufferNamespace(this); } + // ========================================================================== + // Theme to WASM Config Conversion + // ========================================================================== + + /** + * Parse a CSS color string to 0xRRGGBB format. + * Returns 0 if the color is undefined or invalid. + */ + private parseColorToHex(color?: string): number { + if (!color) return 0; + + // Handle hex colors (#RGB, #RRGGBB) + if (color.startsWith('#')) { + let hex = color.slice(1); + if (hex.length === 3) { + hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; + } + const value = Number.parseInt(hex, 16); + return Number.isNaN(value) ? 0 : value; + } + + // Handle rgb(r, g, b) format + const match = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/); + if (match) { + const r = Number.parseInt(match[1], 10); + const g = Number.parseInt(match[2], 10); + const b = Number.parseInt(match[3], 10); + return (r << 16) | (g << 8) | b; + } + + return 0; + } + + /** + * Convert terminal options to WASM terminal config. + */ + private buildWasmConfig(): GhosttyTerminalConfig | undefined { + const theme = this.options.theme; + const scrollback = this.options.scrollback; + + // If no theme and default scrollback, use defaults + if (!theme && scrollback === 1000) { + return undefined; + } + + // Build palette array from theme colors + // Order: black, red, green, yellow, blue, magenta, cyan, white, + // brightBlack, brightRed, brightGreen, brightYellow, brightBlue, brightMagenta, brightCyan, brightWhite + const palette: number[] = [ + this.parseColorToHex(theme?.black), + this.parseColorToHex(theme?.red), + this.parseColorToHex(theme?.green), + this.parseColorToHex(theme?.yellow), + this.parseColorToHex(theme?.blue), + this.parseColorToHex(theme?.magenta), + this.parseColorToHex(theme?.cyan), + this.parseColorToHex(theme?.white), + this.parseColorToHex(theme?.brightBlack), + this.parseColorToHex(theme?.brightRed), + this.parseColorToHex(theme?.brightGreen), + this.parseColorToHex(theme?.brightYellow), + this.parseColorToHex(theme?.brightBlue), + this.parseColorToHex(theme?.brightMagenta), + this.parseColorToHex(theme?.brightCyan), + this.parseColorToHex(theme?.brightWhite), + ]; + + return { + scrollbackLimit: scrollback, + fgColor: this.parseColorToHex(theme?.foreground), + bgColor: this.parseColorToHex(theme?.background), + cursorColor: this.parseColorToHex(theme?.cursor), + palette, + }; + } + // ========================================================================== // Option Change Handling (for mutable options) // ========================================================================== @@ -248,8 +325,9 @@ export class Terminal implements ITerminalCore { parent.setAttribute('tabindex', '0'); } - // Create WASM terminal with current dimensions - this.wasmTerm = this.ghostty!.createTerminal(this.cols, this.rows); + // Create WASM terminal with current dimensions and theme config + const wasmConfig = this.buildWasmConfig(); + this.wasmTerm = this.ghostty!.createTerminal(this.cols, this.rows, wasmConfig); // Create canvas element this.canvas = document.createElement('canvas'); @@ -549,7 +627,8 @@ export class Terminal implements ITerminalCore { if (this.wasmTerm) { this.wasmTerm.free(); } - this.wasmTerm = this.ghostty!.createTerminal(this.cols, this.rows); + const wasmConfig = this.buildWasmConfig(); + this.wasmTerm = this.ghostty!.createTerminal(this.cols, this.rows, wasmConfig); // Clear renderer this.renderer!.clear(); diff --git a/lib/types.ts b/lib/types.ts index 8a83afb..7cf6b49 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -387,6 +387,25 @@ export interface GhosttyWasmExports extends WebAssembly.Exports { // Terminal Types // ============================================================================ +/** + * Terminal configuration for WASM. + * All colors use 0xRRGGBB format. A value of 0 means "use default". + */ +export interface GhosttyTerminalConfig { + scrollbackLimit?: number; + fgColor?: number; // 0xRRGGBB + bgColor?: number; // 0xRRGGBB + cursorColor?: number; // 0xRRGGBB + palette?: number[]; // 16 colors, 0xRRGGBB format +} + +/** + * Size of GhosttyTerminalConfig struct in WASM memory (bytes). + * Layout: scrollback_limit(u32) + fg_color(u32) + bg_color(u32) + cursor_color(u32) + palette[16](u32*16) + * Total: 4 + 4 + 4 + 4 + 64 = 80 bytes + */ +export const GHOSTTY_CONFIG_SIZE = 80; + /** * Opaque terminal pointer (WASM memory address) */ diff --git a/patches/ghostty-wasm-api.patch b/patches/ghostty-wasm-api.patch index 98acfc7..1e2ce5b 100644 --- a/patches/ghostty-wasm-api.patch +++ b/patches/ghostty-wasm-api.patch @@ -32,7 +32,7 @@ new file mode 100644 index 000000000..078a0b872 --- /dev/null +++ b/include/ghostty/vt/terminal.h -@@ -0,0 +1,486 @@ +@@ -0,0 +1,499 @@ +/** + * @file terminal.h + * @@ -112,6 +112,7 @@ index 000000000..078a0b872 + * Terminal configuration options. + * + * Used when creating a new terminal to specify behavior and limits. ++ * All colors use 0xRRGGBB format. A value of 0 means "use default". + */ +typedef struct { + /** @@ -131,6 +132,18 @@ index 000000000..078a0b872 + * Initial background color (RGB, 0xRRGGBB format, 0 = use default). + */ + uint32_t bg_color; ++ ++ /** ++ * Cursor color (RGB, 0xRRGGBB format, 0 = use default). ++ */ ++ uint32_t cursor_color; ++ ++ /** ++ * ANSI color palette (16 colors, 0xRRGGBB format, 0 = use default). ++ * Index 0-7: Normal colors (black, red, green, yellow, blue, magenta, cyan, white) ++ * Index 8-15: Bright colors (same order) ++ */ ++ uint32_t palette[16]; +} GhosttyTerminalConfig; + +/** @@ -609,7 +622,7 @@ new file mode 100644 index 000000000..e79702488 --- /dev/null +++ b/src/terminal/c/terminal.zig -@@ -0,0 +1,611 @@ +@@ -0,0 +1,638 @@ +//! C API wrapper for Terminal +//! +//! This provides a C-compatible interface to Ghostty's Terminal for WASM export. @@ -655,6 +668,8 @@ index 000000000..e79702488 + scrollback_limit: u32, + fg_color: u32, + bg_color: u32, ++ cursor_color: u32, ++ palette: [16]u32, + }; +}; + @@ -679,6 +694,8 @@ index 000000000..e79702488 + scrollback_limit: u32, + fg_color: u32, + bg_color: u32, ++ cursor_color: u32, ++ palette: [16]u32, +}; + +// ============================================================================ @@ -705,10 +722,14 @@ index 000000000..e79702488 + .scrollback_limit = cfg.scrollback_limit, + .fg_color = cfg.fg_color, + .bg_color = cfg.bg_color, ++ .cursor_color = cfg.cursor_color, ++ .palette = cfg.palette, + } else .{ + .scrollback_limit = 10_000, + .fg_color = 0, + .bg_color = 0, ++ .cursor_color = 0, ++ .palette = [_]u32{0} ** 16, + }; + + // Allocate wrapper @@ -735,6 +756,25 @@ index 000000000..e79702488 + }; + colors.background = color.DynamicRGB.init(rgb); + } ++ if (config.cursor_color != 0) { ++ const rgb = color.RGB{ ++ .r = @truncate((config.cursor_color >> 16) & 0xFF), ++ .g = @truncate((config.cursor_color >> 8) & 0xFF), ++ .b = @truncate(config.cursor_color & 0xFF), ++ }; ++ colors.cursor = color.DynamicRGB.init(rgb); ++ } ++ // Apply palette colors (0 = use default, non-zero = override) ++ for (config.palette, 0..) |palette_color, i| { ++ if (palette_color != 0) { ++ const rgb = color.RGB{ ++ .r = @truncate((palette_color >> 16) & 0xFF), ++ .g = @truncate((palette_color >> 8) & 0xFF), ++ .b = @truncate(palette_color & 0xFF), ++ }; ++ colors.palette.set(@intCast(i), rgb); ++ } ++ } + + // Create terminal + const terminal = Terminal.init(