Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
8 changes: 7 additions & 1 deletion apps/docs/src/app/api-reference/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,17 @@ The React and Vue `<Terminal>` components and the vanilla `WTerm` constructor al
<td><code>24</code></td>
<td>Initial row count</td>
</tr>
<tr>
<td><code>core</code></td>
<td><code>TerminalCore</code></td>
<td>—</td>
<td>A pre-constructed terminal core instance. When provided, <code>wasmUrl</code> is ignored and this core is used instead of loading the built-in Zig WASM binary. See <a href="/ghostty">Ghostty Core</a> for an example.</td>
</tr>
<tr>
<td><code>wasmUrl</code></td>
<td><code>string</code></td>
<td>—</td>
<td>URL to serve the WASM binary separately. When omitted, the ~12 KB binary is decoded from an inlined base64 string.</td>
<td>URL to serve the WASM binary separately. When omitted, the ~12 KB binary is decoded from an inlined base64 string. Ignored when <code>core</code> is provided.</td>
</tr>
<tr>
<td><code>autoResize</code></td>
Expand Down
6 changes: 5 additions & 1 deletion apps/docs/src/app/configuration/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
149 changes: 149 additions & 0 deletions apps/docs/src/app/ghostty/page.mdx
Original file line number Diff line number Diff line change
@@ -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 <Terminal core={core} />;
}
```

### Vue

```vue
<script setup lang="ts">
import { Terminal } from "@wterm/vue";
import { GhosttyCore } from "@wterm/ghostty";

const core = await GhosttyCore.load();
</script>

<template>
<Terminal :core="core" />
</template>
```

## Options

`GhosttyCore.load()` accepts an optional options object:

<table>
<thead>
<tr>
<th>Option</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>wasmPath</code></td>
<td><code>string</code></td>
<td>Custom path to the ghostty-vt WASM binary. By default it resolves to the committed binary inside the package.</td>
</tr>
<tr>
<td><code>scrollbackLimit</code></td>
<td><code>number</code></td>
<td>Maximum scrollback lines (default: 10000).</td>
</tr>
</tbody>
</table>

## 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

<table>
<thead>
<tr>
<th></th>
<th>Built-in (default)</th>
<th><code>@wterm/ghostty</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>Bundle size</td>
<td>~12 KB WASM (inlined)</td>
<td>~400 KB WASM (fetched)</td>
</tr>
<tr>
<td>VT compliance</td>
<td>Basic VT100/VT220/xterm</td>
<td>Comprehensive</td>
</tr>
<tr>
<td>Unicode</td>
<td>Single codepoints</td>
<td>Full grapheme clusters</td>
</tr>
<tr>
<td>Color model</td>
<td>256-color palette indices</td>
<td>Pre-resolved 24-bit RGB</td>
</tr>
<tr>
<td>Dependencies</td>
<td>None</td>
<td>None (WASM built from source)</td>
</tr>
<tr>
<td>Setup</td>
<td>Zero-config</td>
<td>Requires <code>@wterm/ghostty</code> install</td>
</tr>
</tbody>
</table>
2 changes: 1 addition & 1 deletion apps/docs/src/app/react/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function App() {

## Props

The `<Terminal>` 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 `<Terminal>` 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.

Expand Down
2 changes: 1 addition & 1 deletion apps/docs/src/app/vanilla/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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`).

Expand Down
2 changes: 1 addition & 1 deletion apps/docs/src/app/vue/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ function onData(chunk: string) {

## Props

The `<Terminal>` 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 `<Terminal>` 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 `<div>`.

Expand Down
11 changes: 11 additions & 0 deletions apps/docs/src/lib/docs-navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down Expand Up @@ -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,
},
],
},
{
Expand All @@ -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`,
Expand Down
1 change: 1 addition & 0 deletions apps/docs/src/lib/page-titles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const PAGE_TITLES: Record<string, string> = {
react: "React",
vue: "Vue",
vanilla: "Vanilla JS",
ghostty: "Ghostty Core",
"just-bash": "Just Bash",
markdown: "Markdown",
core: "Core / Advanced",
Expand Down
27 changes: 27 additions & 0 deletions examples/ghostty/README.md
Original file line number Diff line number Diff line change
@@ -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 `<div id="terminal">` |
27 changes: 27 additions & 0 deletions examples/ghostty/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>wterm — Ghostty Core Example</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
height: 100%;
background: #1a1a2e;
}
#terminal {
height: 100%;
}
</style>
</head>
<body>
<div id="terminal"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
21 changes: 21 additions & 0 deletions examples/ghostty/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
16 changes: 16 additions & 0 deletions examples/ghostty/src/main.ts
Original file line number Diff line number Diff line change
@@ -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",
);
13 changes: 13 additions & 0 deletions examples/ghostty/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"]
}
7 changes: 7 additions & 0 deletions examples/ghostty/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from "vite";

export default defineConfig({
build: {
target: "esnext",
},
});
1 change: 1 addition & 0 deletions examples/ghostty/wterm-dom.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module "@wterm/dom/css";
Loading
Loading