Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions demo/bin/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,10 @@ const HTML_TEMPLATE = `<!doctype html>
rows: 24,
fontFamily: 'JetBrains Mono, Menlo, Monaco, monospace',
fontSize: 14,
theme: {
background: '#1e1e1e',
foreground: '#d4d4d4',
},
});

const fitAddon = new FitAddon();
Expand Down
61 changes: 57 additions & 4 deletions lib/ghostty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
import {
CellFlags,
type Cursor,
GHOSTTY_CONFIG_SIZE,
type GhosttyCell,
type GhosttyTerminalConfig,
type GhosttyWasmExports,
KeyEncoderOption,
type KeyEvent,
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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)');
}
Expand Down
85 changes: 82 additions & 3 deletions lib/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

// ============================================================================
Expand Down Expand Up @@ -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)
// ==========================================================================
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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();
Expand Down
19 changes: 19 additions & 0 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
*/
Expand Down
44 changes: 42 additions & 2 deletions patches/ghostty-wasm-api.patch
Original file line number Diff line number Diff line change
Expand Up @@ -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
+ *
Expand Down Expand Up @@ -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 {
+ /**
Expand All @@ -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;
+
+/**
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -655,6 +668,8 @@ index 000000000..e79702488
+ scrollback_limit: u32,
+ fg_color: u32,
+ bg_color: u32,
+ cursor_color: u32,
+ palette: [16]u32,
+ };
+};
+
Expand All @@ -679,6 +694,8 @@ index 000000000..e79702488
+ scrollback_limit: u32,
+ fg_color: u32,
+ bg_color: u32,
+ cursor_color: u32,
+ palette: [16]u32,
+};
+
+// ============================================================================
Expand All @@ -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
Expand All @@ -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(
Expand Down