From f3d860346d0b4e5de730f643db2bf650f47b89c7 Mon Sep 17 00:00:00 2001 From: alexma233 Date: Sat, 1 Aug 2026 23:18:11 +0800 Subject: [PATCH] fix(web): use raw wterm with scrollback --- deno.lock | 21 +-- web/e2e/navigation.spec.ts | 13 +- web/package.json | 3 +- web/scripts/check-bundle-budget.ts | 1 - web/src/app.css | 97 +++++------ web/src/lib/components/app/code-editor.svelte | 11 +- .../components/app/terminal-surface.svelte | 89 ++++------ web/src/lib/wterm/ghostty-core-patch.ts | 160 ------------------ 8 files changed, 96 insertions(+), 299 deletions(-) delete mode 100644 web/src/lib/wterm/ghostty-core-patch.ts diff --git a/deno.lock b/deno.lock index e71edaa..1317c23 100644 --- a/deno.lock +++ b/deno.lock @@ -27,8 +27,8 @@ "npm:@sveltejs/vite-plugin-svelte@7.2.0": "7.2.0_svelte@5.56.8_vite@8.1.5__@types+node@26.1.1__yaml@2.9.0", "npm:@tailwindcss/vite@4.3.3": "4.3.3_vite@8.1.5__@types+node@26.1.1__yaml@2.9.0_@types+node@26.1.1_yaml@2.9.0", "npm:@types/node@26.1.1": "26.1.1", - "npm:@wterm/dom@0.3.0": "0.3.0", - "npm:@wterm/ghostty@0.3.0": "0.3.0", + "npm:@wterm/core@0.3.1": "0.3.1", + "npm:@wterm/dom@0.3.1": "0.3.1", "npm:ajv@8.20.0": "8.20.0", "npm:bits-ui@2.18.1": "2.18.1_svelte@5.56.8", "npm:codemirror@6.0.2": "6.0.2", @@ -935,17 +935,11 @@ "typescript@5.4.5" ] }, - "@wterm/core@0.3.0": { - "integrity": "sha512-aQ73QBP+eyA3kcn31mA7DmAi7qmEsQijZdmvgwxtSjCi2248EAOZgtkGDpjlspfrxyVxQbUl/IF+gCVcXt4nZQ==" + "@wterm/core@0.3.1": { + "integrity": "sha512-Ml4QMy2gj8pXupkZcMX77gN9iYv2B1Mkkc4WkVydTSnJf2LvyLJ3YgtMwL+N5SfqP+XxHy905AP27kNQPS0eIw==" }, - "@wterm/dom@0.3.0": { - "integrity": "sha512-OiUl7reGSlW02yb5wGXMplMZhW2HBkBSFxvbFj15I832UK0QFIHRvhE2l6Xgae8ROn9bNLaALoKDFvcUuz80iQ==", - "dependencies": [ - "@wterm/core" - ] - }, - "@wterm/ghostty@0.3.0": { - "integrity": "sha512-TDjEmUSRD7IaFxq31P7+I3Inkp5mHCfBfO6LYj3/vbLbWOkOZOKPhjAcZ36AYFHe9Q0HLCFvLWmrxIO3aGCjDA==", + "@wterm/dom@0.3.1": { + "integrity": "sha512-UUgmfGak0WvWqPuIgtNhsbeRPl4F822ITP7yMNKeHnr5mhYtIOCHdC5amjtvuKC59oMGOjurgoUzMsXF/6/EFg==", "dependencies": [ "@wterm/core" ] @@ -1569,8 +1563,7 @@ "npm:@sveltejs/vite-plugin-svelte@7.2.0", "npm:@tailwindcss/vite@4.3.3", "npm:@types/node@26.1.1", - "npm:@wterm/dom@0.3.0", - "npm:@wterm/ghostty@0.3.0", + "npm:@wterm/dom@0.3.1", "npm:ajv@8.20.0", "npm:bits-ui@2.18.1", "npm:codemirror@6.0.2", diff --git a/web/e2e/navigation.spec.ts b/web/e2e/navigation.spec.ts index d690282..2f6a33c 100644 --- a/web/e2e/navigation.spec.ts +++ b/web/e2e/navigation.spec.ts @@ -115,26 +115,19 @@ test("service editor has an accessible name", async ({ page }) => { ).toBeVisible(); }); -test("terminal runtime loads only after opening the terminal", async ({ +test("terminal initializes only after opening the terminal", async ({ page, }) => { - const terminalRequests: string[] = []; - page.on("request", (request) => { - if (request.url().includes("ghostty") || request.url().endsWith(".wasm")) { - terminalRequests.push(request.url()); - } - }); - await page.goto("/nodes/main/docker/containers"); await page .locator('a[href^="/nodes/main/docker/containers/"]') .first() .click(); await expect(page.getByRole("tab", { name: "Info" })).toBeVisible(); - expect(terminalRequests).toEqual([]); + await expect(page.locator(".wterm")).toHaveCount(0); await page.getByRole("tab", { name: "Terminal" }).click(); - await expect.poll(() => terminalRequests.length).toBeGreaterThan(0); + await expect(page.locator(".wterm")).toBeVisible(); }); test("locale persists through SSR reloads", async ({ page }) => { diff --git a/web/package.json b/web/package.json index 94a9a56..4a57a26 100644 --- a/web/package.json +++ b/web/package.json @@ -53,8 +53,7 @@ "@lezer/common": "1.5.2", "@lezer/highlight": "1.2.3", "@lezer/lr": "1.4.10", - "@wterm/dom": "0.3.0", - "@wterm/ghostty": "0.3.0", + "@wterm/dom": "0.3.1", "ajv": "8.20.0", "codemirror": "6.0.2", "dequal": "2.0.3", diff --git a/web/scripts/check-bundle-budget.ts b/web/scripts/check-bundle-budget.ts index 363c31e..083dda9 100644 --- a/web/scripts/check-bundle-budget.ts +++ b/web/scripts/check-bundle-budget.ts @@ -82,7 +82,6 @@ const initialEntries = dependencies([ for (const pattern of [ "src/lib/components/app/code-editor.svelte", "@wterm+dom", - "@wterm+ghostty", "src/lib/i18n/messages/", ]) { if ([...initialEntries].some((entry) => entry.includes(pattern))) { diff --git a/web/src/app.css b/web/src/app.css index 7e34042..d4f8d4e 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -6,6 +6,12 @@ :root { --font-sans: ui-sans-serif, system-ui, sans-serif; + --code-font-family: "JetBrains Mono", Consolas, monospace; + --code-font-size: 14px; + --code-line-height: 1.6; + --code-background: #ffffff; + --code-foreground: #24292e; + --code-cursor: #24292e; --background: oklch(1 0 0); --foreground: oklch(0.145 0 0); --card: oklch(1 0 0); @@ -51,6 +57,9 @@ } .dark { + --code-background: #0d1117; + --code-foreground: #c9d1d9; + --code-cursor: #ffffff; --background: oklch(0.145 0 0); --foreground: oklch(0.985 0 0); --card: oklch(0.205 0 0); @@ -351,30 +360,28 @@ } .terminal-surface .wterm { - --term-font-family: - ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, - monospace; - --term-font-size: 12px; - --term-line-height: 1.45; - --term-bg: #ffffff; - --term-fg: #24292f; - --term-cursor: #0969da; - --term-color-0: #24292f; - --term-color-1: #cf222e; - --term-color-2: #116329; - --term-color-3: #4d2d00; - --term-color-4: #0969da; - --term-color-5: #8250df; - --term-color-6: #1b7c83; - --term-color-7: #d0d7de; - --term-color-8: #6e7781; - --term-color-9: #a40e26; - --term-color-10: #1a7f37; - --term-color-11: #9a6700; - --term-color-12: #0550ae; - --term-color-13: #6639ba; - --term-color-14: #3192aa; - --term-color-15: #57606a; + --term-font-family: var(--code-font-family); + --term-font-size: var(--code-font-size); + --term-line-height: var(--code-line-height); + --term-bg: var(--code-background); + --term-fg: var(--code-foreground); + --term-cursor: var(--code-cursor); + --term-color-0: #3e3e3e; + --term-color-1: #970b16; + --term-color-2: #07962a; + --term-color-3: #c5bb94; + --term-color-4: #003e8a; + --term-color-5: #e94691; + --term-color-6: #7cc4df; + --term-color-7: #b2b2b2; + --term-color-8: #666666; + --term-color-9: #de0000; + --term-color-10: #7ac895; + --term-color-11: #d7b600; + --term-color-12: #2e6cba; + --term-color-13: #f29592; + --term-color-14: #00c7cb; + --term-color-15: #ffffff; box-sizing: border-box; width: 100%; height: 100%; @@ -393,35 +400,23 @@ will-change: auto; } - .dark .terminal-surface .wterm, - .terminal-surface .wterm.theme-composia-dark { - --term-bg: #0d1117; - --term-fg: #c9d1d9; - --term-cursor: #58a6ff; - --term-color-0: #484f58; - --term-color-1: #ff7b72; - --term-color-2: #7ee787; - --term-color-3: #d29922; - --term-color-4: #58a6ff; - --term-color-5: #bc8cff; - --term-color-6: #39c5cf; - --term-color-7: #b1bac4; - --term-color-8: #6e7681; - --term-color-9: #ffa198; + .dark .terminal-surface .wterm { + --term-color-0: #000000; + --term-color-1: #f78166; + --term-color-2: #56d364; + --term-color-3: #e3b341; + --term-color-4: #6ca4f8; + --term-color-5: #db61a2; + --term-color-6: #2b7489; + --term-color-7: #ffffff; + --term-color-8: #4d4d4d; + --term-color-9: #f78166; --term-color-10: #56d364; --term-color-11: #e3b341; - --term-color-12: #79c0ff; - --term-color-13: #d2a8ff; - --term-color-14: #56d4dd; - --term-color-15: #f0f6fc; - } - - @media (max-width: 640px) { - .terminal-surface .wterm { - --term-font-size: 11px; - --term-line-height: 1.4; - padding: 0.5rem; - } + --term-color-12: #6ca4f8; + --term-color-13: #db61a2; + --term-color-14: #2b7489; + --term-color-15: #ffffff; } .toolbar-surface { diff --git a/web/src/lib/components/app/code-editor.svelte b/web/src/lib/components/app/code-editor.svelte index 09bfe10..6670ddc 100644 --- a/web/src/lib/components/app/code-editor.svelte +++ b/web/src/lib/components/app/code-editor.svelte @@ -67,6 +67,10 @@ height: "100%", borderRadius: editorBorderRadius(), overflow: "hidden", + color: "var(--code-foreground)", + backgroundColor: "var(--code-background)", + fontFamily: "var(--code-font-family)", + fontSize: "var(--code-font-size)", }, "&.cm-focused": { outline: "none", @@ -76,6 +80,10 @@ }, ".cm-content": { minHeight: "100%", + lineHeight: "var(--code-line-height)", + }, + ".cm-cursor, .cm-dropCursor": { + borderLeftColor: "var(--code-cursor)", }, }); @@ -248,7 +256,8 @@ // CodeMirror packages can be duplicated by the lockfile, so normalize the // language-data type to the public LanguageDescription API used here. - const languageDescriptions = languages as unknown as readonly LanguageDescription[]; + const languageDescriptions = + languages as unknown as readonly LanguageDescription[]; return LanguageDescription.matchFilename(languageDescriptions, fileName); } diff --git a/web/src/lib/components/app/terminal-surface.svelte b/web/src/lib/components/app/terminal-surface.svelte index e3fc0f6..7273f4a 100644 --- a/web/src/lib/components/app/terminal-surface.svelte +++ b/web/src/lib/components/app/terminal-surface.svelte @@ -1,11 +1,9 @@
-
+
diff --git a/web/src/lib/wterm/ghostty-core-patch.ts b/web/src/lib/wterm/ghostty-core-patch.ts deleted file mode 100644 index cd1759a..0000000 --- a/web/src/lib/wterm/ghostty-core-patch.ts +++ /dev/null @@ -1,160 +0,0 @@ -import type { CellData, TerminalCore } from "@wterm/dom"; - -const DEFAULT_COLOR = 256; -const REVERSE_FLAG = 0x20; -const CELL_BYTES = 16; - -const BLANK_CELL: CellData = { - char: 32, - fg: DEFAULT_COLOR, - bg: DEFAULT_COLOR, - flags: 0, -}; - -export interface TerminalThemeColors { - background: number; - foreground: number; -} - -interface GhosttyWasmExports { - memory: WebAssembly.Memory; - get_scrollback_line( - termPtr: number, - offset: number, - bufPtr: number, - maxCols: number, - ): number; - alloc_buffer(length: number): number; - free_buffer(ptr: number, length: number): void; -} - -interface GhosttyCoreInternals { - wasm?: { exports: GhosttyWasmExports }; - termPtr?: number; -} - -interface WasmCellData { - codepoint: number; - fgR: number; - fgG: number; - fgB: number; - bgR: number; - bgG: number; - bgB: number; - flags: number; - colorFlags: number; -} - -function packRgb(r: number, g: number, b: number) { - return (r << 16) | (g << 8) | b; -} - -function parseCell(view: DataView, byteOffset: number): WasmCellData { - return { - codepoint: view.getUint32(byteOffset, true), - fgR: view.getUint8(byteOffset + 4), - fgG: view.getUint8(byteOffset + 5), - fgB: view.getUint8(byteOffset + 6), - bgR: view.getUint8(byteOffset + 7), - bgG: view.getUint8(byteOffset + 8), - bgB: view.getUint8(byteOffset + 9), - flags: view.getUint8(byteOffset + 10), - colorFlags: view.getUint8(byteOffset + 12), - }; -} - -function readScrollbackCell( - core: TerminalCore, - offset: number, - col: number, -): CellData | null { - const internals = core as unknown as GhosttyCoreInternals; - const wasm = internals.wasm; - const termPtr = internals.termPtr; - if (!wasm || !termPtr) { - return null; - } - - const maxCols = core.getCols(); - const lineSize = maxCols * CELL_BYTES; - const bufPtr = wasm.exports.alloc_buffer(lineSize); - if (bufPtr === 0) { - return BLANK_CELL; - } - - try { - const length = wasm.exports.get_scrollback_line( - termPtr, - offset, - bufPtr, - maxCols, - ); - if (length === 0 || col >= length) { - return BLANK_CELL; - } - - const view = new DataView(wasm.exports.memory.buffer, bufPtr, lineSize); - const cell = parseCell(view, col * CELL_BYTES); - if (cell.codepoint === 0 && cell.flags === 0 && cell.colorFlags === 0) { - return BLANK_CELL; - } - - const result: CellData = { - char: cell.codepoint || 32, - fg: DEFAULT_COLOR, - bg: DEFAULT_COLOR, - flags: cell.flags, - }; - if (cell.colorFlags & 1) { - result.fgRgb = packRgb(cell.fgR, cell.fgG, cell.fgB); - } - if (cell.colorFlags & 2) { - result.bgRgb = packRgb(cell.bgR, cell.bgG, cell.bgB); - } - return result; - } finally { - wasm.exports.free_buffer(bufPtr, lineSize); - } -} - -function hasDefaultColors(cell: CellData) { - return ( - cell.fg === DEFAULT_COLOR && - cell.bg === DEFAULT_COLOR && - cell.fgRgb === undefined && - cell.bgRgb === undefined - ); -} - -function normalizeReverseDefaultCell( - cell: CellData, - colors: TerminalThemeColors, -): CellData { - if (!(cell.flags & REVERSE_FLAG) || !hasDefaultColors(cell)) { - return cell; - } - - return { - ...cell, - flags: cell.flags & ~REVERSE_FLAG, - fgRgb: colors.background, - bgRgb: colors.foreground, - }; -} - -export function patchGhosttyCore( - core: TerminalCore, - getThemeColors: () => TerminalThemeColors, -) { - const originalGetCell = core.getCell.bind(core); - const originalGetScrollbackCell = core.getScrollbackCell.bind(core); - - core.getCell = (row, col) => - normalizeReverseDefaultCell(originalGetCell(row, col), getThemeColors()); - core.getScrollbackCell = (offset, col) => { - const cell = - readScrollbackCell(core, offset, col) ?? - originalGetScrollbackCell(offset, col); - return normalizeReverseDefaultCell(cell, getThemeColors()); - }; -} -- 2.51.2