feat: upgrade to ghostty-web 0.4.0-ime-fix from rcarmo/ghostty-web

- Vendor patched version with native theme/palette support at WASM level
- Remove color remapping patches (no longer needed)
- Pre-load Ghostty WASM before terminal creation
- Bundle size reduced from 1.16 MB to 0.67 MB
- Includes IME input fixes

Bump version to 0.7.0
This commit is contained in:
GitHub Copilot
2026-01-28 11:15:09 +00:00
parent 05d42d275f
commit b6bb994e8e
6 changed files with 58 additions and 196 deletions
+33
View File
@@ -0,0 +1,33 @@
{
"name": "textual-webterm-frontend",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "textual-webterm-frontend",
"dependencies": {
"ghostty-web": "github:rcarmo/ghostty-web"
},
"devDependencies": {
"typescript": "^5.7.0"
}
},
"node_modules/ghostty-web": {
"version": "0.4.0-ime-fix",
"resolved": "git+ssh://git@github.com/rcarmo/ghostty-web.git#50fc9127151f7d9d20d5c7bfaea8a6dba8b15bf5",
"license": "MIT"
},
"node_modules/typescript": {
"version": "5.9.3",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
}
}
}
+1 -1
View File
@@ -3,7 +3,7 @@
"private": true,
"type": "module",
"dependencies": {
"ghostty-web": "^0.1.0"
"ghostty-web": "github:rcarmo/ghostty-web"
},
"devDependencies": {
"typescript": "^5.7.0"
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "textual-webterm"
version = "0.6.9"
version = "0.7.0"
description = "Serve terminal sessions over the web"
authors = ["Will McGugan <will@textualize.io>"]
license = "MIT"
Binary file not shown.
File diff suppressed because one or more lines are too long
+8 -189
View File
@@ -6,7 +6,7 @@
* - Server → Client: ["stdout", data], ["pong", data], or binary frames
*/
import { Terminal, FitAddon, type ITerminalOptions, type ITheme } from "ghostty-web";
import { Terminal, FitAddon, Ghostty, type ITerminalOptions, type ITheme } from "ghostty-web";
/** Default font stack - prefers system monospace, falls back through programming fonts */
const DEFAULT_FONT_FAMILY =
@@ -275,187 +275,6 @@ const THEMES: Record<string, ITheme> = {
},
};
/**
* ghostty-web's internal default palette (Tomorrow Night theme).
* 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: "#c5c8c6",
background: "#1d1f21",
cursor: "#c5c8c6",
cursorAccent: "#1d1f21",
selectionBackground: "#373b41",
black: "#1d1f21",
red: "#cc6666",
green: "#b5bd68",
yellow: "#f0c674",
blue: "#81a2be",
magenta: "#b294bb",
cyan: "#8abeb7",
white: "#c5c8c6",
brightBlack: "#969896",
brightRed: "#cc6666",
brightGreen: "#b5bd68",
brightYellow: "#f0c674",
brightBlue: "#81a2be",
brightMagenta: "#b294bb",
brightCyan: "#8abeb7",
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),
];
}
/**
* Build a fast lookup table for color remapping.
* Uses a flat Uint8Array indexed by packed RGB (r << 16 | g << 8 | b) % tableSize.
* Returns null for unmapped colors (non-palette colors like 256-color/truecolor).
*/
function buildColorLookup(theme: ITheme): {
lookup: Uint32Array;
hasMapping: Uint8Array;
} {
// Use a hash table with separate chaining avoided by using unique keys
// Since we only have 18 colors, a small table with direct indexing works
const TABLE_SIZE = 65536; // 2^16 - good for sparse lookups
const lookup = new Uint32Array(TABLE_SIZE);
const hasMapping = new Uint8Array(TABLE_SIZE);
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 [dr, dg, db] = hexToRgb(defaultColor);
const [tr, tg, tb] = hexToRgb(themeColor);
// Hash: use lower bits of combined RGB
const hash = ((dr * 31 + dg) * 31 + db) & (TABLE_SIZE - 1);
// Pack theme RGB into uint32: r | g << 8 | b << 16
lookup[hash] = tr | (tg << 8) | (tb << 16);
hasMapping[hash] = 1;
}
}
return { lookup, hasMapping };
}
/** Compute hash for RGB color (must match buildColorLookup) */
function colorHash(r: number, g: number, b: number): number {
return ((r * 31 + g) * 31 + b) & 65535;
}
/**
* Patch the WASM terminal's getLine method to remap colors at the source.
* This is more efficient than patching renderCell since it processes
* colors once per line fetch rather than once per cell render.
*/
function patchWasmColors(terminal: Terminal, theme: ITheme): void {
const internalTerminal = terminal as unknown as Record<string, unknown>;
const wasmTerminal = internalTerminal.terminal as Record<string, unknown> | undefined;
if (!wasmTerminal) {
console.warn("[webterm:patch] No WASM terminal found, cannot patch colors");
return;
}
const { lookup, hasMapping } = buildColorLookup(theme);
console.log("[webterm:patch] Built optimized color lookup table");
// Patch getLine to remap colors in returned cells
const originalGetLine = wasmTerminal.getLine as (row: number) => Array<{
fg_r: number; fg_g: number; fg_b: number;
bg_r: number; bg_g: number; bg_b: number;
[key: string]: unknown;
}> | null;
if (!originalGetLine) {
console.warn("[webterm:patch] No getLine method found on WASM terminal");
return;
}
wasmTerminal.getLine = function(row: number) {
const cells = originalGetLine.call(this, row);
if (!cells) return cells;
// Remap colors in-place for performance
for (let i = 0; i < cells.length; i++) {
const cell = cells[i];
// Remap foreground
const fgHash = colorHash(cell.fg_r, cell.fg_g, cell.fg_b);
if (hasMapping[fgHash]) {
const packed = lookup[fgHash];
cell.fg_r = packed & 0xff;
cell.fg_g = (packed >> 8) & 0xff;
cell.fg_b = (packed >> 16) & 0xff;
}
// Remap background
const bgHash = colorHash(cell.bg_r, cell.bg_g, cell.bg_b);
if (hasMapping[bgHash]) {
const packed = lookup[bgHash];
cell.bg_r = packed & 0xff;
cell.bg_g = (packed >> 8) & 0xff;
cell.bg_b = (packed >> 16) & 0xff;
}
}
return cells;
};
// Also patch getScrollbackLine for scrollback rendering
const originalGetScrollbackLine = wasmTerminal.getScrollbackLine as (offset: number) => Array<{
fg_r: number; fg_g: number; fg_b: number;
bg_r: number; bg_g: number; bg_b: number;
[key: string]: unknown;
}> | null;
if (originalGetScrollbackLine) {
wasmTerminal.getScrollbackLine = function(offset: number) {
const cells = originalGetScrollbackLine.call(this, offset);
if (!cells) return cells;
for (let i = 0; i < cells.length; i++) {
const cell = cells[i];
const fgHash = colorHash(cell.fg_r, cell.fg_g, cell.fg_b);
if (hasMapping[fgHash]) {
const packed = lookup[fgHash];
cell.fg_r = packed & 0xff;
cell.fg_g = (packed >> 8) & 0xff;
cell.fg_b = (packed >> 16) & 0xff;
}
const bgHash = colorHash(cell.bg_r, cell.bg_g, cell.bg_b);
if (hasMapping[bgHash]) {
const packed = lookup[bgHash];
cell.bg_r = packed & 0xff;
cell.bg_g = (packed >> 8) & 0xff;
cell.bg_b = (packed >> 16) & 0xff;
}
}
return cells;
};
}
console.log("[webterm:patch] Successfully patched WASM getLine/getScrollbackLine for theme colors");
}
/** Configuration options passed via data attributes or window config */
interface TerminalConfig {
@@ -580,9 +399,12 @@ class WebTerminal {
console.log("[webterm:create] wsUrl:", wsUrl);
console.log("[webterm:create] Config received:", JSON.stringify(config, null, 2));
// Determine WASM path - try to find it relative to the script location
// Determine WASM path and pre-load Ghostty
const wasmPath = getWasmPath();
console.log("[webterm:create] WASM path:", wasmPath);
console.log("[webterm:create] Loading Ghostty WASM...");
const ghostty = await Ghostty.load(wasmPath);
console.log("[webterm:create] Ghostty loaded:", ghostty);
// Build terminal options
const themeToUse = config.theme ?? THEMES.xterm;
@@ -595,7 +417,7 @@ class WebTerminal {
cursorBlink: true,
cursorStyle: "block",
theme: themeToUse,
wasmPath,
ghostty,
};
console.log("[webterm:create] Full ITerminalOptions:", JSON.stringify(options, null, 2));
@@ -609,14 +431,11 @@ class WebTerminal {
console.log("[webterm:create] Loading FitAddon into terminal...");
terminal.loadAddon(fitAddon);
// Open terminal (this loads WASM and initializes everything)
// Open terminal (initializes rendering - WASM already loaded)
console.log("[webterm:create] Calling terminal.open(container)...");
await terminal.open(container);
terminal.open(container);
console.log("[webterm:create] terminal.open() completed");
// Patch WASM terminal to remap colors from ghostty-web's default palette to our theme
patchWasmColors(terminal, themeToUse);
// Check internal state after open
const internalTerminal = terminal as unknown as Record<string, unknown>;
console.log("[webterm:create] Terminal internal keys:", Object.keys(internalTerminal));