From 422000a48d91b7caa03c27cdf9d2a0142e0e28dc Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 30 Apr 2026 13:49:08 -0500 Subject: [PATCH 1/3] feat: add @wterm/ghostty package with libghostty WASM core - New @wterm/ghostty package with GhosttyCore implementing TerminalCore - Custom wasm_api.zig thin export layer over ghostty's Terminal - Build scripts to fetch, patch, and compile ghostty to WASM - Extract TerminalCore interface into standalone module for reuse - Add core toggle (Built-in / Ghostty) to the local example - Ghostty example with vanilla TS setup - Docs page, navigation, and README updates --- .gitignore | 1 + README.md | 4 +- apps/docs/src/app/api-reference/page.mdx | 8 +- apps/docs/src/app/configuration/page.mdx | 6 +- apps/docs/src/app/ghostty/page.mdx | 149 ++++++++ apps/docs/src/app/react/page.mdx | 2 +- apps/docs/src/app/vanilla/page.mdx | 2 +- apps/docs/src/app/vue/page.mdx | 2 +- apps/docs/src/lib/docs-navigation.ts | 11 + apps/docs/src/lib/page-titles.ts | 1 + examples/ghostty/README.md | 27 ++ examples/ghostty/index.html | 27 ++ examples/ghostty/package.json | 21 ++ examples/ghostty/src/main.ts | 16 + examples/ghostty/tsconfig.json | 13 + examples/ghostty/vite.config.ts | 3 + examples/ghostty/wterm-dom.d.ts | 1 + examples/local/.gitignore | 1 + examples/local/app/page.tsx | 82 ++++- examples/local/package.json | 11 +- packages/@wterm/core/README.md | 19 +- packages/@wterm/core/src/index.ts | 5 +- packages/@wterm/core/src/terminal-core.ts | 65 ++++ packages/@wterm/core/src/wasm-bridge.ts | 22 +- packages/@wterm/dom/src/debug.ts | 6 +- packages/@wterm/dom/src/input.ts | 6 +- packages/@wterm/dom/src/renderer.ts | 140 +++++--- packages/@wterm/dom/src/wterm.ts | 17 +- packages/@wterm/ghostty/README.md | 107 ++++++ packages/@wterm/ghostty/package.json | 47 +++ packages/@wterm/ghostty/scripts/build-wasm.sh | 69 ++++ .../ghostty/scripts/patch-ghostty-wasm.sh | 202 +++++++++++ packages/@wterm/ghostty/src/ghostty-core.ts | 325 +++++++++++++++++ packages/@wterm/ghostty/src/index.ts | 2 + packages/@wterm/ghostty/src/wasm-bindings.ts | 183 ++++++++++ packages/@wterm/ghostty/tsconfig.json | 9 + packages/@wterm/ghostty/wasm/.gitkeep | 0 packages/@wterm/ghostty/wasm/ghostty-vt.wasm | Bin 0 -> 428644 bytes packages/@wterm/ghostty/zig/build.zig | 33 ++ packages/@wterm/ghostty/zig/build.zig.zon | 17 + packages/@wterm/ghostty/zig/src/wasm_api.zig | 336 ++++++++++++++++++ packages/@wterm/react/src/Terminal.tsx | 11 +- packages/@wterm/vue/src/Terminal.ts | 9 +- pnpm-lock.yaml | 75 ++-- 44 files changed, 1951 insertions(+), 142 deletions(-) create mode 100644 apps/docs/src/app/ghostty/page.mdx create mode 100644 examples/ghostty/README.md create mode 100644 examples/ghostty/index.html create mode 100644 examples/ghostty/package.json create mode 100644 examples/ghostty/src/main.ts create mode 100644 examples/ghostty/tsconfig.json create mode 100644 examples/ghostty/vite.config.ts create mode 100644 examples/ghostty/wterm-dom.d.ts create mode 100644 packages/@wterm/core/src/terminal-core.ts create mode 100644 packages/@wterm/ghostty/README.md create mode 100644 packages/@wterm/ghostty/package.json create mode 100755 packages/@wterm/ghostty/scripts/build-wasm.sh create mode 100755 packages/@wterm/ghostty/scripts/patch-ghostty-wasm.sh create mode 100644 packages/@wterm/ghostty/src/ghostty-core.ts create mode 100644 packages/@wterm/ghostty/src/index.ts create mode 100644 packages/@wterm/ghostty/src/wasm-bindings.ts create mode 100644 packages/@wterm/ghostty/tsconfig.json create mode 100644 packages/@wterm/ghostty/wasm/.gitkeep create mode 100755 packages/@wterm/ghostty/wasm/ghostty-vt.wasm create mode 100644 packages/@wterm/ghostty/zig/build.zig create mode 100644 packages/@wterm/ghostty/zig/build.zig.zon create mode 100644 packages/@wterm/ghostty/zig/src/wasm_api.zig diff --git a/.gitignore b/.gitignore index 9eb20c0..98f8fd5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ zig-out/ .zig-cache/ *.wasm !packages/@wterm/core/wasm/wterm.wasm +!packages/@wterm/ghostty/wasm/ghostty-vt.wasm .DS_Store node_modules/ dist/ diff --git a/README.md b/README.md index 81d4b26..16d0447 100644 --- a/README.md +++ b/README.md @@ -8,15 +8,17 @@ wterm ("dub-term") renders to the DOM — native text selection, copy/paste, fin | Package | Description | |---|---| -| [`@wterm/core`](packages/@wterm/core) | Headless WASM bridge + WebSocket transport | +| [`@wterm/core`](packages/@wterm/core) | Headless WASM bridge, `TerminalCore` interface, WebSocket transport | | [`@wterm/dom`](packages/@wterm/dom) | DOM renderer, input handler — vanilla JS terminal | | [`@wterm/react`](packages/@wterm/react) | React component + `useTerminal` hook (TypeScript) | | [`@wterm/vue`](packages/@wterm/vue) | Vue 3 component + template ref API | +| [`@wterm/ghostty`](packages/@wterm/ghostty) | Full-featured VT emulation core powered by libghostty | | [`@wterm/just-bash`](packages/@wterm/just-bash) | In-browser Bash shell powered by just-bash | | [`@wterm/markdown`](packages/@wterm/markdown) | Render Markdown in the terminal | ## Features +- **Pluggable cores** — built-in lightweight Zig core (~12 KB) or opt-in [libghostty](packages/@wterm/ghostty) backend (~400 KB) for full VT compliance - **Zig + WASM core** — VT100/VT220/xterm escape sequence parser compiled to a ~12 KB `.wasm` binary (release build) - **DOM rendering** — native text selection, clipboard, browser find, and screen reader support - **Dirty-row tracking** — only touched rows are re-rendered each frame via `requestAnimationFrame` diff --git a/apps/docs/src/app/api-reference/page.mdx b/apps/docs/src/app/api-reference/page.mdx index 1241923..9c03113 100644 --- a/apps/docs/src/app/api-reference/page.mdx +++ b/apps/docs/src/app/api-reference/page.mdx @@ -28,11 +28,17 @@ The React and Vue `` components and the vanilla `WTerm` constructor al 24 Initial row count + + core + TerminalCore + — + A pre-constructed terminal core instance. When provided, wasmUrl is ignored and this core is used instead of loading the built-in Zig WASM binary. See Ghostty Core for an example. + wasmUrl string — - URL to serve the WASM binary separately. When omitted, the ~12 KB binary is decoded from an inlined base64 string. + URL to serve the WASM binary separately. When omitted, the ~12 KB binary is decoded from an inlined base64 string. Ignored when core is provided. autoResize diff --git a/apps/docs/src/app/configuration/page.mdx b/apps/docs/src/app/configuration/page.mdx index 75364ae..ded05f5 100644 --- a/apps/docs/src/app/configuration/page.mdx +++ b/apps/docs/src/app/configuration/page.mdx @@ -2,10 +2,14 @@ ## Options -The React and Vue `Terminal` components and the vanilla `WTerm` constructor accept the same core options — `cols`, `rows`, `wasmUrl`, `autoResize`, `cursorBlink`, and `debug` — plus event callbacks `onData`, `onTitle`, and `onResize` (exposed as `@data`, `@title`, `@resize` events in Vue). +The React and Vue `Terminal` components and the vanilla `WTerm` constructor accept the same core options — `cols`, `rows`, `core`, `wasmUrl`, `autoResize`, `cursorBlink`, and `debug` — plus event callbacks `onData`, `onTitle`, and `onResize` (exposed as `@data`, `@title`, `@resize` events in Vue). See the full [API Reference](/api-reference#terminal-options) for types, defaults, and descriptions. +### Pluggable cores + +By default, wterm uses its built-in lightweight Zig WASM core (~12 KB). To use a different terminal emulation backend, pass a `TerminalCore` instance via the `core` option. See the [Ghostty Core](/ghostty) page for an example using libghostty. + ### React-only The React `Terminal` component adds `theme`, `onReady`, and `onError` on top of the shared options. It also spreads standard HTML attributes onto the root `div`, so you can pass `className`, `style`, ARIA attributes, and other DOM props directly. diff --git a/apps/docs/src/app/ghostty/page.mdx b/apps/docs/src/app/ghostty/page.mdx new file mode 100644 index 0000000..a84823d --- /dev/null +++ b/apps/docs/src/app/ghostty/page.mdx @@ -0,0 +1,149 @@ +# Ghostty Core + +The `@wterm/ghostty` package provides a full-featured terminal emulation core powered by [libghostty](https://ghostty.org) built directly from upstream source. It implements the same `TerminalCore` interface as wterm's built-in Zig core, so it's a drop-in replacement. + +## Why use it? + +wterm ships with a lightweight built-in core (~12 KB WASM) that covers basic VT100/VT220/xterm escape sequences. For apps that need comprehensive terminal emulation — full Unicode grapheme clusters, all SGR attributes, terminal modes, and more — `@wterm/ghostty` provides all of that via Ghostty's battle-tested VT parser (~400 KB WASM). + +## Install + +```bash +npm install @wterm/ghostty +``` + +## Usage + +Load the Ghostty core and pass it to `WTerm` via the `core` option. Everything else stays the same. + +### Vanilla JS + +```ts +import { WTerm } from "@wterm/dom"; +import { GhosttyCore } from "@wterm/ghostty"; +import "@wterm/dom/css"; + +const core = await GhosttyCore.load(); +const term = new WTerm(document.getElementById("terminal"), { core }); +await term.init(); +``` + +### React + +```tsx +import { Terminal } from "@wterm/react"; +import { GhosttyCore } from "@wterm/ghostty"; +import "@wterm/dom/css"; + +const core = await GhosttyCore.load(); + +function App() { + return ; +} +``` + +### Vue + +```vue + + + +``` + +## Options + +`GhosttyCore.load()` accepts an optional options object: + + + + + + + + + + + + + + + + + + + + + +
OptionTypeDescription
wasmPathstringCustom path to the ghostty-vt WASM binary. By default it resolves to the committed binary inside the package.
scrollbackLimitnumberMaximum scrollback lines (default: 10000).
+ +## How it works + +`@wterm/ghostty` builds libghostty directly from [ghostty-org/ghostty](https://github.com/ghostty-org/ghostty) source — no third-party npm packages or pre-built binaries from other projects. The architecture: + +1. **Zig package dependency**: ghostty v1.3.1 is declared as a URL dependency in `zig/build.zig.zon`. Zig's package manager fetches it automatically. +2. **WASM compatibility patches**: ghostty's `Terminal` uses `posix.mmap` and Mach VM allocators internally, which don't exist on `wasm32-freestanding`. The build script applies small, targeted patches to replace these with `std.heap.wasm_allocator` behind comptime `isWasm()` checks. The patches only touch `page.zig` and `PageList.zig`. +3. **Thin WASM export layer**: `zig/src/wasm_api.zig` (~300 lines) imports ghostty's `Terminal` and `RenderState` APIs and exports ~20 functions to JavaScript. +4. **Committed WASM binary**: The built `wasm/ghostty-vt.wasm` is checked into the repo so consumers never need Zig installed. +5. **TypeScript bindings**: `wasm-bindings.ts` loads the WASM module and provides typed accessors for the exported functions. +6. **TerminalCore adapter**: `ghostty-core.ts` implements the `TerminalCore` interface by calling the WASM bindings, converting ghostty's pre-resolved 24-bit RGB colors to wterm's `CellData` format via the `fgRgb`/`bgRgb` fields. + +The `TerminalCore` interface means the DOM renderer, input handler, and framework bindings don't need to know which core they're talking to. + +## Rebuilding the WASM + +Only needed by maintainers. Requires [Zig 0.15.x](https://ziglang.org/download/) (ghostty's required Zig version, separate from wterm's Zig 0.16.x): + +```bash +pnpm --filter @wterm/ghostty rebuild-wasm +``` + +## Comparison + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Built-in (default)@wterm/ghostty
Bundle size~12 KB WASM (inlined)~400 KB WASM (fetched)
VT complianceBasic VT100/VT220/xtermComprehensive
UnicodeSingle codepointsFull grapheme clusters
Color model256-color palette indicesPre-resolved 24-bit RGB
DependenciesNoneNone (WASM built from source)
SetupZero-configRequires @wterm/ghostty install
diff --git a/apps/docs/src/app/react/page.mdx b/apps/docs/src/app/react/page.mdx index b724066..7fd56c6 100644 --- a/apps/docs/src/app/react/page.mdx +++ b/apps/docs/src/app/react/page.mdx @@ -43,7 +43,7 @@ function App() { ## Props -The `` component accepts all [shared terminal options](/api-reference#terminal-options) (`cols`, `rows`, `wasmUrl`, `autoResize`, `cursorBlink`, `onData`, `onTitle`, `onResize`) plus [React-only props](/api-reference#react-only-props) (`theme`, `onReady`, `onError`). +The `` component accepts all [shared terminal options](/api-reference#terminal-options) (`cols`, `rows`, `core`, `wasmUrl`, `autoResize`, `cursorBlink`, `onData`, `onTitle`, `onResize`) plus [React-only props](/api-reference#react-only-props) (`theme`, `onReady`, `onError`). Pass a [`TerminalCore`](/ghostty) instance to the `core` prop to use an alternative emulation backend. Standard `div` props (`className`, `style`, `id`, etc.) are forwarded to the container element. diff --git a/apps/docs/src/app/vanilla/page.mdx b/apps/docs/src/app/vanilla/page.mdx index f5dd152..1e214f9 100644 --- a/apps/docs/src/app/vanilla/page.mdx +++ b/apps/docs/src/app/vanilla/page.mdx @@ -43,7 +43,7 @@ await term.init(); ## Options and Methods -The `WTerm` constructor accepts all [shared terminal options](/api-reference#terminal-options) (`cols`, `rows`, `wasmUrl`, `autoResize`, `cursorBlink`, `onData`, `onTitle`, `onResize`). +The `WTerm` constructor accepts all [shared terminal options](/api-reference#terminal-options) (`cols`, `rows`, `core`, `wasmUrl`, `autoResize`, `cursorBlink`, `onData`, `onTitle`, `onResize`). Pass a [`TerminalCore`](/ghostty) instance to the `core` option to use an alternative emulation backend. See [WTerm Methods](/api-reference#wterm-methods) for the full list of instance methods (`init`, `write`, `resize`, `focus`, `destroy`). diff --git a/apps/docs/src/app/vue/page.mdx b/apps/docs/src/app/vue/page.mdx index 26f0408..6124915 100644 --- a/apps/docs/src/app/vue/page.mdx +++ b/apps/docs/src/app/vue/page.mdx @@ -45,7 +45,7 @@ function onData(chunk: string) { ## Props -The `` component accepts all [shared terminal options](/api-reference#terminal-options) (`cols`, `rows`, `wasmUrl`, `autoResize`, `cursorBlink`) plus [Vue-only props](/api-reference#vue-only-props) (`theme`). +The `` component accepts all [shared terminal options](/api-reference#terminal-options) (`cols`, `rows`, `core`, `wasmUrl`, `autoResize`, `cursorBlink`) plus [Vue-only props](/api-reference#vue-only-props) (`theme`). Pass a [`TerminalCore`](/ghostty) instance to the `core` prop to use an alternative emulation backend. Because `inheritAttrs` is enabled, standard DOM attributes (`class`, `style`, `id`, ARIA props, etc.) are forwarded to the root `
`. diff --git a/apps/docs/src/lib/docs-navigation.ts b/apps/docs/src/lib/docs-navigation.ts index bee32cb..39ea8f7 100644 --- a/apps/docs/src/lib/docs-navigation.ts +++ b/apps/docs/src/lib/docs-navigation.ts @@ -33,6 +33,7 @@ export const navGroups: NavGroup[] = [ { label: "Packages", items: [ + { name: "Ghostty Core", href: "/ghostty" }, { name: "Just Bash", href: "/just-bash" }, { name: "Markdown", href: "/markdown" }, { name: "Core / Advanced", href: "/core" }, @@ -66,6 +67,11 @@ export const navGroups: NavGroup[] = [ href: `${GITHUB}/tree/main/examples/markdown-streaming`, external: true, }, + { + name: "Ghostty Core", + href: `${GITHUB}/tree/main/examples/ghostty`, + external: true, + }, ], }, { @@ -91,6 +97,11 @@ export const navGroups: NavGroup[] = [ href: `${GITHUB}/tree/main/packages/@wterm/vue`, external: true, }, + { + name: "@wterm/ghostty", + href: `${GITHUB}/tree/main/packages/@wterm/ghostty`, + external: true, + }, { name: "@wterm/just-bash", href: `${GITHUB}/tree/main/packages/@wterm/just-bash`, diff --git a/apps/docs/src/lib/page-titles.ts b/apps/docs/src/lib/page-titles.ts index 9f81950..f0ed772 100644 --- a/apps/docs/src/lib/page-titles.ts +++ b/apps/docs/src/lib/page-titles.ts @@ -7,6 +7,7 @@ export const PAGE_TITLES: Record = { react: "React", vue: "Vue", vanilla: "Vanilla JS", + ghostty: "Ghostty Core", "just-bash": "Just Bash", markdown: "Markdown", core: "Core / Advanced", diff --git a/examples/ghostty/README.md b/examples/ghostty/README.md new file mode 100644 index 0000000..423875a --- /dev/null +++ b/examples/ghostty/README.md @@ -0,0 +1,27 @@ +# Ghostty Core Example + +Minimal Vite + vanilla TypeScript terminal using the [libghostty](https://ghostty.org) backend (built from source) via `@wterm/ghostty` instead of wterm's built-in Zig core. + +## Setup + +From the monorepo root: + +```bash +pnpm install +pnpm --filter ghostty-example dev +``` + +Opens at `ghostty-example.wterm.localhost` via [portless](https://github.com/vercel-labs/portless). + +## How It Works + +- `@wterm/ghostty` loads the ghostty-vt WASM binary (~400 KB, built from upstream ghostty source) and creates a `GhosttyCore` instance +- The core is passed to `WTerm` via the `core` option — from that point on, everything works identically to the built-in core +- `@wterm/dom` renders the terminal grid into the DOM as usual, consuming `TerminalCore` methods + +## Key Files + +| File | Description | +|---|---| +| `src/main.ts` | Loads the Ghostty core and creates the terminal | +| `index.html` | Minimal HTML with `
` | diff --git a/examples/ghostty/index.html b/examples/ghostty/index.html new file mode 100644 index 0000000..133520e --- /dev/null +++ b/examples/ghostty/index.html @@ -0,0 +1,27 @@ + + + + + + wterm — Ghostty Core Example + + + +
+ + + diff --git a/examples/ghostty/package.json b/examples/ghostty/package.json new file mode 100644 index 0000000..8ca1b48 --- /dev/null +++ b/examples/ghostty/package.json @@ -0,0 +1,21 @@ +{ + "name": "ghostty-example", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "predev": "command -v portless >/dev/null 2>&1 || (echo '\\nportless is required but not installed. Run: npm i -g portless\\nSee: https://github.com/vercel-labs/portless\\n' && exit 1)", + "dev": "portless ghostty-example.wterm vite", + "build": "vite build", + "preview": "vite preview", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "@wterm/dom": "workspace:*", + "@wterm/ghostty": "workspace:*" + }, + "devDependencies": { + "typescript": "^6.0.2", + "vite": "^6.3.5" + } +} diff --git a/examples/ghostty/src/main.ts b/examples/ghostty/src/main.ts new file mode 100644 index 0000000..216c188 --- /dev/null +++ b/examples/ghostty/src/main.ts @@ -0,0 +1,16 @@ +import { WTerm } from "@wterm/dom"; +import { GhosttyCore } from "@wterm/ghostty"; +import "@wterm/dom/css"; + +const el = document.getElementById("terminal")!; + +const core = await GhosttyCore.load(); +const term = new WTerm(el, { core }); + +await term.init(); + +term.write( + "\x1b[1;36mwterm\x1b[0m powered by \x1b[1;35mlibghostty\x1b[0m 🚀\r\n\r\n" + + "Full VT emulation • Kitty protocols • Unicode grapheme clusters\r\n\r\n" + + "Type anything to echo it back:\r\n", +); diff --git a/examples/ghostty/tsconfig.json b/examples/ghostty/tsconfig.json new file mode 100644 index 0000000..11e5462 --- /dev/null +++ b/examples/ghostty/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noEmit": true, + "isolatedModules": true, + "skipLibCheck": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"] + }, + "include": ["src", "*.d.ts"] +} diff --git a/examples/ghostty/vite.config.ts b/examples/ghostty/vite.config.ts new file mode 100644 index 0000000..6150f9a --- /dev/null +++ b/examples/ghostty/vite.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from "vite"; + +export default defineConfig({}); diff --git a/examples/ghostty/wterm-dom.d.ts b/examples/ghostty/wterm-dom.d.ts new file mode 100644 index 0000000..aefbc0a --- /dev/null +++ b/examples/ghostty/wterm-dom.d.ts @@ -0,0 +1 @@ +declare module "@wterm/dom/css"; diff --git a/examples/local/.gitignore b/examples/local/.gitignore index 71e5011..7b7894a 100644 --- a/examples/local/.gitignore +++ b/examples/local/.gitignore @@ -33,6 +33,7 @@ yarn-error.log* # wasm build artifact /public/wterm.wasm +/public/ghostty-vt.wasm # vercel .vercel diff --git a/examples/local/app/page.tsx b/examples/local/app/page.tsx index 38399c1..abceb16 100644 --- a/examples/local/app/page.tsx +++ b/examples/local/app/page.tsx @@ -3,17 +3,42 @@ import { useCallback, useRef, useState } from "react"; import { Terminal, useTerminal } from "@wterm/react"; import type { WTerm } from "@wterm/dom"; +import type { TerminalCore } from "@wterm/core"; +import { GhosttyCore } from "@wterm/ghostty"; import "@wterm/react/css"; +type CoreKind = "builtin" | "ghostty"; + +async function loadCore(kind: CoreKind): Promise { + if (kind === "ghostty") + return GhosttyCore.load({ wasmPath: "/ghostty-vt.wasm" }); + return undefined; +} + export default function LocalTerminal() { const [debugEnabled] = useState( () => typeof window !== "undefined" && new URLSearchParams(window.location.search).has("debug"), ); + const [activeCore, setActiveCore] = useState("builtin"); + const [core, setCore] = useState(undefined); + const [switching, setSwitching] = useState(false); const { ref, write } = useTerminal(); const wsRef = useRef(null); + const switchCore = useCallback(async (kind: CoreKind) => { + if (wsRef.current) { + wsRef.current.close(); + wsRef.current = null; + } + setSwitching(true); + const loaded = await loadCore(kind); + setCore(loaded); + setActiveCore(kind); + setSwitching(false); + }, []); + const handleReady = useCallback( (wt: WTerm) => { const proto = window.location.protocol === "https:" ? "wss:" : "ws:"; @@ -51,19 +76,50 @@ export default function LocalTerminal() { return (
- +
+ Core +
+ + +
+
+ {!switching && ( + + )}
); } diff --git a/examples/local/package.json b/examples/local/package.json index f3c58ec..812f79b 100644 --- a/examples/local/package.json +++ b/examples/local/package.json @@ -3,9 +3,9 @@ "version": "0.1.0", "private": true, "scripts": { - "predev": "mkdir -p public && cp node_modules/@wterm/core/wasm/wterm.wasm public/wterm.wasm && chmod +x node_modules/node-pty/prebuilds/darwin-*/spawn-helper 2>/dev/null; (command -v portless >/dev/null 2>&1 || (echo '\\nportless is required but not installed. Run: npm i -g portless\\nSee: https://github.com/vercel-labs/portless\\n' && exit 1))", + "predev": "mkdir -p public && cp node_modules/@wterm/core/wasm/wterm.wasm public/wterm.wasm && cp node_modules/@wterm/ghostty/wasm/ghostty-vt.wasm public/ghostty-vt.wasm && chmod +x node_modules/node-pty/prebuilds/darwin-*/spawn-helper 2>/dev/null; (command -v portless >/dev/null 2>&1 || (echo '\\nportless is required but not installed. Run: npm i -g portless\\nSee: https://github.com/vercel-labs/portless\\n' && exit 1))", "dev": "portless local-example.wterm tsx server.ts", - "prebuild": "mkdir -p public && cp node_modules/@wterm/core/wasm/wterm.wasm public/wterm.wasm && chmod +x node_modules/node-pty/prebuilds/darwin-*/spawn-helper 2>/dev/null", + "prebuild": "mkdir -p public && cp node_modules/@wterm/core/wasm/wterm.wasm public/wterm.wasm && cp node_modules/@wterm/ghostty/wasm/ghostty-vt.wasm public/ghostty-vt.wasm && chmod +x node_modules/node-pty/prebuilds/darwin-*/spawn-helper 2>/dev/null", "build": "next build", "start": "next start", "lint": "eslint", @@ -15,6 +15,7 @@ "@tailwindcss/postcss": "^4.2.2", "@wterm/core": "workspace:*", "@wterm/dom": "workspace:*", + "@wterm/ghostty": "workspace:*", "@wterm/react": "workspace:*", "lucide-react": "^1.8.0", "next": "16.2.3", @@ -22,17 +23,17 @@ "postcss": "^8.5.9", "react": "19.2.5", "react-dom": "19.2.5", - "ws": "^8.18.2", - "tailwindcss": "^4.2.2" + "tailwindcss": "^4.2.2", + "ws": "^8.18.2" }, "devDependencies": { "@types/node": "^25.6.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@types/ws": "^8.18.1", - "tsx": "^4.19.4", "eslint": "^9.39.4", "eslint-config-next": "16.2.3", + "tsx": "^4.19.4", "typescript": "^6.0.2" } } diff --git a/packages/@wterm/core/README.md b/packages/@wterm/core/README.md index 0f21212..b29c42d 100644 --- a/packages/@wterm/core/README.md +++ b/packages/@wterm/core/README.md @@ -9,6 +9,7 @@ Headless terminal emulator core for [wterm](https://github.com/vercel-labs/wterm | [`@wterm/dom`](https://www.npmjs.com/package/@wterm/dom) | DOM renderer, input handler — vanilla JS terminal | | [`@wterm/react`](https://www.npmjs.com/package/@wterm/react) | React component + `useTerminal` hook | | [`@wterm/vue`](https://www.npmjs.com/package/@wterm/vue) | Vue 3 component + template ref API | +| [`@wterm/ghostty`](https://www.npmjs.com/package/@wterm/ghostty) | Full-featured VT emulation core powered by libghostty | | [`@wterm/just-bash`](https://www.npmjs.com/package/@wterm/just-bash) | In-browser Bash shell powered by just-bash | | [`@wterm/markdown`](https://www.npmjs.com/package/@wterm/markdown) | Streaming Markdown-to-ANSI renderer for terminals | @@ -18,11 +19,27 @@ Headless terminal emulator core for [wterm](https://github.com/vercel-labs/wterm npm install @wterm/core ``` +## Pluggable Cores + +`@wterm/core` defines a `TerminalCore` interface that any terminal emulation backend can implement. The built-in `WasmBridge` implements it using wterm's lightweight Zig WASM binary (~12 KB). For full-featured emulation (Kitty protocols, proper grapheme handling, mouse tracking, etc.), use [`@wterm/ghostty`](https://www.npmjs.com/package/@wterm/ghostty) which implements the same interface using libghostty (~400 KB). + +```ts +import { WTerm } from "@wterm/dom"; +import { GhosttyCore } from "@wterm/ghostty"; + +// Default — uses built-in lightweight core +const term = new WTerm(el); + +// Opt-in — uses libghostty for full VT emulation +const core = await GhosttyCore.load(); +const term = new WTerm(el, { core }); +``` + ## API ### `WasmBridge` -Low-level interface to the Zig/WASM terminal state machine. +Low-level interface to the Zig/WASM terminal state machine. Implements the `TerminalCore` interface. ```ts import { WasmBridge } from "@wterm/core"; diff --git a/packages/@wterm/core/src/index.ts b/packages/@wterm/core/src/index.ts index c757cc5..bf32657 100644 --- a/packages/@wterm/core/src/index.ts +++ b/packages/@wterm/core/src/index.ts @@ -1,8 +1,9 @@ -export { WasmBridge } from "./wasm-bridge.js"; export type { CellData, CursorState, UnhandledSequence, -} from "./wasm-bridge.js"; + TerminalCore, +} from "./terminal-core.js"; +export { WasmBridge } from "./wasm-bridge.js"; export { WebSocketTransport } from "./transport.js"; export type { WebSocketTransportOptions } from "./transport.js"; diff --git a/packages/@wterm/core/src/terminal-core.ts b/packages/@wterm/core/src/terminal-core.ts new file mode 100644 index 0000000..f8c77d5 --- /dev/null +++ b/packages/@wterm/core/src/terminal-core.ts @@ -0,0 +1,65 @@ +export interface CellData { + char: number; + fg: number; + bg: number; + flags: number; + /** Resolved 24-bit foreground color (0xRRGGBB). Present when the core provides true color. */ + fgRgb?: number; + /** Resolved 24-bit background color (0xRRGGBB). Present when the core provides true color. */ + bgRgb?: number; +} + +export interface CursorState { + row: number; + col: number; + visible: boolean; +} + +export interface UnhandledSequence { + final: string; + private: string; + paramCount: number; + params: number[]; +} + +/** + * Abstract terminal emulation core. Both the built-in Zig WASM core + * (`WasmBridge`) and alternative backends (e.g. `@wterm/ghostty`) implement + * this interface so that `@wterm/dom` can render any core interchangeably. + */ +export interface TerminalCore { + // -- Lifecycle -- + init(cols: number, rows: number): void; + resize(cols: number, rows: number): void; + + // -- I/O -- + writeString(str: string): void; + writeRaw(data: Uint8Array): void; + + // -- Grid -- + getCell(row: number, col: number): CellData; + isDirtyRow(row: number): boolean; + clearDirty(): void; + getCols(): number; + getRows(): number; + + // -- Cursor -- + getCursor(): CursorState; + + // -- Modes -- + cursorKeysApp(): boolean; + bracketedPaste(): boolean; + usingAltScreen(): boolean; + + // -- Side outputs -- + getTitle(): string | null; + getResponse(): string | null; + + // -- Scrollback -- + getScrollbackCount(): number; + getScrollbackCell(offset: number, col: number): CellData; + getScrollbackLineLen(offset: number): number; + + // -- Debug -- + getUnhandledSequences(): UnhandledSequence[]; +} diff --git a/packages/@wterm/core/src/wasm-bridge.ts b/packages/@wterm/core/src/wasm-bridge.ts index 73db73e..cd5f371 100644 --- a/packages/@wterm/core/src/wasm-bridge.ts +++ b/packages/@wterm/core/src/wasm-bridge.ts @@ -1,15 +1,4 @@ -export interface CellData { - char: number; - fg: number; - bg: number; - flags: number; -} - -export interface CursorState { - row: number; - col: number; - visible: boolean; -} +import type { CellData, CursorState, UnhandledSequence, TerminalCore } from "./terminal-core.js"; interface WasmExports { memory: WebAssembly.Memory; @@ -45,13 +34,6 @@ interface WasmExports { getDebugLogMax(): number; } -export interface UnhandledSequence { - final: string; - private: string; - paramCount: number; - params: number[]; -} - import { WASM_BASE64 } from "./wasm-inline.js"; function decodeBase64(base64: string): ArrayBuffer { @@ -61,7 +43,7 @@ function decodeBase64(base64: string): ArrayBuffer { return bytes.buffer; } -export class WasmBridge { +export class WasmBridge implements TerminalCore { private exports: WasmExports; private memory: WebAssembly.Memory; private gridPtr = 0; diff --git a/packages/@wterm/dom/src/debug.ts b/packages/@wterm/dom/src/debug.ts index b8fe0ab..cfce891 100644 --- a/packages/@wterm/dom/src/debug.ts +++ b/packages/@wterm/dom/src/debug.ts @@ -1,4 +1,4 @@ -import type { WasmBridge, CellData } from "@wterm/core"; +import type { TerminalCore, CellData } from "@wterm/core"; const FLAG_NAMES: Record = { 0x01: "bold", @@ -193,7 +193,7 @@ const MAX_TRACES = 500; export class DebugAdapter { private _traces: TraceEntry[] = []; - private _bridge: WasmBridge | null = null; + private _bridge: TerminalCore | null = null; private _perf: PerfStats = { frameCount: 0, totalRenderMs: 0, @@ -210,7 +210,7 @@ export class DebugAdapter { return this._perf; } - setBridge(bridge: WasmBridge): void { + setBridge(bridge: TerminalCore): void { this._bridge = bridge; } diff --git a/packages/@wterm/dom/src/input.ts b/packages/@wterm/dom/src/input.ts index e75feab..5e2bfce 100644 --- a/packages/@wterm/dom/src/input.ts +++ b/packages/@wterm/dom/src/input.ts @@ -1,4 +1,4 @@ -import type { WasmBridge } from "@wterm/core"; +import type { TerminalCore } from "@wterm/core"; const NORMAL_KEYS: Record = { ArrowUp: "\x1b[A", @@ -45,7 +45,7 @@ export class InputHandler { private element: HTMLElement; private textarea: HTMLTextAreaElement; private onData: (data: string) => void; - private getBridge: () => WasmBridge | null; + private getBridge: () => TerminalCore | null; private composing = false; private _onKeyDown: (e: KeyboardEvent) => void; @@ -59,7 +59,7 @@ export class InputHandler { constructor( element: HTMLElement, onData: (data: string) => void, - getBridge: () => WasmBridge | null, + getBridge: () => TerminalCore | null, ) { this.element = element; this.onData = onData; diff --git a/packages/@wterm/dom/src/renderer.ts b/packages/@wterm/dom/src/renderer.ts index 5f6ce51..c32c593 100644 --- a/packages/@wterm/dom/src/renderer.ts +++ b/packages/@wterm/dom/src/renderer.ts @@ -1,4 +1,4 @@ -import type { WasmBridge } from "@wterm/core"; +import type { TerminalCore } from "@wterm/core"; const DEFAULT_COLOR = 256; const FLAG_BOLD = 0x01; @@ -9,6 +9,13 @@ const FLAG_REVERSE = 0x20; const FLAG_INVISIBLE = 0x40; const FLAG_STRIKETHROUGH = 0x80; +function rgbToCSS(packed: number): string { + const r = (packed >> 16) & 0xff; + const g = (packed >> 8) & 0xff; + const b = packed & 0xff; + return `rgb(${r},${g},${b})`; +} + function colorToCSS(index: number): string | null { if (index === DEFAULT_COLOR) return null; if (index < 16) return `var(--term-color-${index})`; @@ -23,19 +30,47 @@ function colorToCSS(index: number): string | null { return `rgb(${level},${level},${level})`; } -function buildCellStyle(fg: number, bg: number, flags: number): string { - let fgC = fg, - bgC = bg; +function cellFgCSS( + fg: number, + fgRgb: number | undefined, +): string | null { + if (fgRgb !== undefined) return rgbToCSS(fgRgb); + return colorToCSS(fg); +} + +function cellBgCSS( + bg: number, + bgRgb: number | undefined, +): string | null { + if (bgRgb !== undefined) return rgbToCSS(bgRgb); + return colorToCSS(bg); +} + +function buildCellStyle( + fg: number, + bg: number, + flags: number, + fgRgb?: number, + bgRgb?: number, +): string { + let fgIdx = fg, + bgIdx = bg, + fgR = fgRgb, + bgR = bgRgb; + if (flags & FLAG_REVERSE) { - const tmp = fgC; - fgC = bgC; - bgC = tmp; - if (fgC === DEFAULT_COLOR) fgC = 0; - if (bgC === DEFAULT_COLOR) bgC = 7; + const tmpIdx = fgIdx; + fgIdx = bgIdx; + bgIdx = tmpIdx; + const tmpR = fgR; + fgR = bgR; + bgR = tmpR; + if (fgR === undefined && fgIdx === DEFAULT_COLOR) fgIdx = 0; + if (bgR === undefined && bgIdx === DEFAULT_COLOR) bgIdx = 7; } - const fgCSS = colorToCSS(fgC); - const bgCSS = colorToCSS(bgC); + const fgCSS = cellFgCSS(fgIdx, fgR); + const bgCSS = cellBgCSS(bgIdx, bgR); let style = ""; if (fgCSS) style += `color:${fgCSS};`; @@ -64,17 +99,23 @@ function resolveColors( fg: number, bg: number, flags: number, + fgRgb?: number, + bgRgb?: number, ): { fg: string; bg: string } { - let fgC = fg, - bgC = bg; + let fgIdx = fg, + bgIdx = bg, + fgR = fgRgb, + bgR = bgRgb; + if (flags & FLAG_REVERSE) { - [fgC, bgC] = [bgC, fgC]; - if (fgC === DEFAULT_COLOR) fgC = 0; - if (bgC === DEFAULT_COLOR) bgC = 7; + [fgIdx, bgIdx] = [bgIdx, fgIdx]; + [fgR, bgR] = [bgR, fgR]; + if (fgR === undefined && fgIdx === DEFAULT_COLOR) fgIdx = 0; + if (bgR === undefined && bgIdx === DEFAULT_COLOR) bgIdx = 7; } return { - fg: colorToCSS(fgC) || "var(--term-fg)", - bg: colorToCSS(bgC) || "var(--term-bg)", + fg: cellFgCSS(fgIdx, fgR) || "var(--term-fg)", + bg: cellBgCSS(bgIdx, bgR) || "var(--term-bg)", }; } @@ -201,6 +242,8 @@ export class Renderer { fg: number; bg: number; flags: number; + fgRgb?: number; + bgRgb?: number; }, lineLen: number, cursorCol: number, @@ -243,7 +286,7 @@ export class Renderer { if (inBounds && cp >= 0x2580 && cp <= 0x259f) { flushRun(col); - const colors = resolveColors(cell.fg, cell.bg, cell.flags); + const colors = resolveColors(cell.fg, cell.bg, cell.flags, cell.fgRgb, cell.bgRgb); const span = document.createElement("span"); span.className = col === cursorCol ? "term-block term-cursor" : "term-block"; @@ -257,7 +300,7 @@ export class Renderer { } else { const ch = inBounds && cp >= 32 ? String.fromCodePoint(cp) : " "; const style = inBounds - ? buildCellStyle(cell.fg, cell.bg, cell.flags) + ? buildCellStyle(cell.fg, cell.bg, cell.flags, cell.fgRgb, cell.bgRgb) : ""; if (style !== runStyle) { @@ -272,18 +315,17 @@ export class Renderer { } flushRun(this.cols); - // Extend the row background when the line fills the full width. - // When lineLen < cols, bgCss stays "" which clears any stale bg - // via the prevRowBg comparison below. let bgCss = ""; if (lineLen >= this.cols && this.cols > 0) { const lastCell = getCell(this.cols - 1); - let bgC = lastCell.bg; + let bgIdx = lastCell.bg; + let bgR = lastCell.bgRgb; if (lastCell.flags & FLAG_REVERSE) { - bgC = lastCell.fg; - if (bgC === DEFAULT_COLOR) bgC = 7; + bgIdx = lastCell.fg; + bgR = lastCell.fgRgb; + if (bgR === undefined && bgIdx === DEFAULT_COLOR) bgIdx = 7; } - bgCss = colorToCSS(bgC) || ""; + bgCss = cellBgCSS(bgIdx, bgR) || ""; } const boxShadow = bgCss ? `0 1px 0 ${bgCss}` : ""; if (rowIndex >= 0) { @@ -299,16 +341,16 @@ export class Renderer { } private _buildScrollbackRowEl( - bridge: WasmBridge, + core: TerminalCore, sbOffset: number, ): HTMLDivElement { const rowEl = document.createElement("div"); rowEl.className = "term-row term-scrollback-row"; - const lineLen = bridge.getScrollbackLineLen(sbOffset); + const lineLen = core.getScrollbackLineLen(sbOffset); this._buildRowContent( rowEl, - (col) => bridge.getScrollbackCell(sbOffset, col), + (col) => core.getScrollbackCell(sbOffset, col), lineLen, -1, -1, @@ -316,8 +358,8 @@ export class Renderer { return rowEl; } - private syncScrollback(bridge: WasmBridge): void { - const scrollbackCount = bridge.getScrollbackCount(); + private syncScrollback(core: TerminalCore): void { + const scrollbackCount = core.getScrollbackCount(); if (scrollbackCount === this._renderedScrollbackCount) return; @@ -327,7 +369,7 @@ export class Renderer { const fragment = document.createDocumentFragment(); for (let i = newCount - 1; i >= 0; i--) { - const rowEl = this._buildScrollbackRowEl(bridge, i); + const rowEl = this._buildScrollbackRowEl(core, i); fragment.appendChild(rowEl); this._scrollbackRowEls.push(rowEl); } @@ -344,9 +386,9 @@ export class Renderer { this._renderedScrollbackCount = scrollbackCount; } - render(bridge: WasmBridge): void { - const rows = bridge.getRows(); - const cols = bridge.getCols(); + render(core: TerminalCore): void { + const rows = core.getRows(); + const cols = core.getCols(); let resized = false; if (rows !== this.rows || cols !== this.cols) { @@ -354,16 +396,16 @@ export class Renderer { resized = true; } - this.syncScrollback(bridge); + this.syncScrollback(core); - const cursor = bridge.getCursor(); + const cursor = core.getCursor(); const cursorVisible = cursor.visible; const needsCursorUpdate = cursor.row !== this.prevCursorRow || cursor.col !== this.prevCursorCol; for (let r = 0; r < this.rows; r++) { - const isDirty = resized || bridge.isDirtyRow(r); + const isDirty = resized || core.isDirtyRow(r); const hadCursor = r === this.prevCursorRow && needsCursorUpdate; const hasCursor = r === cursor.row; @@ -371,7 +413,7 @@ export class Renderer { const cCol = hasCursor && cursorVisible ? cursor.col : -1; this._buildRowContent( this.rowEls[r], - (col) => bridge.getCell(r, col), + (col) => core.getCell(r, col), this.cols, cCol, r, @@ -382,23 +424,23 @@ export class Renderer { this.prevCursorRow = cursor.row; this.prevCursorCol = cursor.col; - // Update the container background only when the last row was actually - // repainted, avoiding stale reads during partial mid-redraw frames. - const lastRowDirty = resized || bridge.isDirtyRow(this.rows - 1); + const lastRowDirty = resized || core.isDirtyRow(this.rows - 1); if (lastRowDirty) { - const bottomRight = bridge.getCell(this.rows - 1, this.cols - 1); - let gridBg = bottomRight.bg; + const bottomRight = core.getCell(this.rows - 1, this.cols - 1); + let gridBgIdx = bottomRight.bg; + let gridBgRgb = bottomRight.bgRgb; if (bottomRight.flags & FLAG_REVERSE) { - gridBg = bottomRight.fg; - if (gridBg === DEFAULT_COLOR) gridBg = 7; + gridBgIdx = bottomRight.fg; + gridBgRgb = bottomRight.fgRgb; + if (gridBgRgb === undefined && gridBgIdx === DEFAULT_COLOR) gridBgIdx = 7; } - const containerBg = colorToCSS(gridBg) || ""; + const containerBg = cellBgCSS(gridBgIdx, gridBgRgb) || ""; if (containerBg !== this.prevContainerBg) { this.container.style.background = containerBg; this.prevContainerBg = containerBg; } } - bridge.clearDirty(); + core.clearDirty(); } } diff --git a/packages/@wterm/dom/src/wterm.ts b/packages/@wterm/dom/src/wterm.ts index 5552297..8139d1d 100644 --- a/packages/@wterm/dom/src/wterm.ts +++ b/packages/@wterm/dom/src/wterm.ts @@ -1,4 +1,4 @@ -import { WasmBridge } from "@wterm/core"; +import { WasmBridge, type TerminalCore } from "@wterm/core"; import { Renderer } from "./renderer.js"; import { InputHandler } from "./input.js"; import { DebugAdapter } from "./debug.js"; @@ -6,6 +6,11 @@ import { DebugAdapter } from "./debug.js"; export interface WTermOptions { cols?: number; rows?: number; + /** + * A pre-constructed terminal core. When provided, `wasmUrl` is ignored and + * this core is used directly instead of loading the built-in Zig WASM binary. + */ + core?: TerminalCore; wasmUrl?: string; autoResize?: boolean; cursorBlink?: boolean; @@ -19,10 +24,11 @@ export class WTerm { element: HTMLElement; cols: number; rows: number; - bridge: WasmBridge | null = null; + bridge: TerminalCore | null = null; autoResize: boolean; debug: DebugAdapter | null = null; + private _coreOption: TerminalCore | undefined; private wasmUrl: string | undefined; private _debugEnabled: boolean; private renderer: Renderer | null = null; @@ -42,6 +48,7 @@ export class WTerm { constructor(element: HTMLElement, options: WTermOptions = {}) { this.element = element; + this._coreOption = options.core; this.wasmUrl = options.wasmUrl; this.cols = options.cols || 80; this.rows = options.rows || 24; @@ -67,7 +74,11 @@ export class WTerm { async init(): Promise { try { - this.bridge = await WasmBridge.load(this.wasmUrl); + if (this._coreOption) { + this.bridge = this._coreOption; + } else { + this.bridge = await WasmBridge.load(this.wasmUrl); + } if (this._destroyed) return this; this.bridge.init(this.cols, this.rows); diff --git a/packages/@wterm/ghostty/README.md b/packages/@wterm/ghostty/README.md new file mode 100644 index 0000000..b0660c7 --- /dev/null +++ b/packages/@wterm/ghostty/README.md @@ -0,0 +1,107 @@ +# @wterm/ghostty + +Full-featured terminal emulation core for [wterm](https://github.com/vercel-labs/wterm), powered by [libghostty](https://ghostty.org) built from source. + +Drop-in replacement for wterm's built-in Zig core. Implements the same `TerminalCore` interface with comprehensive VT emulation: proper Unicode grapheme handling, all SGR attributes, terminal modes, and more. + +## Install + +```bash +npm install @wterm/ghostty +``` + +## Usage + +### Vanilla JS + +```ts +import { WTerm } from "@wterm/dom"; +import { GhosttyCore } from "@wterm/ghostty"; +import "@wterm/dom/css"; + +const core = await GhosttyCore.load(); +const term = new WTerm(document.getElementById("terminal"), { core }); +await term.init(); +``` + +### React + +```tsx +import { Terminal } from "@wterm/react"; +import { GhosttyCore } from "@wterm/ghostty"; +import "@wterm/dom/css"; + +const core = await GhosttyCore.load(); + +function App() { + return ; +} +``` + +### Vue + +```vue + + + +``` + +## Options + +`GhosttyCore.load()` accepts an options object: + +| Option | Type | Description | +|---|---|---| +| `wasmPath` | `string` | Custom path to the ghostty-vt WASM binary | +| `scrollbackLimit` | `number` | Maximum scrollback lines (default: 10000) | + +## Architecture + +The WASM binary is built from upstream [ghostty-org/ghostty](https://github.com/ghostty-org/ghostty) (v1.3.1) using it as a Zig package dependency — no third-party npm packages or pre-built binaries from other projects. + +``` +ghostty (Zig dep) → WASM patches → wasm_api.zig (~300 LOC) → ghostty-vt.wasm → TypeScript bindings +``` + +ghostty's `Terminal` and `Page` types use `posix.mmap` and Mach VM allocators internally, which don't exist on `wasm32-freestanding`. The build script applies small, targeted patches to replace these with `std.heap.wasm_allocator` behind comptime `isWasm()` checks (see `scripts/patch-ghostty-wasm.sh`). The patches are pinned to ghostty v1.3.1 and only touch two files: `page.zig` and `PageList.zig`. + +The committed `wasm/ghostty-vt.wasm` binary means consumers never need Zig installed. Only maintainers rebuilding the WASM need Zig 0.15.x. + +### Rebuilding the WASM + +Requires [Zig 0.15.x](https://ziglang.org/download/) (ghostty's required version): + +```bash +pnpm --filter @wterm/ghostty rebuild-wasm +``` + +This fetches the ghostty source via Zig's package manager, applies WASM compatibility patches, compiles our export layer to `wasm32-freestanding`, and copies the binary to `wasm/`. + +### Upgrading ghostty + +1. Edit the URL tag in `zig/build.zig.zon` to the new ghostty version +2. Run `zig fetch ` from the `zig/` directory to get the new hash +3. Update the hash in `build.zig.zon` +4. Verify the patches in `scripts/patch-ghostty-wasm.sh` still apply cleanly +5. Run `pnpm --filter @wterm/ghostty rebuild-wasm` + +## Tradeoffs vs built-in core + +| | Built-in (default) | `@wterm/ghostty` | +|---|---|---| +| Bundle size | ~12 KB WASM | ~400 KB WASM | +| VT compliance | Basic VT100/VT220/xterm | Comprehensive | +| Unicode | Single codepoints | Full grapheme clusters | +| Dependencies | None | None (WASM built from source) | +| Setup | Zero-config | Requires `@wterm/ghostty` install | + +## License + +Apache-2.0 diff --git a/packages/@wterm/ghostty/package.json b/packages/@wterm/ghostty/package.json new file mode 100644 index 0000000..3902cab --- /dev/null +++ b/packages/@wterm/ghostty/package.json @@ -0,0 +1,47 @@ +{ + "name": "@wterm/ghostty", + "version": "0.2.1", + "description": "libghostty-powered terminal core for wterm — full-featured VT emulation via Ghostty's WASM build", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "files": [ + "dist", + "wasm" + ], + "scripts": { + "build": "tsc", + "prepublishOnly": "pnpm build", + "type-check": "tsc --noEmit", + "rebuild-wasm": "bash scripts/build-wasm.sh" + }, + "dependencies": { + "@wterm/core": "workspace:*" + }, + "devDependencies": { + "@internal/ts": "workspace:*", + "typescript": "^6.0.2" + }, + "keywords": [ + "terminal", + "emulator", + "ghostty", + "libghostty", + "wasm", + "xterm" + ], + "license": "Apache-2.0", + "homepage": "https://wterm.dev", + "repository": { + "type": "git", + "url": "https://github.com/vercel-labs/wterm", + "directory": "packages/@wterm/ghostty" + } +} diff --git a/packages/@wterm/ghostty/scripts/build-wasm.sh b/packages/@wterm/ghostty/scripts/build-wasm.sh new file mode 100755 index 0000000..54925ab --- /dev/null +++ b/packages/@wterm/ghostty/scripts/build-wasm.sh @@ -0,0 +1,69 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ZIG_DIR="$SCRIPT_DIR/../zig" +OUT_DIR="$SCRIPT_DIR/../wasm" + +GHOSTTY_VERSION="1.3.1" +GHOSTTY_URL="https://github.com/ghostty-org/ghostty/archive/v${GHOSTTY_VERSION}.tar.gz" +GHOSTTY_HASH="ghostty-${GHOSTTY_VERSION}-5UdBCwYm-gQeBa4bu1-sMooCQS4KVriv5wWSIJ_sI-Cb" + +# --------------------------------------------------------------------------- +# 1. Locate Zig 0.15.x +# --------------------------------------------------------------------------- +ZIG="" +ZIGUP_PATH="$HOME/.local/share/zigup/0.15.2/files/zig" +if [[ -x "$ZIGUP_PATH" ]]; then + ZIG="$ZIGUP_PATH" +elif command -v zig &>/dev/null && [[ "$(zig version 2>/dev/null)" =~ ^0\.15\. ]]; then + ZIG="zig" +fi + +if [[ -z "$ZIG" ]]; then + echo "Error: Zig 0.15.x is required but not found." + echo "" + echo "ghostty requires Zig 0.15.x which differs from wterm's Zig 0.16.x." + echo "Install it with: zigup 0.15.2" + echo "or download from https://ziglang.org/download/" + exit 1 +fi + +echo "Using Zig: $ZIG ($($ZIG version))" + +# --------------------------------------------------------------------------- +# 2. Ensure ghostty source is fetched (populate Zig global cache) +# --------------------------------------------------------------------------- +GHOSTTY_SRC="$HOME/.cache/zig/p/$GHOSTTY_HASH" + +if [[ ! -d "$GHOSTTY_SRC" ]]; then + echo "Fetching ghostty v${GHOSTTY_VERSION}..." + cd "$ZIG_DIR" + "$ZIG" build 2>/dev/null || true + if [[ ! -d "$GHOSTTY_SRC" ]]; then + echo "Error: ghostty source not found at $GHOSTTY_SRC after fetch" + exit 1 + fi +fi + +echo "ghostty source: $GHOSTTY_SRC" + +# --------------------------------------------------------------------------- +# 3. Patch page.zig for WASM (mmap → wasm_allocator) +# --------------------------------------------------------------------------- +echo "Applying WASM patches..." +bash "$SCRIPT_DIR/patch-ghostty-wasm.sh" "$GHOSTTY_SRC" + +# --------------------------------------------------------------------------- +# 4. Build +# --------------------------------------------------------------------------- +cd "$ZIG_DIR" +echo "Building ghostty-vt WASM module..." +"$ZIG" build -Doptimize=ReleaseSmall + +mkdir -p "$OUT_DIR" +cp zig-out/bin/ghostty-vt.wasm "$OUT_DIR/" + +echo "" +echo "Built: $OUT_DIR/ghostty-vt.wasm" +ls -lh "$OUT_DIR/ghostty-vt.wasm" diff --git a/packages/@wterm/ghostty/scripts/patch-ghostty-wasm.sh b/packages/@wterm/ghostty/scripts/patch-ghostty-wasm.sh new file mode 100755 index 0000000..03fbe0f --- /dev/null +++ b/packages/@wterm/ghostty/scripts/patch-ghostty-wasm.sh @@ -0,0 +1,202 @@ +#!/bin/bash +# Patches ghostty source for wasm32-freestanding compatibility. +# +# Two files need patching: +# 1. page.zig — uses posix.mmap/munmap for page memory +# 2. PageList.zig — pageAllocator() returns Mach VM allocator on macOS +# +# Both are replaced with wasm_allocator on WASM targets using +# comptime isWasm() checks, matching ghostty's own conditional style. +# +# Pinned to ghostty v1.3.1 — verify after version bumps. +set -euo pipefail + +GHOSTTY_SRC="$1" +PAGE_ZIG="$GHOSTTY_SRC/src/terminal/page.zig" +PAGELIST_ZIG="$GHOSTTY_SRC/src/terminal/PageList.zig" + +if [[ ! -f "$PAGE_ZIG" ]]; then + echo "Error: $PAGE_ZIG not found" + exit 1 +fi + +# Skip if already patched +if grep -q 'wasm_page_alloc' "$PAGE_ZIG" 2>/dev/null; then + echo "Already patched, skipping" + exit 0 +fi + +cp "$PAGE_ZIG" "$PAGE_ZIG.orig" +cp "$PAGELIST_ZIG" "$PAGELIST_ZIG.orig" + +# --------------------------------------------------------------- +# Patch PageList.zig — pageAllocator() +# --------------------------------------------------------------- +python3 -c " +with open('$PAGELIST_ZIG', 'r') as f: + src = f.read() + +old_pa = '''inline fn pageAllocator() Allocator { + // In tests we use our testing allocator so we can detect leaks. + if (builtin.is_test) return std.testing.allocator; + + // On non-macOS we use our standard Zig page allocator. + if (!builtin.target.os.tag.isDarwin()) return std.heap.page_allocator; + + // On macOS we want to tag our memory so we can assign it to our + // core terminal usage. + const mach = @import(\"../os/mach.zig\"); + return mach.taggedPageAllocator(.application_specific_1); +}''' + +new_pa = '''inline fn pageAllocator() Allocator { + if (builtin.is_test) return std.testing.allocator; + if (comptime builtin.target.cpu.arch.isWasm()) { + return std.heap.wasm_allocator; + } else if (comptime builtin.target.os.tag.isDarwin()) { + const mach = @import(\"../os/mach.zig\"); + return mach.taggedPageAllocator(.application_specific_1); + } else { + return std.heap.page_allocator; + } +}''' + +src = src.replace(old_pa, new_pa, 1) + +with open('$PAGELIST_ZIG', 'w') as f: + f.write(src) + +print('PageList.zig patched for WASM') +" + +# --------------------------------------------------------------- +# Patch page.zig — mmap/munmap +# --------------------------------------------------------------- +python3 -c " +import sys + +with open('$PAGE_ZIG', 'r') as f: + src = f.read() + +# 1. Make posix conditional — void on WASM so no symbols are resolved +src = src.replace( + 'const posix = std.posix;', + 'const posix = if (builtin.target.cpu.arch.isWasm()) void else std.posix;', + 1 +) + +# 2. Patch init() to branch on WASM +old_init = ''' pub inline fn init(cap: Capacity) !Page { + const l = layout(cap); + + // We use mmap directly to avoid Zig allocator overhead + // (small but meaningful for this path) and because a private + // anonymous mmap is guaranteed on Linux and macOS to be zeroed, + // which is a critical property for us. + assert(l.total_size % std.heap.page_size_min == 0); + const backing = try posix.mmap( + null, + l.total_size, + posix.PROT.READ | posix.PROT.WRITE, + .{ .TYPE = .PRIVATE, .ANONYMOUS = true }, + -1, + 0, + ); + errdefer posix.munmap(backing); + + const buf = OffsetBuf.init(backing); + return initBuf(buf, l); + }''' + +new_init = ''' // wasm_page_alloc: patched by @wterm/ghostty for WASM compatibility + pub inline fn init(cap: Capacity) !Page { + const l = layout(cap); + + if (comptime builtin.target.cpu.arch.isWasm()) { + const backing = std.heap.wasm_allocator.alignedAlloc( + u8, + std.heap.page_size_min, + l.total_size, + ) catch return error.OutOfMemory; + @memset(backing, 0); + const buf = OffsetBuf.init(backing); + return initBuf(buf, l); + } + + assert(l.total_size % std.heap.page_size_min == 0); + const backing = try posix.mmap( + null, + l.total_size, + posix.PROT.READ | posix.PROT.WRITE, + .{ .TYPE = .PRIVATE, .ANONYMOUS = true }, + -1, + 0, + ); + errdefer posix.munmap(backing); + + const buf = OffsetBuf.init(backing); + return initBuf(buf, l); + }''' + +src = src.replace(old_init, new_init, 1) + +# 3. Patch deinit() +old_deinit = ''' pub inline fn deinit(self: *Page) void { + posix.munmap(self.memory); + self.* = undefined; + }''' + +new_deinit = ''' pub inline fn deinit(self: *Page) void { + if (comptime builtin.target.cpu.arch.isWasm()) { + std.heap.wasm_allocator.free(self.memory); + } else { + posix.munmap(self.memory); + } + self.* = undefined; + }''' + +src = src.replace(old_deinit, new_deinit, 1) + +# 4. Patch clone() +old_clone = ''' pub inline fn clone(self: *const Page) !Page { + const backing = try posix.mmap( + null, + self.memory.len, + posix.PROT.READ | posix.PROT.WRITE, + .{ .TYPE = .PRIVATE, .ANONYMOUS = true }, + -1, + 0, + ); + errdefer posix.munmap(backing); + return self.cloneBuf(backing); + }''' + +new_clone = ''' pub inline fn clone(self: *const Page) !Page { + if (comptime builtin.target.cpu.arch.isWasm()) { + const backing = std.heap.wasm_allocator.alignedAlloc( + u8, + std.heap.page_size_min, + self.memory.len, + ) catch return error.OutOfMemory; + errdefer std.heap.wasm_allocator.free(backing); + return self.cloneBuf(backing); + } + const backing = try posix.mmap( + null, + self.memory.len, + posix.PROT.READ | posix.PROT.WRITE, + .{ .TYPE = .PRIVATE, .ANONYMOUS = true }, + -1, + 0, + ); + errdefer posix.munmap(backing); + return self.cloneBuf(backing); + }''' + +src = src.replace(old_clone, new_clone, 1) + +with open('$PAGE_ZIG', 'w') as f: + f.write(src) + +print('page.zig patched for WASM') +" diff --git a/packages/@wterm/ghostty/src/ghostty-core.ts b/packages/@wterm/ghostty/src/ghostty-core.ts new file mode 100644 index 0000000..4a9ab3e --- /dev/null +++ b/packages/@wterm/ghostty/src/ghostty-core.ts @@ -0,0 +1,325 @@ +import type { + CellData, + CursorState, + UnhandledSequence, + TerminalCore, +} from "@wterm/core"; +import { + type GhosttyWasm, + loadGhosttyWasm, + parseCell, + writeString as wasmWriteString, + writeBytes as wasmWriteBytes, + allocBuffer, + freeBuffer, + CELL_BYTES, +} from "./wasm-bindings.js"; + +const DEFAULT_COLOR = 256; + +const WTERM_FLAG_BOLD = 0x01; +const WTERM_FLAG_DIM = 0x02; +const WTERM_FLAG_ITALIC = 0x04; +const WTERM_FLAG_UNDERLINE = 0x08; +const WTERM_FLAG_BLINK = 0x10; +const WTERM_FLAG_REVERSE = 0x20; +const WTERM_FLAG_INVISIBLE = 0x40; +const WTERM_FLAG_STRIKETHROUGH = 0x80; + +// Our WASM layer packs flags in the same order as wterm (see wasm_api.zig): +// bold=1, faint=2, italic=4, underline=8, blink=16, inverse=32, +// invisible=64, strikethrough=128 +// This matches wterm's layout exactly, so no remapping is needed. +const _FLAG_SANITY_CHECK = [ + WTERM_FLAG_BOLD, + WTERM_FLAG_DIM, + WTERM_FLAG_ITALIC, + WTERM_FLAG_UNDERLINE, + WTERM_FLAG_BLINK, + WTERM_FLAG_REVERSE, + WTERM_FLAG_INVISIBLE, + WTERM_FLAG_STRIKETHROUGH, +]; +void _FLAG_SANITY_CHECK; + +function packRgb(r: number, g: number, b: number): number { + return (r << 16) | (g << 8) | b; +} + +const BLANK_CELL: CellData = { + char: 32, + fg: DEFAULT_COLOR, + bg: DEFAULT_COLOR, + flags: 0, +}; + +export interface GhosttyOptions { + wasmPath?: string; + scrollbackLimit?: number; +} + +/** + * Terminal core powered by libghostty built from source. Implements the + * same `TerminalCore` interface as wterm's built-in Zig core, providing + * full-featured VT emulation including proper Unicode grapheme handling, + * all SGR attributes, terminal modes, and more. + * + * @example + * ```ts + * import { WTerm } from '@wterm/dom'; + * import { GhosttyCore } from '@wterm/ghostty'; + * + * const core = await GhosttyCore.load(); + * const term = new WTerm(el, { core }); + * await term.init(); + * ``` + */ +export class GhosttyCore implements TerminalCore { + private wasm: GhosttyWasm; + private termPtr = 0; + private _options: GhosttyOptions; + + private _viewportBufPtr = 0; + private _viewportBufSize = 0; + private _viewportView: DataView | null = null; + private _viewportStale = true; + private _cols = 0; + private _rows = 0; + + private constructor(wasm: GhosttyWasm, options: GhosttyOptions) { + this.wasm = wasm; + this._options = options; + } + + /** + * Load the ghostty-vt WASM binary and create a new `GhosttyCore`. + * The returned core is ready to be passed as the `core` option to `WTerm`. + */ + static async load(options: GhosttyOptions = {}): Promise { + const wasm = await loadGhosttyWasm(options.wasmPath); + return new GhosttyCore(wasm, options); + } + + // -- Lifecycle -- + + init(cols: number, rows: number): void { + this._cols = cols; + this._rows = rows; + const scrollback = this._options.scrollbackLimit ?? 10000; + this.termPtr = this.wasm.exports.init(cols, rows, scrollback); + this._allocViewportBuffer(); + this._invalidate(); + } + + resize(cols: number, rows: number): void { + this._cols = cols; + this._rows = rows; + this.wasm.exports.resize(this.termPtr, cols, rows); + this._allocViewportBuffer(); + this._invalidate(); + } + + // -- I/O -- + + writeString(str: string): void { + wasmWriteString(this.wasm, this.termPtr, str); + this._invalidate(); + } + + writeRaw(data: Uint8Array): void { + wasmWriteBytes(this.wasm, this.termPtr, data); + this._invalidate(); + } + + // -- Grid -- + + getCell(row: number, col: number): CellData { + this._ensureViewport(); + const view = this._viewportView; + if (!view) return BLANK_CELL; + + const idx = row * this._cols + col; + const byteOffset = idx * CELL_BYTES; + if (byteOffset + CELL_BYTES > this._viewportBufSize) return BLANK_CELL; + + const cell = parseCell(view, byteOffset); + 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; + } + + isDirtyRow(row: number): boolean { + this._ensureViewport(); + return this.wasm.exports.is_dirty_row(this.termPtr, row) !== 0; + } + + clearDirty(): void { + this.wasm.exports.clear_dirty(this.termPtr); + this._viewportStale = true; + } + + getCols(): number { + return this._cols; + } + + getRows(): number { + return this._rows; + } + + // -- Cursor -- + + getCursor(): CursorState { + this._ensureViewport(); + return { + row: this.wasm.exports.get_cursor_row(this.termPtr), + col: this.wasm.exports.get_cursor_col(this.termPtr), + visible: this.wasm.exports.get_cursor_visible(this.termPtr) !== 0, + }; + } + + // -- Modes -- + + cursorKeysApp(): boolean { + return this.wasm.exports.cursor_keys_app(this.termPtr) !== 0; + } + + bracketedPaste(): boolean { + return this.wasm.exports.bracketed_paste(this.termPtr) !== 0; + } + + usingAltScreen(): boolean { + return this.wasm.exports.using_alt_screen(this.termPtr) !== 0; + } + + // -- Side outputs -- + + getTitle(): string | null { + // Title changes are delivered through OSC sequences which the + // ReadonlyStream handler doesn't capture. A full stream handler + // would be needed for title support. + return null; + } + + getResponse(): string | null { + const bufSize = 4096; + const bufPtr = allocBuffer(this.wasm, bufSize); + if (bufPtr === 0) return null; + const len = this.wasm.exports.read_response( + this.termPtr, + bufPtr, + bufSize, + ); + if (len === 0) { + freeBuffer(this.wasm, bufPtr, bufSize); + return null; + } + const bytes = new Uint8Array( + this.wasm.exports.memory.buffer, + bufPtr, + len, + ); + const text = new TextDecoder().decode(bytes); + freeBuffer(this.wasm, bufPtr, bufSize); + return text; + } + + // -- Scrollback -- + + getScrollbackCount(): number { + return this.wasm.exports.get_scrollback_count(this.termPtr); + } + + getScrollbackCell(offset: number, col: number): CellData { + const maxCols = this._cols; + const lineSize = maxCols * CELL_BYTES; + const bufPtr = allocBuffer(this.wasm, lineSize); + if (bufPtr === 0) return BLANK_CELL; + + const len = this.wasm.exports.get_scrollback_line( + this.termPtr, + offset, + bufPtr, + maxCols, + ); + if (len === 0 || col >= len) { + freeBuffer(this.wasm, bufPtr, lineSize); + return BLANK_CELL; + } + + const view = new DataView( + this.wasm.exports.memory.buffer, + bufPtr, + lineSize, + ); + const cell = parseCell(view, col * CELL_BYTES); + freeBuffer(this.wasm, bufPtr, lineSize); + + return { + char: cell.codepoint || 32, + fg: DEFAULT_COLOR, + bg: DEFAULT_COLOR, + flags: cell.flags, + fgRgb: packRgb(cell.fgR, cell.fgG, cell.fgB), + bgRgb: packRgb(cell.bgR, cell.bgG, cell.bgB), + }; + } + + getScrollbackLineLen(offset: number): number { + const maxCols = this._cols; + const lineSize = maxCols * CELL_BYTES; + const bufPtr = allocBuffer(this.wasm, lineSize); + if (bufPtr === 0) return 0; + + const len = this.wasm.exports.get_scrollback_line( + this.termPtr, + offset, + bufPtr, + maxCols, + ); + freeBuffer(this.wasm, bufPtr, lineSize); + return len; + } + + // -- Debug -- + + getUnhandledSequences(): UnhandledSequence[] { + return []; + } + + // -- Internal helpers -- + + private _invalidate(): void { + this._viewportStale = true; + } + + private _allocViewportBuffer(): void { + if (this._viewportBufPtr !== 0) { + freeBuffer(this.wasm, this._viewportBufPtr, this._viewportBufSize); + } + this._viewportBufSize = this._cols * this._rows * CELL_BYTES; + this._viewportBufPtr = allocBuffer(this.wasm, this._viewportBufSize); + this._viewportView = null; + this._viewportStale = true; + } + + private _ensureViewport(): void { + if (!this._viewportStale) return; + this.wasm.exports.update(this.termPtr); + this.wasm.exports.get_viewport(this.termPtr, this._viewportBufPtr); + this._viewportView = new DataView( + this.wasm.exports.memory.buffer, + this._viewportBufPtr, + this._viewportBufSize, + ); + this._viewportStale = false; + } +} diff --git a/packages/@wterm/ghostty/src/index.ts b/packages/@wterm/ghostty/src/index.ts new file mode 100644 index 0000000..67d6f1d --- /dev/null +++ b/packages/@wterm/ghostty/src/index.ts @@ -0,0 +1,2 @@ +export { GhosttyCore } from "./ghostty-core.js"; +export type { GhosttyOptions } from "./ghostty-core.js"; diff --git a/packages/@wterm/ghostty/src/wasm-bindings.ts b/packages/@wterm/ghostty/src/wasm-bindings.ts new file mode 100644 index 0000000..1736868 --- /dev/null +++ b/packages/@wterm/ghostty/src/wasm-bindings.ts @@ -0,0 +1,183 @@ +/** + * Low-level typed bindings to the ghostty-vt WASM module built from + * our Zig export layer (zig/src/wasm_api.zig). + * + * Each exported Zig function maps 1:1 to a property on GhosttyExports. + * This module handles WASM loading, memory management, and cell parsing. + */ + +export interface GhosttyExports { + memory: WebAssembly.Memory; + + // Lifecycle + init(cols: number, rows: number, max_scrollback: number): number; + deinit(ptr: number): void; + resize(ptr: number, cols: number, rows: number): void; + + // Data input + write(ptr: number, data_ptr: number, data_len: number): void; + + // Render state + update(ptr: number): void; + get_viewport(ptr: number, buf_ptr: number): number; + + // Dirty tracking + is_dirty(ptr: number): number; + is_dirty_row(ptr: number, row: number): number; + clear_dirty(ptr: number): void; + + // Cursor + get_cursor_row(ptr: number): number; + get_cursor_col(ptr: number): number; + get_cursor_visible(ptr: number): number; + + // Modes + cursor_keys_app(ptr: number): number; + bracketed_paste(ptr: number): number; + using_alt_screen(ptr: number): number; + + // Grid + get_cols(ptr: number): number; + get_rows(ptr: number): number; + + // Scrollback + get_scrollback_count(ptr: number): number; + get_scrollback_line( + ptr: number, + offset: number, + buf_ptr: number, + max_cols: number, + ): number; + + // Responses + read_response(ptr: number, buf_ptr: number, buf_len: number): number; + + // Memory + alloc_buffer(len: number): number; + free_buffer(ptr: number, len: number): void; +} + +export interface GhosttyWasm { + exports: GhosttyExports; + instance: WebAssembly.Instance; +} + +const CELL_BYTES = 16; + +const DEFAULT_WASM_PATH = new URL("../wasm/ghostty-vt.wasm", import.meta.url) + .href; + +/** + * Load the ghostty-vt WASM module. + * + * @param wasmUrl - URL or path to the .wasm file. Defaults to the + * committed binary at `../wasm/ghostty-vt.wasm`. + */ +export async function loadGhosttyWasm( + wasmUrl?: string, +): Promise { + const url = wasmUrl ?? DEFAULT_WASM_PATH; + const response = await fetch(url); + const bytes = await response.arrayBuffer(); + + let wasmMemory: WebAssembly.Memory; + + const { instance } = await WebAssembly.instantiate(bytes, { + env: { + log(ptr: number, len: number) { + const text = new TextDecoder().decode( + new Uint8Array(wasmMemory.buffer, ptr, len), + ); + console.log("[ghostty-vt]", text); + }, + }, + }); + + wasmMemory = instance.exports.memory as WebAssembly.Memory; + const exports = instance.exports as unknown as GhosttyExports; + return { exports, instance }; +} + +/** Parsed cell data from the viewport buffer. */ +export interface WasmCellData { + codepoint: number; + fgR: number; + fgG: number; + fgB: number; + bgR: number; + bgG: number; + bgB: number; + flags: number; + width: number; + /** Bit 0: has explicit fg color, Bit 1: has explicit bg color */ + colorFlags: number; +} + +/** + * Parse a single cell from the viewport buffer at the given byte offset. + * The buffer layout matches the 16-byte struct from wasm_api.zig. + */ +export 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), + width: view.getUint8(byteOffset + 11), + colorFlags: view.getUint8(byteOffset + 12), + }; +} + +/** Byte size of one cell in the viewport buffer. */ +export { CELL_BYTES }; + +/** + * Allocate a buffer in WASM memory and return its pointer. + * The caller must free it with freeBuffer when done. + */ +export function allocBuffer(wasm: GhosttyWasm, size: number): number { + return wasm.exports.alloc_buffer(size); +} + +/** Free a buffer previously allocated with allocBuffer. */ +export function freeBuffer( + wasm: GhosttyWasm, + ptr: number, + size: number, +): void { + wasm.exports.free_buffer(ptr, size); +} + +/** + * Write a UTF-8 string into WASM memory and call the terminal's write + * function. Handles allocation/deallocation of the transfer buffer. + */ +export function writeString( + wasm: GhosttyWasm, + termPtr: number, + str: string, +): void { + const encoded = new TextEncoder().encode(str); + writeBytes(wasm, termPtr, encoded); +} + +/** + * Write raw bytes into the terminal. Handles allocation/deallocation + * of the transfer buffer. + */ +export function writeBytes( + wasm: GhosttyWasm, + termPtr: number, + data: Uint8Array, +): void { + if (data.length === 0) return; + const bufPtr = allocBuffer(wasm, data.length); + if (bufPtr === 0) return; + new Uint8Array(wasm.exports.memory.buffer, bufPtr, data.length).set(data); + wasm.exports.write(termPtr, bufPtr, data.length); + freeBuffer(wasm, bufPtr, data.length); +} diff --git a/packages/@wterm/ghostty/tsconfig.json b/packages/@wterm/ghostty/tsconfig.json new file mode 100644 index 0000000..f02d9c6 --- /dev/null +++ b/packages/@wterm/ghostty/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@internal/ts/tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"], + "exclude": ["src/__tests__"] +} diff --git a/packages/@wterm/ghostty/wasm/.gitkeep b/packages/@wterm/ghostty/wasm/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/@wterm/ghostty/wasm/ghostty-vt.wasm b/packages/@wterm/ghostty/wasm/ghostty-vt.wasm new file mode 100755 index 0000000000000000000000000000000000000000..bac589b1e7103fdec02f6f5bcef732a0bc188ed9 GIT binary patch literal 428644 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 )Q2@Tqnx59`7zLnoJ+J7K^2gM8e+c`Qoz=0 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

gL0 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 G!Tle_jAp6; literal 0 HcmV?d00001 diff --git a/packages/@wterm/ghostty/zig/build.zig b/packages/@wterm/ghostty/zig/build.zig new file mode 100644 index 0000000..f5d0328 --- /dev/null +++ b/packages/@wterm/ghostty/zig/build.zig @@ -0,0 +1,33 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const optimize = b.standardOptimizeOption(.{}); + + const wasm_target = b.resolveTargetQuery(.{ + .cpu_arch = .wasm32, + .os_tag = .freestanding, + }); + + const exe_mod = b.createModule(.{ + .root_source_file = b.path("src/wasm_api.zig"), + .target = wasm_target, + .optimize = optimize, + }); + + if (b.lazyDependency("ghostty", .{ + .target = wasm_target, + .simd = false, + })) |dep| { + exe_mod.addImport("ghostty-vt", dep.module("ghostty-vt")); + } + + const exe = b.addExecutable(.{ + .name = "ghostty-vt", + .root_module = exe_mod, + }); + + exe.rdynamic = true; + exe.entry = .disabled; + + b.installArtifact(exe); +} diff --git a/packages/@wterm/ghostty/zig/build.zig.zon b/packages/@wterm/ghostty/zig/build.zig.zon new file mode 100644 index 0000000..ac0c582 --- /dev/null +++ b/packages/@wterm/ghostty/zig/build.zig.zon @@ -0,0 +1,17 @@ +.{ + .name = .wterm_ghostty, + .version = "0.0.0", + .fingerprint = 0xe486cbc71ccad0ae, + .minimum_zig_version = "0.15.1", + .dependencies = .{ + .ghostty = .{ + .url = "https://github.com/ghostty-org/ghostty/archive/v1.3.1.tar.gz", + .hash = "ghostty-1.3.1-5UdBCwYm-gQeBa4bu1-sMooCQS4KVriv5wWSIJ_sI-Cb", + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/packages/@wterm/ghostty/zig/src/wasm_api.zig b/packages/@wterm/ghostty/zig/src/wasm_api.zig new file mode 100644 index 0000000..0bb88ab --- /dev/null +++ b/packages/@wterm/ghostty/zig/src/wasm_api.zig @@ -0,0 +1,336 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const vt = @import("ghostty-vt"); +const Terminal = vt.Terminal; +const Screen = vt.Screen; +const RenderState = vt.RenderState; +const Style = vt.Style; +const color = vt.color; +const modes = vt.modes; + +const Allocator = std.mem.Allocator; +const allocator = std.heap.wasm_allocator; + +pub const std_options: std.Options = .{ + .logFn = wasmLog, +}; + +fn wasmLog( + comptime level: std.log.Level, + comptime scope: @TypeOf(.EnumLiteral), + comptime format: []const u8, + args: anytype, +) void { + _ = level; + _ = scope; + var buf: [2048]u8 = undefined; + const str = std.fmt.bufPrint(&buf, format, args) catch return; + JS.log(str.ptr, str.len); +} + +const JS = struct { + extern "env" fn log(ptr: [*]const u8, len: usize) void; +}; + +// --------------------------------------------------------------- +// Cell layout written into the JS-owned viewport buffer. +// 16 bytes per cell, little-endian. +// +// offset size field +// ------ ---- ----- +// 0 4 codepoint (u32) +// 4 1 fg_r +// 5 1 fg_g +// 6 1 fg_b +// 7 1 bg_r +// 8 1 bg_g +// 9 1 bg_b +// 10 1 flags (bold=1, faint=2, italic=4, underline=8, +// blink=16, inverse=32, invisible=64, +// strikethrough=128) +// 11 1 width (0 = spacer, 1 = normal, 2 = wide) +// 12 1 color_flags (bit 0 = has explicit fg, +// bit 1 = has explicit bg) +// 13 3 reserved +// --------------------------------------------------------------- +const CELL_BYTES = 16; + +const State = struct { + terminal: Terminal, + stream: vt.ReadonlyStream, + render: RenderState, +}; + +fn stateFromPtr(ptr: usize) *State { + return @ptrFromInt(ptr); +} + +// -- Lifecycle -------------------------------------------------- + +export fn init(cols: u16, rows: u16, max_scrollback: u32) usize { + const state = allocator.create(State) catch return 0; + state.terminal = Terminal.init(allocator, .{ + .cols = cols, + .rows = rows, + .max_scrollback = max_scrollback, + }) catch { + allocator.destroy(state); + return 0; + }; + state.stream = state.terminal.vtStream(); + state.render = RenderState.empty; + return @intFromPtr(state); +} + +export fn deinit(ptr: usize) void { + const state = stateFromPtr(ptr); + state.render.deinit(allocator); + state.stream.deinit(); + state.terminal.deinit(allocator); + allocator.destroy(state); +} + +export fn resize(ptr: usize, cols: u16, rows: u16) void { + const state = stateFromPtr(ptr); + state.terminal.resize(allocator, cols, rows) catch {}; +} + +// -- Data input ------------------------------------------------- + +export fn write(ptr: usize, data_ptr: [*]const u8, data_len: u32) void { + const state = stateFromPtr(ptr); + state.stream.nextSlice(data_ptr[0..data_len]) catch {}; +} + +// -- Render state ----------------------------------------------- + +export fn update(ptr: usize) void { + const state = stateFromPtr(ptr); + state.render.update(allocator, &state.terminal) catch {}; +} + +fn packFlags(style: Style) u8 { + var f: u8 = 0; + if (style.flags.bold) f |= 0x01; + if (style.flags.faint) f |= 0x02; + if (style.flags.italic) f |= 0x04; + if (style.flags.underline != .none) f |= 0x08; + if (style.flags.blink) f |= 0x10; + if (style.flags.inverse) f |= 0x20; + if (style.flags.invisible) f |= 0x40; + if (style.flags.strikethrough) f |= 0x80; + return f; +} + +fn resolveRgb(c: Style.Color, palette: *const color.Palette) color.RGB { + return switch (c) { + .none => .{}, + .palette => |idx| palette[idx], + .rgb => |rgb| rgb, + }; +} + +fn cellWidth(cell: vt.Cell) u8 { + return switch (cell.wide) { + .narrow => 1, + .wide => 2, + .spacer_tail, .spacer_head => 0, + }; +} + +/// Write the entire viewport into a JS-provided flat buffer. +/// Returns the number of cells written (rows * cols). +export fn get_viewport(ptr: usize, buf_ptr: [*]u8) u32 { + const state = stateFromPtr(ptr); + const rs = &state.render; + const rows = rs.rows; + const cols = rs.cols; + const palette = &rs.colors.palette; + + const row_cells_slice = rs.row_data.items(.cells); + + var offset: usize = 0; + for (0..rows) |y| { + if (y >= row_cells_slice.len) { + // Pad remaining rows with blank cells + const remaining = (@as(usize, rows) - y) * @as(usize, cols) * CELL_BYTES; + @memset(buf_ptr[offset .. offset + remaining], 0); + break; + } + const cells_mal = row_cells_slice[y]; + const raw_cells = cells_mal.items(.raw); + const style_cells = cells_mal.items(.style); + + for (0..cols) |x| { + if (x >= raw_cells.len) { + @memset(buf_ptr[offset .. offset + CELL_BYTES], 0); + offset += CELL_BYTES; + continue; + } + const raw = raw_cells[x]; + const style = style_cells[x]; + + const cp: u32 = switch (raw.content_tag) { + .codepoint, .codepoint_grapheme => raw.content.codepoint, + else => 0, + }; + + const has_fg = style.fg_color != .none; + const has_bg_style = style.bg_color != .none; + const has_bg_cell = raw.content_tag == .bg_color_palette or raw.content_tag == .bg_color_rgb; + const has_bg = has_bg_style or has_bg_cell; + + const fg = if (has_fg) resolveRgb(style.fg_color, palette) else color.RGB{}; + const bg = if (has_bg_cell) switch (raw.content_tag) { + .bg_color_palette => palette[raw.content.color_palette], + .bg_color_rgb => blk: { + const c = raw.content.color_rgb; + break :blk color.RGB{ .r = c.r, .g = c.g, .b = c.b }; + }, + else => unreachable, + } else if (has_bg_style) resolveRgb(style.bg_color, palette) else color.RGB{}; + + const flags = packFlags(style); + const width = cellWidth(raw); + const color_flags: u8 = (if (has_fg) @as(u8, 1) else 0) | (if (has_bg) @as(u8, 2) else 0); + + std.mem.writeInt(u32, buf_ptr[offset..][0..4], cp, .little); + buf_ptr[offset + 4] = fg.r; + buf_ptr[offset + 5] = fg.g; + buf_ptr[offset + 6] = fg.b; + buf_ptr[offset + 7] = bg.r; + buf_ptr[offset + 8] = bg.g; + buf_ptr[offset + 9] = bg.b; + buf_ptr[offset + 10] = flags; + buf_ptr[offset + 11] = width; + buf_ptr[offset + 12] = color_flags; + buf_ptr[offset + 13] = 0; + buf_ptr[offset + 14] = 0; + buf_ptr[offset + 15] = 0; + offset += CELL_BYTES; + } + } + + return @as(u32, rows) * @as(u32, cols); +} + +// -- Dirty tracking --------------------------------------------- + +export fn is_dirty(ptr: usize) u32 { + const state = stateFromPtr(ptr); + return switch (state.render.dirty) { + .false => 0, + .partial => 1, + .full => 2, + }; +} + +export fn is_dirty_row(ptr: usize, row: u16) u32 { + const state = stateFromPtr(ptr); + const row_dirty = state.render.row_data.items(.dirty); + if (row >= row_dirty.len) return 0; + return if (row_dirty[row]) 1 else 0; +} + +export fn clear_dirty(ptr: usize) void { + const state = stateFromPtr(ptr); + state.render.dirty = .false; + const row_dirty = state.render.row_data.items(.dirty); + for (row_dirty) |*d| d.* = false; +} + +// -- Cursor ----------------------------------------------------- + +export fn get_cursor_row(ptr: usize) u32 { + const state = stateFromPtr(ptr); + return state.render.cursor.active.y; +} + +export fn get_cursor_col(ptr: usize) u32 { + const state = stateFromPtr(ptr); + return state.render.cursor.active.x; +} + +export fn get_cursor_visible(ptr: usize) u32 { + const state = stateFromPtr(ptr); + return if (state.render.cursor.visible) 1 else 0; +} + +// -- Modes ------------------------------------------------------ + +export fn cursor_keys_app(ptr: usize) u32 { + const state = stateFromPtr(ptr); + return if (state.terminal.modes.get(.cursor_keys)) 1 else 0; +} + +export fn bracketed_paste(ptr: usize) u32 { + const state = stateFromPtr(ptr); + return if (state.terminal.modes.get(.bracketed_paste)) 1 else 0; +} + +export fn using_alt_screen(ptr: usize) u32 { + const state = stateFromPtr(ptr); + return if (state.terminal.screens.active_key != .primary) 1 else 0; +} + +// -- Grid dimensions -------------------------------------------- + +export fn get_cols(ptr: usize) u32 { + const state = stateFromPtr(ptr); + return state.render.cols; +} + +export fn get_rows(ptr: usize) u32 { + const state = stateFromPtr(ptr); + return state.render.rows; +} + +// -- Scrollback ------------------------------------------------- + +export fn get_scrollback_count(ptr: usize) u32 { + const state = stateFromPtr(ptr); + const screen: *Screen = state.terminal.screens.active; + var total: usize = 0; + var node_ = screen.pages.pages.first; + while (node_) |node| : (node_ = node.next) { + total += node.data.size.rows; + } + if (total <= state.terminal.rows) return 0; + return @intCast(total - state.terminal.rows); +} + +export fn get_scrollback_line(ptr: usize, offset: u32, buf_ptr: [*]u8, max_cols: u32) u32 { + _ = ptr; + _ = offset; + _ = buf_ptr; + _ = max_cols; + // TODO: scrollback line reading requires navigating the page list + // backwards. This is a complex operation that will be implemented + // when scrollback support is prioritized. + return 0; +} + +// -- Responses -------------------------------------------------- + +export fn read_response(ptr: usize, buf_ptr: [*]u8, buf_len: u32) u32 { + _ = ptr; + _ = buf_ptr; + _ = buf_len; + // The ReadonlyStream ignores queries that produce responses. + // A full-featured stream handler would be needed to support + // device status reports and other response-generating sequences. + return 0; +} + +// -- Memory management ------------------------------------------ + +export fn alloc_buffer(len: u32) usize { + const buf = allocator.alloc(u8, len) catch return 0; + return @intFromPtr(buf.ptr); +} + +export fn free_buffer(buf_ptr: usize, len: u32) void { + const slice: [*]u8 = @ptrFromInt(buf_ptr); + allocator.free(slice[0..len]); +} + diff --git a/packages/@wterm/react/src/Terminal.tsx b/packages/@wterm/react/src/Terminal.tsx index ff5dcb3..f37062b 100644 --- a/packages/@wterm/react/src/Terminal.tsx +++ b/packages/@wterm/react/src/Terminal.tsx @@ -5,7 +5,7 @@ import { forwardRef, type HTMLAttributes, } from "react"; -import { WTerm } from "@wterm/dom"; +import { WTerm, type TerminalCore } from "@wterm/dom"; // onResize and onError are omitted from HTMLAttributes because we redefine // them with different signatures (terminal dimensions / WASM init errors). @@ -15,6 +15,11 @@ export interface TerminalProps extends Omit< > { cols?: number; rows?: number; + /** + * A pre-constructed terminal core. When provided, `wasmUrl` is ignored and + * this core is used instead of loading the built-in Zig WASM binary. + */ + core?: TerminalCore; wasmUrl?: string; theme?: string; autoResize?: boolean; @@ -39,6 +44,7 @@ const Terminal = forwardRef(function Terminal( { cols = 80, rows = 24, + core, wasmUrl, theme, autoResize = false, @@ -92,6 +98,7 @@ const Terminal = forwardRef(function Terminal( const wt = new WTerm(el, { cols, rows, + core, wasmUrl, autoResize: autoResizeRef.current, cursorBlink, @@ -125,7 +132,7 @@ const Terminal = forwardRef(function Terminal( }, // Re-run only when the WASM source changes // eslint-disable-next-line react-hooks/exhaustive-deps - [wasmUrl], + [core, wasmUrl], ); // Sync props to the existing instance (render-time checks) diff --git a/packages/@wterm/vue/src/Terminal.ts b/packages/@wterm/vue/src/Terminal.ts index 198ac79..cc6e4df 100644 --- a/packages/@wterm/vue/src/Terminal.ts +++ b/packages/@wterm/vue/src/Terminal.ts @@ -7,8 +7,9 @@ import { onMounted, onBeforeUnmount, watch, + type PropType, } from "vue"; -import { WTerm } from "@wterm/dom"; +import { WTerm, type TerminalCore } from "@wterm/dom"; /** * Vue wrapper around {@link WTerm} from `@wterm/dom`. Creates a `WTerm` in @@ -59,6 +60,11 @@ const Terminal = defineComponent({ * @defaultValue 24 */ rows: { type: Number, default: 24 }, + /** + * A pre-constructed terminal core. When provided, `wasmUrl` is ignored and + * this core is used instead of loading the built-in Zig WASM binary. + */ + core: { type: Object as PropType, default: undefined }, /** * Optional override for the WASM binary URL used by the terminal core. */ @@ -129,6 +135,7 @@ const Terminal = defineComponent({ const wt = new WTerm(el, { cols: props.cols, rows: props.rows, + core: props.core, wasmUrl: props.wasmUrl, autoResize: props.autoResize, cursorBlink: props.cursorBlink, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c2d832..432b5e2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -127,7 +127,7 @@ importers: version: 9.39.4(jiti@2.6.1) eslint-config-next: specifier: 16.2.3 - version: 16.2.3(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2) + version: 16.2.3(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2) tailwindcss: specifier: ^4 version: 4.2.2 @@ -135,6 +135,22 @@ importers: specifier: ^6.0.2 version: 6.0.2 + examples/ghostty: + dependencies: + '@wterm/dom': + specifier: workspace:* + version: link:../../packages/@wterm/dom + '@wterm/ghostty': + specifier: workspace:* + version: link:../../packages/@wterm/ghostty + devDependencies: + typescript: + specifier: ^6.0.2 + version: 6.0.2 + vite: + specifier: ^6.3.5 + version: 6.4.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) + examples/local: dependencies: '@tailwindcss/postcss': @@ -146,6 +162,9 @@ importers: '@wterm/dom': specifier: workspace:* version: link:../../packages/@wterm/dom + '@wterm/ghostty': + specifier: workspace:* + version: link:../../packages/@wterm/ghostty '@wterm/react': specifier: workspace:* version: link:../../packages/@wterm/react @@ -191,7 +210,7 @@ importers: version: 9.39.4(jiti@2.6.1) eslint-config-next: specifier: 16.2.3 - version: 16.2.3(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2) + version: 16.2.3(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2) tsx: specifier: ^4.19.4 version: 4.21.0 @@ -507,6 +526,19 @@ importers: specifier: ^6.0.2 version: 6.0.2 + packages/@wterm/ghostty: + dependencies: + '@wterm/core': + specifier: workspace:* + version: link:../core + devDependencies: + '@internal/ts': + specifier: workspace:* + version: link:../../@internal/ts + typescript: + specifier: ^6.0.2 + version: 6.0.2 + packages/@wterm/just-bash: devDependencies: '@internal/ts': @@ -11245,8 +11277,8 @@ snapshots: '@next/eslint-plugin-next': 16.2.3 eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-react-hooks: 7.0.1(eslint@9.39.4(jiti@2.6.1)) @@ -11288,21 +11320,6 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)): - dependencies: - '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.3 - eslint: 9.39.4(jiti@2.6.1) - get-tsconfig: 4.13.7 - is-bun-module: 2.0.0 - stable-hash: 0.0.5 - tinyglobby: 0.2.16 - unrs-resolver: 1.11.1 - optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) - transitivePeerDependencies: - - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 @@ -11314,32 +11331,22 @@ snapshots: tinyglobby: 0.2.16 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2) - eslint: 9.39.4(jiti@2.6.1) - eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) - transitivePeerDependencies: - - supports-color - - eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)): - dependencies: - debug: 3.2.7 - optionalDependencies: eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.10 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -11350,7 +11357,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -11379,7 +11386,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 From 3e0896706047a4cf3def88de35678035e3fab25d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 30 Apr 2026 13:56:42 -0500 Subject: [PATCH 2/3] style: fix prettier formatting --- packages/@wterm/core/src/wasm-bridge.ts | 7 ++++++- packages/@wterm/dom/src/renderer.ts | 21 ++++++++++---------- packages/@wterm/ghostty/src/ghostty-core.ts | 18 ++++++----------- packages/@wterm/ghostty/src/wasm-bindings.ts | 10 ++-------- 4 files changed, 25 insertions(+), 31 deletions(-) diff --git a/packages/@wterm/core/src/wasm-bridge.ts b/packages/@wterm/core/src/wasm-bridge.ts index cd5f371..981c300 100644 --- a/packages/@wterm/core/src/wasm-bridge.ts +++ b/packages/@wterm/core/src/wasm-bridge.ts @@ -1,4 +1,9 @@ -import type { CellData, CursorState, UnhandledSequence, TerminalCore } from "./terminal-core.js"; +import type { + CellData, + CursorState, + UnhandledSequence, + TerminalCore, +} from "./terminal-core.js"; interface WasmExports { memory: WebAssembly.Memory; diff --git a/packages/@wterm/dom/src/renderer.ts b/packages/@wterm/dom/src/renderer.ts index c32c593..4842c87 100644 --- a/packages/@wterm/dom/src/renderer.ts +++ b/packages/@wterm/dom/src/renderer.ts @@ -30,18 +30,12 @@ function colorToCSS(index: number): string | null { return `rgb(${level},${level},${level})`; } -function cellFgCSS( - fg: number, - fgRgb: number | undefined, -): string | null { +function cellFgCSS(fg: number, fgRgb: number | undefined): string | null { if (fgRgb !== undefined) return rgbToCSS(fgRgb); return colorToCSS(fg); } -function cellBgCSS( - bg: number, - bgRgb: number | undefined, -): string | null { +function cellBgCSS(bg: number, bgRgb: number | undefined): string | null { if (bgRgb !== undefined) return rgbToCSS(bgRgb); return colorToCSS(bg); } @@ -286,7 +280,13 @@ export class Renderer { if (inBounds && cp >= 0x2580 && cp <= 0x259f) { flushRun(col); - const colors = resolveColors(cell.fg, cell.bg, cell.flags, cell.fgRgb, cell.bgRgb); + const colors = resolveColors( + cell.fg, + cell.bg, + cell.flags, + cell.fgRgb, + cell.bgRgb, + ); const span = document.createElement("span"); span.className = col === cursorCol ? "term-block term-cursor" : "term-block"; @@ -432,7 +432,8 @@ export class Renderer { if (bottomRight.flags & FLAG_REVERSE) { gridBgIdx = bottomRight.fg; gridBgRgb = bottomRight.fgRgb; - if (gridBgRgb === undefined && gridBgIdx === DEFAULT_COLOR) gridBgIdx = 7; + if (gridBgRgb === undefined && gridBgIdx === DEFAULT_COLOR) + gridBgIdx = 7; } const containerBg = cellBgCSS(gridBgIdx, gridBgRgb) || ""; if (containerBg !== this.prevContainerBg) { diff --git a/packages/@wterm/ghostty/src/ghostty-core.ts b/packages/@wterm/ghostty/src/ghostty-core.ts index 4a9ab3e..0a67716 100644 --- a/packages/@wterm/ghostty/src/ghostty-core.ts +++ b/packages/@wterm/ghostty/src/ghostty-core.ts @@ -152,8 +152,10 @@ export class GhosttyCore implements TerminalCore { 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); + 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; } @@ -213,20 +215,12 @@ export class GhosttyCore implements TerminalCore { const bufSize = 4096; const bufPtr = allocBuffer(this.wasm, bufSize); if (bufPtr === 0) return null; - const len = this.wasm.exports.read_response( - this.termPtr, - bufPtr, - bufSize, - ); + const len = this.wasm.exports.read_response(this.termPtr, bufPtr, bufSize); if (len === 0) { freeBuffer(this.wasm, bufPtr, bufSize); return null; } - const bytes = new Uint8Array( - this.wasm.exports.memory.buffer, - bufPtr, - len, - ); + const bytes = new Uint8Array(this.wasm.exports.memory.buffer, bufPtr, len); const text = new TextDecoder().decode(bytes); freeBuffer(this.wasm, bufPtr, bufSize); return text; diff --git a/packages/@wterm/ghostty/src/wasm-bindings.ts b/packages/@wterm/ghostty/src/wasm-bindings.ts index 1736868..ba67e02 100644 --- a/packages/@wterm/ghostty/src/wasm-bindings.ts +++ b/packages/@wterm/ghostty/src/wasm-bindings.ts @@ -73,9 +73,7 @@ const DEFAULT_WASM_PATH = new URL("../wasm/ghostty-vt.wasm", import.meta.url) * @param wasmUrl - URL or path to the .wasm file. Defaults to the * committed binary at `../wasm/ghostty-vt.wasm`. */ -export async function loadGhosttyWasm( - wasmUrl?: string, -): Promise { +export async function loadGhosttyWasm(wasmUrl?: string): Promise { const url = wasmUrl ?? DEFAULT_WASM_PATH; const response = await fetch(url); const bytes = await response.arrayBuffer(); @@ -144,11 +142,7 @@ export function allocBuffer(wasm: GhosttyWasm, size: number): number { } /** Free a buffer previously allocated with allocBuffer. */ -export function freeBuffer( - wasm: GhosttyWasm, - ptr: number, - size: number, -): void { +export function freeBuffer(wasm: GhosttyWasm, ptr: number, size: number): void { wasm.exports.free_buffer(ptr, size); } From 5f535767c98ca4e932695462834706d4de2116bc Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 30 Apr 2026 14:10:40 -0500 Subject: [PATCH 3/3] fix: ghostty example build target, render perf, PTY spawn race - Set vite build target to esnext for top-level await support - Add write coalescing (setTimeout batching) to reduce animation flashing - Switch renderer to innerHTML for fewer DOM operations per frame - Defer PTY spawn until first RESIZE to eliminate zsh % artifact - Split local example into / (built-in) and /ghostty routes with toggle --- examples/ghostty/vite.config.ts | 6 ++- examples/local/app/core-toggle.tsx | 37 +++++++++++++ examples/local/app/ghostty/page.tsx | 80 +++++++++++++++++++++++++++ examples/local/app/page.tsx | 84 ++++++----------------------- examples/local/server.ts | 66 +++++++++++++---------- packages/@wterm/dom/src/renderer.ts | 58 ++++++++++++-------- packages/@wterm/dom/src/wterm.ts | 18 ++++--- 7 files changed, 222 insertions(+), 127 deletions(-) create mode 100644 examples/local/app/core-toggle.tsx create mode 100644 examples/local/app/ghostty/page.tsx diff --git a/examples/ghostty/vite.config.ts b/examples/ghostty/vite.config.ts index 6150f9a..87d46cd 100644 --- a/examples/ghostty/vite.config.ts +++ b/examples/ghostty/vite.config.ts @@ -1,3 +1,7 @@ import { defineConfig } from "vite"; -export default defineConfig({}); +export default defineConfig({ + build: { + target: "esnext", + }, +}); diff --git a/examples/local/app/core-toggle.tsx b/examples/local/app/core-toggle.tsx new file mode 100644 index 0000000..026d368 --- /dev/null +++ b/examples/local/app/core-toggle.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { usePathname, useRouter } from "next/navigation"; + +export function CoreToggle() { + const pathname = usePathname(); + const router = useRouter(); + const active = pathname === "/ghostty" ? "ghostty" : "builtin"; + + return ( +

+ Core +
+ + +
+
+ ); +} diff --git a/examples/local/app/ghostty/page.tsx b/examples/local/app/ghostty/page.tsx new file mode 100644 index 0000000..e9f55e0 --- /dev/null +++ b/examples/local/app/ghostty/page.tsx @@ -0,0 +1,80 @@ +"use client"; + +import { useCallback, useRef, useState, useEffect } from "react"; +import { Terminal, useTerminal } from "@wterm/react"; +import type { WTerm } from "@wterm/dom"; +import type { TerminalCore } from "@wterm/core"; +import { GhosttyCore } from "@wterm/ghostty"; +import { CoreToggle } from "../core-toggle"; +import "@wterm/react/css"; + +export default function GhosttyTerminal() { + const [debugEnabled] = useState( + () => + typeof window !== "undefined" && + new URLSearchParams(window.location.search).has("debug"), + ); + const [core, setCore] = useState(null); + const { ref, write } = useTerminal(); + const wsRef = useRef(null); + + useEffect(() => { + GhosttyCore.load({ wasmPath: "/ghostty-vt.wasm" }).then(setCore); + }, []); + + const handleReady = useCallback( + (wt: WTerm) => { + const proto = window.location.protocol === "https:" ? "wss:" : "ws:"; + const wsUrl = `${proto}//${window.location.host}/api/terminal`; + const ws = new WebSocket(wsUrl); + wsRef.current = ws; + + ws.onopen = () => { + ws.send(`\x1b[RESIZE:${wt.cols};${wt.rows}]`); + }; + + ws.onmessage = (event: MessageEvent) => { + write(event.data as string); + }; + + ws.onclose = () => { + write("\r\n\x1b[90m[session ended]\x1b[0m\r\n"); + wsRef.current = null; + }; + }, + [write], + ); + + const handleData = useCallback((data: string) => { + if (wsRef.current?.readyState === WebSocket.OPEN) { + wsRef.current.send(data); + } + }, []); + + const handleResize = useCallback((cols: number, rows: number) => { + if (wsRef.current?.readyState === WebSocket.OPEN) { + wsRef.current.send(`\x1b[RESIZE:${cols};${rows}]`); + } + }, []); + + if (!core) return null; + + return ( +
+ + +
+ ); +} diff --git a/examples/local/app/page.tsx b/examples/local/app/page.tsx index abceb16..3289e3d 100644 --- a/examples/local/app/page.tsx +++ b/examples/local/app/page.tsx @@ -3,42 +3,18 @@ import { useCallback, useRef, useState } from "react"; import { Terminal, useTerminal } from "@wterm/react"; import type { WTerm } from "@wterm/dom"; -import type { TerminalCore } from "@wterm/core"; -import { GhosttyCore } from "@wterm/ghostty"; +import { CoreToggle } from "./core-toggle"; import "@wterm/react/css"; -type CoreKind = "builtin" | "ghostty"; - -async function loadCore(kind: CoreKind): Promise { - if (kind === "ghostty") - return GhosttyCore.load({ wasmPath: "/ghostty-vt.wasm" }); - return undefined; -} - export default function LocalTerminal() { const [debugEnabled] = useState( () => typeof window !== "undefined" && new URLSearchParams(window.location.search).has("debug"), ); - const [activeCore, setActiveCore] = useState("builtin"); - const [core, setCore] = useState(undefined); - const [switching, setSwitching] = useState(false); const { ref, write } = useTerminal(); const wsRef = useRef(null); - const switchCore = useCallback(async (kind: CoreKind) => { - if (wsRef.current) { - wsRef.current.close(); - wsRef.current = null; - } - setSwitching(true); - const loaded = await loadCore(kind); - setCore(loaded); - setActiveCore(kind); - setSwitching(false); - }, []); - const handleReady = useCallback( (wt: WTerm) => { const proto = window.location.protocol === "https:" ? "wss:" : "ws:"; @@ -76,50 +52,20 @@ export default function LocalTerminal() { return (
-
- Core -
- - -
-
- {!switching && ( - - )} + +
); } diff --git a/examples/local/server.ts b/examples/local/server.ts index 4f4c098..cee1679 100644 --- a/examples/local/server.ts +++ b/examples/local/server.ts @@ -21,35 +21,37 @@ function cleanEnv(): Record { function handlePTYConnection(ws: WebSocket) { const shell = process.env.SHELL || "/bin/zsh"; - - let ptyProcess: pty.IPty; - try { - ptyProcess = pty.spawn(shell, ["-l"], { - name: "xterm-256color", - cols: 80, - rows: 24, - cwd: process.env.HOME || "/", - env: cleanEnv(), - }); - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - console.error(`Failed to spawn PTY: ${msg}`); - if (ws.readyState === WebSocket.OPEN) { - ws.send(`\r\n\x1b[31mFailed to spawn shell: ${msg}\x1b[0m\r\n`); - ws.close(); + let ptyProcess: pty.IPty | null = null; + + function spawnPTY(cols: number, rows: number) { + try { + ptyProcess = pty.spawn(shell, ["-l"], { + name: "xterm-256color", + cols, + rows, + cwd: process.env.HOME || "/", + env: cleanEnv(), + }); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + console.error(`Failed to spawn PTY: ${msg}`); + if (ws.readyState === WebSocket.OPEN) { + ws.send(`\r\n\x1b[31mFailed to spawn shell: ${msg}\x1b[0m\r\n`); + ws.close(); + } + return; } - return; - } - ptyProcess.onData((data) => { - if (ws.readyState === WebSocket.OPEN) { - ws.send(data); - } - }); + ptyProcess.onData((data) => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(data); + } + }); - ptyProcess.onExit(() => { - if (ws.readyState === WebSocket.OPEN) ws.close(); - }); + ptyProcess.onExit(() => { + if (ws.readyState === WebSocket.OPEN) ws.close(); + }); + } ws.on("message", (msg: Buffer | string) => { const input = typeof msg === "string" ? msg : msg.toString("utf-8"); @@ -57,16 +59,22 @@ function handlePTYConnection(ws: WebSocket) { if (input.startsWith("\x1b[RESIZE:")) { const match = input.match(/\x1b\[RESIZE:(\d+);(\d+)\]/); if (match) { - ptyProcess.resize(parseInt(match[1], 10), parseInt(match[2], 10)); + const cols = parseInt(match[1], 10); + const rows = parseInt(match[2], 10); + if (!ptyProcess) { + spawnPTY(cols, rows); + } else { + ptyProcess.resize(cols, rows); + } return; } } - ptyProcess.write(input); + if (ptyProcess) ptyProcess.write(input); }); ws.on("close", () => { - ptyProcess.kill(); + if (ptyProcess) ptyProcess.kill(); }); } diff --git a/packages/@wterm/dom/src/renderer.ts b/packages/@wterm/dom/src/renderer.ts index 4842c87..a5ed237 100644 --- a/packages/@wterm/dom/src/renderer.ts +++ b/packages/@wterm/dom/src/renderer.ts @@ -89,6 +89,13 @@ function appendRun(parent: HTMLElement, text: string, style: string): void { parent.appendChild(span); } +function escapeHTML(text: string): string { + return text + .replace(/&/g, "&") + .replace(//g, ">"); +} + function resolveColors( fg: number, bg: number, @@ -243,32 +250,39 @@ export class Renderer { cursorCol: number, rowIndex: number, ): void { - rowEl.textContent = ""; - + let html = ""; let runStyle = ""; let runText = ""; let runStart = 0; const flushRun = (endCol: number) => { if (!runText) return; + const escaped = escapeHTML(runText); if (cursorCol >= runStart && cursorCol < endCol) { const offset = cursorCol - runStart; - const before = runText.slice(0, offset); - const cursorChar = runText[offset]; - const after = runText.slice(offset + 1); - - if (before) appendRun(rowEl, before, runStyle); - - const cursorSpan = document.createElement("span"); - cursorSpan.className = "term-cursor"; - if (runStyle) cursorSpan.style.cssText = runStyle; - cursorSpan.textContent = cursorChar; - rowEl.appendChild(cursorSpan); - - if (after) appendRun(rowEl, after, runStyle); + const chars = [...runText]; + const before = chars.slice(0, offset).join(""); + const cursorChar = chars[offset] || " "; + const after = chars.slice(offset + 1).join(""); + + if (before) { + html += runStyle + ? `${escapeHTML(before)}` + : `${escapeHTML(before)}`; + } + html += runStyle + ? `${escapeHTML(cursorChar)}` + : `${escapeHTML(cursorChar)}`; + if (after) { + html += runStyle + ? `${escapeHTML(after)}` + : `${escapeHTML(after)}`; + } } else { - appendRun(rowEl, runText, runStyle); + html += runStyle + ? `${escaped}` + : `${escaped}`; } }; @@ -287,12 +301,10 @@ export class Renderer { cell.fgRgb, cell.bgRgb, ); - const span = document.createElement("span"); - span.className = - col === cursorCol ? "term-block term-cursor" : "term-block"; - span.style.background = getBlockBackground(cp, colors.fg, colors.bg); - if (cell.flags & FLAG_DIM) span.style.opacity = "0.5"; - rowEl.appendChild(span); + const cls = col === cursorCol ? "term-block term-cursor" : "term-block"; + const bg = getBlockBackground(cp, colors.fg, colors.bg); + const dim = cell.flags & FLAG_DIM ? "opacity:0.5;" : ""; + html += ``; runStyle = ""; runText = ""; @@ -315,6 +327,8 @@ export class Renderer { } flushRun(this.cols); + rowEl.innerHTML = html; + let bgCss = ""; if (lineLen >= this.cols && this.cols > 0) { const lastCell = getCell(this.cols - 1); diff --git a/packages/@wterm/dom/src/wterm.ts b/packages/@wterm/dom/src/wterm.ts index 8139d1d..0885ef9 100644 --- a/packages/@wterm/dom/src/wterm.ts +++ b/packages/@wterm/dom/src/wterm.ts @@ -34,6 +34,7 @@ export class WTerm { private renderer: Renderer | null = null; private input: InputHandler | null = null; private rafId: number | null = null; + private _renderTimer: ReturnType | null = null; private resizeObserver: ResizeObserver | null = null; private _destroyed = false; private _shouldScrollToBottom = false; @@ -172,12 +173,16 @@ export class WTerm { } private _scheduleRender(): void { - if (this.rafId == null) { - this.rafId = requestAnimationFrame(() => { - this.rafId = null; - this._doRender(); - }); - } + if (this._renderTimer != null) return; + this._renderTimer = setTimeout(() => { + this._renderTimer = null; + if (this.rafId == null) { + this.rafId = requestAnimationFrame(() => { + this.rafId = null; + this._doRender(); + }); + } + }, 0); } private _initialRender(): void { @@ -301,6 +306,7 @@ export class WTerm { destroy(): void { this._destroyed = true; + if (this._renderTimer != null) clearTimeout(this._renderTimer); if (this.rafId != null) cancelAnimationFrame(this.rafId); if (this.resizeObserver) this.resizeObserver.disconnect(); if (this.input) this.input.destroy();