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
46 changes: 46 additions & 0 deletions apps/web/src/components/ThreadTerminalDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,51 @@ function parseTerminalColor(value: string, fallback: GhosttyColor): GhosttyColor
};
}

function hexToGhosttyColor(hex: string): GhosttyColor {
const value = Number.parseInt(hex.slice(1), 16);
return { r: (value >> 16) & 255, g: (value >> 8) & 255, b: value & 255 };
}

// VS Code's default ANSI colors.
const TERMINAL_PALETTES = {
light: [
"#000000",
"#cd3131",
"#107c10",
"#949800",
"#0451a5",
"#bc05bc",
"#0598bc",
"#555555",
"#666666",
"#cd3131",
"#14ce14",
"#b5ba00",
"#0451a5",
"#bc05bc",
"#0598bc",
"#a5a5a5",
].map(hexToGhosttyColor),
dark: [
"#000000",
"#cd3131",
"#0dbc79",
"#e5e510",
"#2472c8",
"#bc3fbc",
"#11a8cd",
"#e5e5e5",
"#666666",
"#f14c4c",
"#23d18b",
"#f5f543",
"#3b8eea",
"#d670d6",
"#29b8db",
"#e5e5e5",
].map(hexToGhosttyColor),
};

function runtimeEnvSignature(runtimeEnv: Record<string, string> | undefined): string {
if (!runtimeEnv) return "";
return JSON.stringify(
Expand Down Expand Up @@ -249,6 +294,7 @@ export function terminalThemeFromApp(mountElement?: HTMLElement | null): Ghostty
terminalCursor,
isDark ? { r: 180, g: 203, b: 255 } : { r: 38, g: 56, b: 78 },
),
palette: TERMINAL_PALETTES[isDark ? "dark" : "light"],
selectionBackground: terminalSelection,
};
}
Expand Down
20 changes: 20 additions & 0 deletions apps/web/src/terminal/ghostty/core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,26 @@ describe("GhosttyTerminalCore snapshots", () => {
]);
});

it("paints ANSI colors from the theme palette and repaints them on theme change", async () => {
const core = await createCore();
const theme = {
foreground: { r: 255, g: 255, b: 255 },
background: { r: 0, g: 0, b: 0 },
cursor: { r: 255, g: 255, b: 255 },
};
const palette = Array.from({ length: 16 }, (_, index) => ({ r: index, g: 1, b: 2 }));
core.write("\x1b[31mR\x1b[38;5;196mX\x1b[0m");
core.setTheme({ ...theme, palette });
let cells = core.snapshot().rowData[0]!.cells;
expect(cells[0]!.foreground).toEqual({ r: 1, g: 1, b: 2 });
expect(cells[1]!.foreground).toEqual({ r: 255, g: 0, b: 0 });

core.setTheme({ ...theme, palette: palette.map((color) => ({ ...color, g: 9 })) });
cells = core.snapshot().rowData[0]!.cells;
expect(cells[0]!.foreground).toEqual({ r: 1, g: 9, b: 2 });
expect(cells[1]!.foreground).toEqual({ r: 255, g: 0, b: 0 });
});

it("reuses a grown grapheme buffer and releases it on disposal", async () => {
const core = await createCore();
const runtime = await loadGhosttyRuntime();
Expand Down
15 changes: 15 additions & 0 deletions apps/web/src/terminal/ghostty/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const MAX_SCROLLBACK_ROWS = 10_000;
// wasm32 C ABI layout for GhosttyTerminalSelectionFormatOptions at the
// libghostty-vt revision pinned alongside this module.
const SELECTION_FORMAT_OPTIONS_SIZE = 16;
const PALETTE_BYTES = 256 * 3;

const RENDER_DATA = {
cols: 1,
Expand Down Expand Up @@ -81,6 +82,7 @@ export interface GhosttyTheme {
readonly foreground: GhosttyColor;
readonly background: GhosttyColor;
readonly cursor: GhosttyColor;
readonly palette?: readonly GhosttyColor[];
/** CSS color the renderer overlays on selected cells; not sent to Ghostty. */
readonly selectionBackground?: string;
}
Expand Down Expand Up @@ -415,6 +417,19 @@ export class GhosttyTerminalCore {
this.runtime.call("ghostty_terminal_set", this.terminal, option, color);
}
this.runtime.free(color, 3);
if (theme.palette) this.setPalette(theme.palette);
}

private setPalette(colors: readonly GhosttyColor[]): void {
// Ghostty takes all 256 entries; keep its defaults for 16-255.
const palette = this.runtime.alloc(PALETTE_BYTES);
this.runtime.call("ghostty_terminal_get", this.terminal, 25, palette);
const bytes = this.runtime.bytes(palette, PALETTE_BYTES);
colors.slice(0, 16).forEach((color, index) => {
bytes.set([color.r, color.g, color.b], index * 3);
});
this.runtime.call("ghostty_terminal_set", this.terminal, 14, palette);
this.runtime.free(palette, PALETTE_BYTES);
}

scroll(deltaRows: number): void {
Expand Down
Loading