Skip to content

Commit 8748b89

Browse files
committed
fix: restore terminal config support and fix formatting
- Add GhosttyTerminalConfig struct and ghostty_terminal_new_with_config to WASM API - Update GhosttyTerminal constructor to accept optional config parameter - Update Ghostty.createTerminal() to pass config through - Use same parseColorToHex() and buildWasmConfig() pattern as main branch - Fix double semicolon formatting issue in terminal.ts - Add tests for config (scrollback, theme colors, palette colors) Restored config options: - scrollbackLimit: Max scrollback lines (0 = unlimited) - fgColor: Default foreground color (0xRRGGBB) - bgColor: Default background color (0xRRGGBB) - cursorColor: Cursor color (0xRRGGBB) - palette: 16-color ANSI palette
1 parent 96da9d4 commit 8748b89

File tree

5 files changed

+481
-133
lines changed

5 files changed

+481
-133
lines changed

lib/ghostty.ts

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import {
1010
CellFlags,
1111
type Cursor,
1212
DirtyState,
13+
GHOSTTY_CONFIG_SIZE,
1314
type GhosttyCell,
15+
type GhosttyTerminalConfig,
1416
type GhosttyWasmExports,
1517
KeyEncoderOption,
1618
type KeyEvent,
@@ -27,6 +29,7 @@ export {
2729
type Cursor,
2830
DirtyState,
2931
type GhosttyCell,
32+
type GhosttyTerminalConfig,
3033
KeyEncoderOption,
3134
type RGB,
3235
type RenderStateColors,
@@ -49,8 +52,12 @@ export class Ghostty {
4952
return new KeyEncoder(this.exports);
5053
}
5154

52-
createTerminal(cols: number = 80, rows: number = 24): GhosttyTerminal {
53-
return new GhosttyTerminal(this.exports, this.memory, cols, rows);
55+
createTerminal(
56+
cols: number = 80,
57+
rows: number = 24,
58+
config?: GhosttyTerminalConfig
59+
): GhosttyTerminal {
60+
return new GhosttyTerminal(this.exports, this.memory, cols, rows, config);
5461
}
5562

5663
static async load(wasmPath?: string): Promise<Ghostty> {
@@ -261,13 +268,61 @@ export class GhosttyTerminal {
261268
/** Cell pool for zero-allocation rendering */
262269
private cellPool: GhosttyCell[] = [];
263270

264-
constructor(exports: GhosttyWasmExports, memory: WebAssembly.Memory, cols: number, rows: number) {
271+
constructor(
272+
exports: GhosttyWasmExports,
273+
memory: WebAssembly.Memory,
274+
cols: number = 80,
275+
rows: number = 24,
276+
config?: GhosttyTerminalConfig
277+
) {
265278
this.exports = exports;
266279
this.memory = memory;
267280
this._cols = cols;
268281
this._rows = rows;
269282

270-
this.handle = this.exports.ghostty_terminal_new(cols, rows);
283+
if (config) {
284+
// Allocate config struct in WASM memory
285+
const configPtr = this.exports.ghostty_wasm_alloc_u8_array(GHOSTTY_CONFIG_SIZE);
286+
if (configPtr === 0) {
287+
throw new Error('Failed to allocate config (out of memory)');
288+
}
289+
290+
try {
291+
// Write config to WASM memory
292+
const view = new DataView(this.memory.buffer);
293+
let offset = configPtr;
294+
295+
// scrollback_limit (u32)
296+
view.setUint32(offset, config.scrollbackLimit ?? 10000, true);
297+
offset += 4;
298+
299+
// fg_color (u32)
300+
view.setUint32(offset, config.fgColor ?? 0, true);
301+
offset += 4;
302+
303+
// bg_color (u32)
304+
view.setUint32(offset, config.bgColor ?? 0, true);
305+
offset += 4;
306+
307+
// cursor_color (u32)
308+
view.setUint32(offset, config.cursorColor ?? 0, true);
309+
offset += 4;
310+
311+
// palette[16] (u32 * 16)
312+
for (let i = 0; i < 16; i++) {
313+
view.setUint32(offset, config.palette?.[i] ?? 0, true);
314+
offset += 4;
315+
}
316+
317+
this.handle = this.exports.ghostty_terminal_new_with_config(cols, rows, configPtr);
318+
} finally {
319+
// Free the config memory
320+
this.exports.ghostty_wasm_free_u8_array(configPtr, GHOSTTY_CONFIG_SIZE);
321+
}
322+
} else {
323+
this.handle = this.exports.ghostty_terminal_new(cols, rows);
324+
}
325+
271326
if (!this.handle) throw new Error('Failed to create terminal');
272327

273328
this.initCellPool();

lib/terminal.test.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1399,6 +1399,117 @@ describe('Buffer Access API', () => {
13991399
});
14001400
});
14011401

1402+
describe('Terminal Config', () => {
1403+
test('should pass scrollback option to WASM terminal', async () => {
1404+
if (typeof document === 'undefined') return;
1405+
1406+
// Create terminal with custom scrollback
1407+
const term = await createIsolatedTerminal({ cols: 80, rows: 24, scrollback: 500 });
1408+
const container = document.createElement('div');
1409+
term.open(container);
1410+
1411+
try {
1412+
// Write enough lines to fill scrollback
1413+
for (let i = 0; i < 600; i++) {
1414+
term.write(`Line ${i}\r\n`);
1415+
}
1416+
1417+
// Scrollback should be limited based on the config
1418+
const scrollbackLength = term.wasmTerm!.getScrollbackLength();
1419+
// With 500 scrollback limit, we wrote 600 lines so scrollback should be capped
1420+
// The actual value depends on ghostty's implementation but should be around 500
1421+
expect(scrollbackLength).toBeLessThan(600);
1422+
expect(scrollbackLength).toBeGreaterThan(450);
1423+
} finally {
1424+
term.dispose();
1425+
}
1426+
});
1427+
1428+
test('should pass theme colors to WASM terminal', async () => {
1429+
if (typeof document === 'undefined') return;
1430+
1431+
// Create terminal with custom theme
1432+
const term = await createIsolatedTerminal({
1433+
cols: 80,
1434+
rows: 24,
1435+
theme: {
1436+
foreground: '#00ff00', // Green
1437+
background: '#000080', // Navy blue
1438+
},
1439+
});
1440+
const container = document.createElement('div');
1441+
term.open(container);
1442+
1443+
try {
1444+
// Get the default colors from render state
1445+
const colors = term.wasmTerm!.getColors();
1446+
1447+
// Verify foreground is green (0x00FF00)
1448+
expect(colors.foreground.r).toBe(0);
1449+
expect(colors.foreground.g).toBe(255);
1450+
expect(colors.foreground.b).toBe(0);
1451+
1452+
// Verify background is navy (0x000080)
1453+
expect(colors.background.r).toBe(0);
1454+
expect(colors.background.g).toBe(0);
1455+
expect(colors.background.b).toBe(128);
1456+
} finally {
1457+
term.dispose();
1458+
}
1459+
});
1460+
1461+
test('should pass palette colors to WASM terminal', async () => {
1462+
if (typeof document === 'undefined') return;
1463+
1464+
// Create terminal with custom red color in palette
1465+
const term = await createIsolatedTerminal({
1466+
cols: 80,
1467+
rows: 24,
1468+
theme: {
1469+
red: '#ff0000', // Bright red for ANSI red
1470+
},
1471+
});
1472+
const container = document.createElement('div');
1473+
term.open(container);
1474+
1475+
try {
1476+
// Write red text using ANSI escape code
1477+
term.write('\x1b[31mRed text\x1b[0m');
1478+
1479+
// Get first cell - should have red foreground
1480+
const line = term.wasmTerm!.getLine(0);
1481+
const firstCell = line[0];
1482+
1483+
// The foreground should be red (0xFF0000)
1484+
expect(firstCell.fg_r).toBe(255);
1485+
expect(firstCell.fg_g).toBe(0);
1486+
expect(firstCell.fg_b).toBe(0);
1487+
} finally {
1488+
term.dispose();
1489+
}
1490+
});
1491+
1492+
test('should use default config when no options provided', async () => {
1493+
if (typeof document === 'undefined') return;
1494+
1495+
// Create terminal with no config
1496+
const term = await createIsolatedTerminal({ cols: 80, rows: 24 });
1497+
const container = document.createElement('div');
1498+
term.open(container);
1499+
1500+
try {
1501+
// Should still work and have reasonable defaults
1502+
const colors = term.wasmTerm!.getColors();
1503+
1504+
// Default colors should be set (light gray foreground, black background)
1505+
expect(colors.foreground).toBeDefined();
1506+
expect(colors.background).toBeDefined();
1507+
} finally {
1508+
term.dispose();
1509+
}
1510+
});
1511+
});
1512+
14021513
describe('Terminal Modes', () => {
14031514
test('should detect bracketed paste mode', async () => {
14041515
if (typeof document === 'undefined') return;

lib/terminal.ts

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
import { BufferNamespace } from './buffer';
1919
import { EventEmitter } from './event-emitter';
20-
import type { Ghostty, GhosttyCell, GhosttyTerminal } from './ghostty';
20+
import type { Ghostty, GhosttyCell, GhosttyTerminal, GhosttyTerminalConfig } from './ghostty';
2121
import { getGhostty } from './index';
2222
import { InputHandler } from './input-handler';
2323
import type {
@@ -220,6 +220,78 @@ export class Terminal implements ITerminalCore {
220220
}
221221
}
222222

223+
/**
224+
* Parse a CSS color string to 0xRRGGBB format.
225+
* Returns 0 if the color is undefined or invalid.
226+
*/
227+
private parseColorToHex(color?: string): number {
228+
if (!color) return 0;
229+
230+
// Handle hex colors (#RGB, #RRGGBB)
231+
if (color.startsWith('#')) {
232+
let hex = color.slice(1);
233+
if (hex.length === 3) {
234+
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
235+
}
236+
const value = Number.parseInt(hex, 16);
237+
return Number.isNaN(value) ? 0 : value;
238+
}
239+
240+
// Handle rgb(r, g, b) format
241+
const match = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
242+
if (match) {
243+
const r = Number.parseInt(match[1], 10);
244+
const g = Number.parseInt(match[2], 10);
245+
const b = Number.parseInt(match[3], 10);
246+
return (r << 16) | (g << 8) | b;
247+
}
248+
249+
return 0;
250+
}
251+
252+
/**
253+
* Convert terminal options to WASM terminal config.
254+
*/
255+
private buildWasmConfig(): GhosttyTerminalConfig | undefined {
256+
const theme = this.options.theme;
257+
const scrollback = this.options.scrollback;
258+
259+
// If no theme and default scrollback, use defaults
260+
if (!theme && scrollback === 1000) {
261+
return undefined;
262+
}
263+
264+
// Build palette array from theme colors
265+
// Order: black, red, green, yellow, blue, magenta, cyan, white,
266+
// brightBlack, brightRed, brightGreen, brightYellow, brightBlue, brightMagenta, brightCyan, brightWhite
267+
const palette: number[] = [
268+
this.parseColorToHex(theme?.black),
269+
this.parseColorToHex(theme?.red),
270+
this.parseColorToHex(theme?.green),
271+
this.parseColorToHex(theme?.yellow),
272+
this.parseColorToHex(theme?.blue),
273+
this.parseColorToHex(theme?.magenta),
274+
this.parseColorToHex(theme?.cyan),
275+
this.parseColorToHex(theme?.white),
276+
this.parseColorToHex(theme?.brightBlack),
277+
this.parseColorToHex(theme?.brightRed),
278+
this.parseColorToHex(theme?.brightGreen),
279+
this.parseColorToHex(theme?.brightYellow),
280+
this.parseColorToHex(theme?.brightBlue),
281+
this.parseColorToHex(theme?.brightMagenta),
282+
this.parseColorToHex(theme?.brightCyan),
283+
this.parseColorToHex(theme?.brightWhite),
284+
];
285+
286+
return {
287+
scrollbackLimit: scrollback,
288+
fgColor: this.parseColorToHex(theme?.foreground),
289+
bgColor: this.parseColorToHex(theme?.background),
290+
cursorColor: this.parseColorToHex(theme?.cursor),
291+
palette,
292+
};
293+
}
294+
223295
// ==========================================================================
224296
// Lifecycle Methods
225297
// ==========================================================================
@@ -259,8 +331,9 @@ export class Terminal implements ITerminalCore {
259331
parent.setAttribute('aria-label', 'Terminal input');
260332
parent.setAttribute('aria-multiline', 'true');
261333

262-
// Create WASM terminal with current dimensions
263-
this.wasmTerm = this.ghostty!.createTerminal(this.cols, this.rows);;
334+
// Create WASM terminal with current dimensions and config
335+
const config = this.buildWasmConfig();
336+
this.wasmTerm = this.ghostty!.createTerminal(this.cols, this.rows, config);
264337

265338
// Create canvas element
266339
this.canvas = document.createElement('canvas');
@@ -560,7 +633,8 @@ export class Terminal implements ITerminalCore {
560633
if (this.wasmTerm) {
561634
this.wasmTerm.free();
562635
}
563-
this.wasmTerm = this.ghostty!.createTerminal(this.cols, this.rows);
636+
const config = this.buildWasmConfig();
637+
this.wasmTerm = this.ghostty!.createTerminal(this.cols, this.rows, config);
564638

565639
// Clear renderer
566640
this.renderer!.clear();

lib/types.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@ export interface GhosttyWasmExports extends WebAssembly.Exports {
344344

345345
// Terminal lifecycle
346346
ghostty_terminal_new(cols: number, rows: number): TerminalHandle;
347+
ghostty_terminal_new_with_config(cols: number, rows: number, configPtr: number): TerminalHandle;
347348
ghostty_terminal_free(terminal: TerminalHandle): void;
348349
ghostty_terminal_resize(terminal: TerminalHandle, cols: number, rows: number): void;
349350
ghostty_terminal_write(terminal: TerminalHandle, dataPtr: number, dataLen: number): void;
@@ -427,14 +428,23 @@ export const CURSOR_STRUCT_SIZE = 8;
427428
*/
428429
export const COLORS_STRUCT_SIZE = 12;
429430

430-
// Legacy - kept for compatibility but not used with new API
431+
/**
432+
* Terminal configuration (passed to ghostty_terminal_new_with_config)
433+
* All color values use 0xRRGGBB format. A value of 0 means "use default".
434+
*/
431435
export interface GhosttyTerminalConfig {
432436
scrollbackLimit?: number;
433437
fgColor?: number;
434438
bgColor?: number;
435439
cursorColor?: number;
436440
palette?: number[];
437441
}
442+
443+
/**
444+
* Size of GhosttyTerminalConfig struct in WASM memory (bytes).
445+
* Layout: scrollback_limit(u32) + fg_color(u32) + bg_color(u32) + cursor_color(u32) + palette[16](u32*16)
446+
* Total: 4 + 4 + 4 + 4 + 64 = 80 bytes
447+
*/
438448
export const GHOSTTY_CONFIG_SIZE = 80;
439449

440450
/**

0 commit comments

Comments
 (0)