Add runtime palette patch for ghostty-web theme colors
- Add color mapping from ghostty-web's default palette to custom themes - Monkey-patch renderer.renderCell to remap fg/bg colors at runtime - Fix THEME_BACKGROUNDS keys to match terminal.ts theme names - Add debug logging for color remapping verification This works around ghostty-web's hardcoded WASM palette by intercepting cell colors before rendering and remapping them to the configured theme.
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "textual-webterm"
|
name = "textual-webterm"
|
||||||
version = "0.6.6"
|
version = "0.6.7"
|
||||||
description = "Serve terminal sessions over the web"
|
description = "Serve terminal sessions over the web"
|
||||||
authors = ["Will McGugan <will@textualize.io>"]
|
authors = ["Will McGugan <will@textualize.io>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -48,9 +48,9 @@ THEME_BACKGROUNDS: dict[str, str] = {
|
|||||||
"dracula": "#282a36",
|
"dracula": "#282a36",
|
||||||
"catppuccin": "#1e1e2e",
|
"catppuccin": "#1e1e2e",
|
||||||
"nord": "#2e3440",
|
"nord": "#2e3440",
|
||||||
"solarized-dark": "#002b36",
|
"gruvbox": "#282828",
|
||||||
"solarized-light": "#fdf6e3",
|
"solarized": "#002b36",
|
||||||
"tokyo-night": "#1a1b26",
|
"tokyo": "#1a1b26",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -275,6 +275,162 @@ const THEMES: Record<string, ITheme> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ghostty-web's internal default palette (from ghostty-web.js const q).
|
||||||
|
* This is what the WASM terminal uses to resolve ANSI color codes to RGB.
|
||||||
|
* We need to know these to remap them to our custom theme.
|
||||||
|
*/
|
||||||
|
const GHOSTTY_DEFAULT_PALETTE: ITheme = {
|
||||||
|
foreground: "#d4d4d4",
|
||||||
|
background: "#1e1e1e",
|
||||||
|
cursor: "#ffffff",
|
||||||
|
cursorAccent: "#1e1e1e",
|
||||||
|
selectionBackground: "#264f78",
|
||||||
|
selectionForeground: "#d4d4d4",
|
||||||
|
black: "#000000",
|
||||||
|
red: "#cd3131",
|
||||||
|
green: "#0dbc79",
|
||||||
|
yellow: "#e5e510",
|
||||||
|
blue: "#2472c8",
|
||||||
|
magenta: "#bc3fbc",
|
||||||
|
cyan: "#11a8cd",
|
||||||
|
white: "#e5e5e5",
|
||||||
|
brightBlack: "#666666",
|
||||||
|
brightRed: "#f14c4c",
|
||||||
|
brightGreen: "#23d18b",
|
||||||
|
brightYellow: "#f5f543",
|
||||||
|
brightBlue: "#3b8eea",
|
||||||
|
brightMagenta: "#d670d6",
|
||||||
|
brightCyan: "#29b8db",
|
||||||
|
brightWhite: "#ffffff",
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Convert hex color to RGB tuple */
|
||||||
|
function hexToRgb(hex: string): [number, number, number] {
|
||||||
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||||
|
if (!result) return [0, 0, 0];
|
||||||
|
return [
|
||||||
|
parseInt(result[1], 16),
|
||||||
|
parseInt(result[2], 16),
|
||||||
|
parseInt(result[3], 16),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a color map from default palette RGB to theme palette RGB */
|
||||||
|
function buildColorMap(theme: ITheme): Map<string, [number, number, number]> {
|
||||||
|
const colorMap = new Map<string, [number, number, number]>();
|
||||||
|
|
||||||
|
// Map each default color to the corresponding theme color
|
||||||
|
const colorKeys: (keyof ITheme)[] = [
|
||||||
|
'foreground', 'background',
|
||||||
|
'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white',
|
||||||
|
'brightBlack', 'brightRed', 'brightGreen', 'brightYellow',
|
||||||
|
'brightBlue', 'brightMagenta', 'brightCyan', 'brightWhite',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const key of colorKeys) {
|
||||||
|
const defaultColor = GHOSTTY_DEFAULT_PALETTE[key];
|
||||||
|
const themeColor = theme[key];
|
||||||
|
if (defaultColor && themeColor) {
|
||||||
|
const defaultRgb = hexToRgb(defaultColor);
|
||||||
|
const themeRgb = hexToRgb(themeColor);
|
||||||
|
// Key is "r,g,b" string for fast lookup
|
||||||
|
const keyStr = `${defaultRgb[0]},${defaultRgb[1]},${defaultRgb[2]}`;
|
||||||
|
colorMap.set(keyStr, themeRgb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return colorMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Patch the renderer to remap colors from ghostty-web's default palette
|
||||||
|
* to our custom theme palette.
|
||||||
|
*
|
||||||
|
* This is necessary because ghostty-web's WASM terminal has a hardcoded
|
||||||
|
* palette and returns pre-resolved RGB values. The theme we pass to the
|
||||||
|
* renderer is only used for background/cursor/selection, not for text colors.
|
||||||
|
*/
|
||||||
|
function patchRendererColors(terminal: Terminal, theme: ITheme): void {
|
||||||
|
const internalTerminal = terminal as unknown as Record<string, unknown>;
|
||||||
|
const renderer = internalTerminal.renderer as Record<string, unknown> | undefined;
|
||||||
|
|
||||||
|
if (!renderer) {
|
||||||
|
console.warn("[webterm:patch] No renderer found, cannot patch colors");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const colorMap = buildColorMap(theme);
|
||||||
|
console.log("[webterm:patch] Built color map with", colorMap.size, "entries");
|
||||||
|
// Log a few mappings for debugging
|
||||||
|
for (const [key, value] of colorMap.entries()) {
|
||||||
|
console.log(`[webterm:patch] Color map: ${key} -> ${value.join(",")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store original renderCell method
|
||||||
|
const originalRenderCell = renderer.renderCell as (
|
||||||
|
cell: { fg_r: number; fg_g: number; fg_b: number; bg_r: number; bg_g: number; bg_b: number; [key: string]: unknown },
|
||||||
|
col: number,
|
||||||
|
row: number
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
if (!originalRenderCell) {
|
||||||
|
console.warn("[webterm:patch] No renderCell method found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let patchLogCount = 0;
|
||||||
|
const maxPatchLogs = 20;
|
||||||
|
const seenColors = new Set<string>();
|
||||||
|
|
||||||
|
// Patch renderCell to remap colors
|
||||||
|
renderer.renderCell = function(
|
||||||
|
cell: { fg_r: number; fg_g: number; fg_b: number; bg_r: number; bg_g: number; bg_b: number; [key: string]: unknown },
|
||||||
|
col: number,
|
||||||
|
row: number
|
||||||
|
) {
|
||||||
|
// Log unique colors we see from WASM (for debugging)
|
||||||
|
const fgKey = `${cell.fg_r},${cell.fg_g},${cell.fg_b}`;
|
||||||
|
const bgKey = `${cell.bg_r},${cell.bg_g},${cell.bg_b}`;
|
||||||
|
|
||||||
|
if (patchLogCount < maxPatchLogs) {
|
||||||
|
if (!seenColors.has(`fg:${fgKey}`)) {
|
||||||
|
seenColors.add(`fg:${fgKey}`);
|
||||||
|
const inMap = colorMap.has(fgKey);
|
||||||
|
console.log(`[webterm:patch] WASM fg color: ${fgKey} (in map: ${inMap})`);
|
||||||
|
patchLogCount++;
|
||||||
|
}
|
||||||
|
if (!seenColors.has(`bg:${bgKey}`)) {
|
||||||
|
seenColors.add(`bg:${bgKey}`);
|
||||||
|
const inMap = colorMap.has(bgKey);
|
||||||
|
console.log(`[webterm:patch] WASM bg color: ${bgKey} (in map: ${inMap})`);
|
||||||
|
patchLogCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remap foreground color
|
||||||
|
const mappedFg = colorMap.get(fgKey);
|
||||||
|
if (mappedFg) {
|
||||||
|
cell.fg_r = mappedFg[0];
|
||||||
|
cell.fg_g = mappedFg[1];
|
||||||
|
cell.fg_b = mappedFg[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remap background color
|
||||||
|
const mappedBg = colorMap.get(bgKey);
|
||||||
|
if (mappedBg) {
|
||||||
|
cell.bg_r = mappedBg[0];
|
||||||
|
cell.bg_g = mappedBg[1];
|
||||||
|
cell.bg_b = mappedBg[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call original method with remapped colors
|
||||||
|
return originalRenderCell.call(this, cell, col, row);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("[webterm:patch] Successfully patched renderer.renderCell for theme colors");
|
||||||
|
}
|
||||||
|
|
||||||
/** Configuration options passed via data attributes or window config */
|
/** Configuration options passed via data attributes or window config */
|
||||||
interface TerminalConfig {
|
interface TerminalConfig {
|
||||||
fontFamily?: string;
|
fontFamily?: string;
|
||||||
@@ -432,6 +588,9 @@ class WebTerminal {
|
|||||||
await terminal.open(container);
|
await terminal.open(container);
|
||||||
console.log("[webterm:create] terminal.open() completed");
|
console.log("[webterm:create] terminal.open() completed");
|
||||||
|
|
||||||
|
// Patch renderer to remap colors from ghostty-web's default palette to our theme
|
||||||
|
patchRendererColors(terminal, themeToUse);
|
||||||
|
|
||||||
// Check internal state after open
|
// Check internal state after open
|
||||||
const internalTerminal = terminal as unknown as Record<string, unknown>;
|
const internalTerminal = terminal as unknown as Record<string, unknown>;
|
||||||
console.log("[webterm:create] Terminal internal keys:", Object.keys(internalTerminal));
|
console.log("[webterm:create] Terminal internal keys:", Object.keys(internalTerminal));
|
||||||
|
|||||||
Reference in New Issue
Block a user