# ghostty-web Addon Compatibility Research

> **Package:** ghostty-web@0.4.0  
> **Date:** 2026-05-28  
> **Source:** npm pack + manual inspection of UMD build, ESM build, and TypeScript definitions

---

## 1. UMD Build Globals

The UMD build (`dist/ghostty-web.umd.cjs`, 639KB) uses standard `(function(e){})(this)` wrapping. When loaded via `<script>` tag, the module assigns to `exports` (CommonJS) or `this`. The following symbols are exported:

| Export Name | Type | Description |
|---|---|---|
| `Terminal` | class | Main terminal class (xterm.js-compatible API) |
| `FitAddon` | class | Bundled FitAddon (activate/dispose/fit/proposeDimensions) |
| `Ghostty` | class | WASM wrapper — `Ghostty.load(wasmPath?)` creates instances |
| `init` | async function | Convenience initializer — calls `Ghostty.load()` internally |
| `getGhostty` | function | Returns the singleton Ghostty instance (internal) |
| `CanvasRenderer` | class | Canvas-based terminal renderer |
| `EventEmitter` | class | Generic event emitter |
| `GhosttyTerminal` | class | Low-level WASM terminal wrapper |
| `InputHandler` | class | Keyboard/input event handler |
| `KeyEncoder` | class | Key encoding for WASM |
| `KeyEncoderOption` | enum | Key encoder options |
| `LinkDetector` | class | URL and link detection |
| `OSC8LinkProvider` | class | OSC8 hyperlink provider |
| `UrlRegexProvider` | class | Regex-based URL provider |
| `SelectionManager` | class | Text selection management |
| `CellFlags` | enum | Cell style flags (bold, italic, etc.) |

**Key difference from xterm.js:** The UMD build does NOT expose a single global like `window.Terminal`. It needs a module loader or manual extraction from the exports object. For `<script>` tag loading, the script would need to be wrapped or the page would use `require()` / a UMD shim to access the exports.

**Recommended approach for vendoring:** Use the UMD file with a small wrapper that extracts `Terminal`, `FitAddon`, and `init` onto `window`:
```javascript
// In a wrapper or after loading ghostty-web.umd.cjs
window.GhosttyWeb = require('ghostty-web'); // or the UMD exports object
```

---

## 2. FitAddon Status: COMPATIBLE (Native)

ghostty-web bundles FitAddon natively. It is exported directly from the package as `FitAddon`.

**API surface:**

| Method | Signature | xterm.js FitAddon Match |
|---|---|---|
| `activate(terminal)` | `activate(terminal: ITerminalCore): void` | Yes |
| `dispose()` | `dispose(): void` | Yes |
| `fit()` | `fit(): void` | Yes |
| `proposeDimensions()` | `proposeDimensions(): {cols, rows} \| undefined` | Yes |
| `observeResize()` | `observeResize(): void` | **Extra** — not in xterm.js FitAddon |

**Implementation notes:**
- Uses `terminal.renderer.getMetrics()` instead of xterm.js's internal `_core._renderService.dimensions` — this means xterm.js's FitAddon won't work with ghostty-web (different internal API), but ghostty-web's native FitAddon works perfectly.
- `proposeDimensions()` reads `element.clientWidth/clientHeight` and computes `{cols, rows}` from font metrics — same pattern as xterm.js FitAddon.
- Includes a debounced `ResizeObserver` via `observeResize()` — bonus feature not in xterm.js FitAddon.
- Minimum dimensions enforced: 2 cols, 1 row.

**Verdict:** Use ghostty-web's native FitAddon. Drop xterm-addon-fit vendor file.

---

## 3. loadAddon() Compatibility

ghostty-web's `Terminal.loadAddon()` implementation:

```javascript
loadAddon(addon) {
  addon.activate(this);
  this.addons.push(addon);
}
```

**Interface contract (from TypeScript definitions):**

```typescript
interface ITerminalAddon {
  activate(terminal: ITerminalCore): void;
  dispose(): void;
}

interface ITerminalCore {
  cols: number;
  rows: number;
  element?: HTMLElement;
  textarea?: HTMLTextAreaElement;
}
```

This is a minimal interface. xterm.js addons that only use `cols`, `rows`, `element`, and public API methods (write, onData, etc.) will work. Addons that reach into xterm.js internals (`_core`, `_renderService`, etc.) will break.

---

## 4. SearchAddon Compatibility: WILL NOT WORK

xterm.js SearchAddon (`@xterm/addon-search`) reaches deep into xterm.js internals:
- Accesses `terminal._core._bufferService` for buffer traversal
- Uses `terminal._core._decorationService` for match highlighting
- Relies on xterm.js's internal `IBuffer` / `IBufferLine` with `getCell()` API

ghostty-web exposes a `buffer` property with `IBufferNamespace` (active, normal, alternate buffers with `getLine()` and `getCell()`), but the internal structure differs from xterm.js — the SearchAddon accesses `_core` which does not exist.

**Fallback plan:** Implement search natively using ghostty-web's public `buffer` API:
- `terminal.buffer.active.getLine(y)` returns `IBufferLine` with `getCell(x).getChars()`
- Iterate lines, build text, find matches, use `terminal.select(col, row, length)` to highlight
- Our current search usage (Ctrl+F bar with `findNext`/`findPrevious`) can be reimplemented in ~50-80 lines using this public API

---

## 5. WebLinksAddon Compatibility: NOT NEEDED

ghostty-web has **built-in link detection** that replaces xterm.js WebLinksAddon:

- `LinkDetector` class handles URL detection internally
- `UrlRegexProvider` provides regex-based URL detection (same as WebLinksAddon)
- `OSC8LinkProvider` handles OSC8 hyperlinks (explicit terminal hyperlinks)
- Both providers are registered automatically during `terminal.open()`
- Links are underlined on hover and clickable (Ctrl/Cmd+click)

Additionally, `terminal.registerLinkProvider(provider)` accepts custom link providers with the interface:
```typescript
interface ILinkProvider {
  provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void;
  dispose?(): void;
}
```

**Verdict:** Drop xterm-addon-web-links vendor file. ghostty-web handles this natively.

---

## 6. ImageAddon Compatibility: WILL NOT WORK

xterm.js ImageAddon (`@xterm/addon-image`) provides inline image rendering (Sixel, iTerm2 IIP, Kitty graphics protocol). It hooks into xterm.js's render layer system (`_core._renderService`) to overlay images on the terminal canvas.

ghostty-web uses its own `CanvasRenderer` with a completely different rendering pipeline (two-pass cell rendering via WASM). There is no render layer plugin system.

**Current usage assessment:** ImageAddon is loaded optionally in our terminal.js (`if (ImageAddon) { ... }`). It's used for Sixel/iTerm2 inline image rendering. This is a nice-to-have feature, not critical for core terminal functionality.

**Fallback plan:** Drop ImageAddon for now. Ghostty's native terminal supports Kitty graphics protocol — this may be exposed in future ghostty-web versions. Monitor the ghostty-web repo for graphics protocol support.

---

## 7. init() and WASM URL Configuration

### init() function

```javascript
// Simplified from source
let ghosttyInstance = null;
async function init() {
  if (!ghosttyInstance) {
    ghosttyInstance = await Ghostty.load();
  }
}
```

`init()` takes **no parameters**. It calls `Ghostty.load()` with no arguments.

### Ghostty.load(wasmPath?) — the key function

```typescript
static load(wasmPath?: string): Promise<Ghostty>;
```

**If `wasmPath` is provided:** Loads WASM directly from that path. This is the mechanism for `/vendor/` serving.

**If `wasmPath` is omitted (default `init()`):** Tries multiple fallback locations in order:
1. Data URL with embedded tiny WASM stub (for probing — this is just the WASM header)
2. `file://` protocol path (for Node/Bun environments)
3. URL relative to the JS file (via `import.meta.url` in ESM)
4. `./ghostty-vt.wasm` (relative to page)
5. `/ghostty-vt.wasm` (root-relative)

**Recommended approach for our vendoring pattern:**

Don't use `init()` — use `Ghostty.load()` directly with an explicit WASM path:

```javascript
const ghostty = await GhosttyWeb.Ghostty.load('/vendor/ghostty-vt.wasm');
const term = new GhosttyWeb.Terminal({ ghostty: ghostty });
```

This gives full control over WASM location and avoids the fallback probe chain.

### WASM file details

- **File:** `ghostty-vt.wasm`
- **Size:** 423KB (413KB gzipped estimate: ~180-200KB)
- **Duplicated:** Same file at both `package/ghostty-vt.wasm` and `package/dist/ghostty-vt.wasm`
- **Content:** Ghostty's VT100 parser compiled to WebAssembly

---

## 8. CSS Requirements

**ghostty-web does NOT ship any CSS files.** No `xterm.css` equivalent exists.

The terminal renders entirely via `<canvas>` element. All styling (colors, fonts, cursor) is handled through:
- Constructor options: `theme`, `fontSize`, `fontFamily`, `cursorStyle`, `cursorBlink`
- Canvas rendering in `CanvasRenderer`
- Inline styles applied programmatically to the container element

**Implication:** When swapping from xterm.js, remove the `xterm.css` stylesheet link. No replacement CSS is needed. The terminal container just needs basic CSS for sizing (width/height).

---

## 9. API Surface Differences

### Compatible APIs (same as xterm.js)

| API | Notes |
|---|---|
| `new Terminal(options)` | Same options: cols, rows, theme, fontSize, fontFamily, cursorBlink, cursorStyle, scrollback, convertEol, disableStdin, allowTransparency |
| `term.open(element)` | Same |
| `term.write(data, callback?)` | Accepts both `string` and `Uint8Array` |
| `term.writeln(data, callback?)` | Same |
| `term.resize(cols, rows)` | Same |
| `term.clear()` | Same |
| `term.reset()` | Same |
| `term.focus()` / `term.blur()` | Same |
| `term.dispose()` | Same — also cleans up addons |
| `term.loadAddon(addon)` | Same interface |
| `term.onData` / `term.onResize` / `term.onTitleChange` / `term.onBell` / `term.onSelectionChange` / `term.onKey` / `term.onScroll` / `term.onRender` / `term.onCursorMove` | Same event API |
| `term.cols` / `term.rows` | Same |
| `term.element` / `term.textarea` | Same |
| `term.buffer` | IBufferNamespace with active/normal/alternate — similar to xterm.js |
| `term.getSelection()` / `term.hasSelection()` / `term.clearSelection()` / `term.selectAll()` / `term.select()` | Same |
| `term.paste(data)` | Same — handles bracketed paste |
| `term.attachCustomKeyEventHandler(handler)` | Same |
| `term.registerLinkProvider(provider)` | Same interface |
| `term.scrollLines()` / `term.scrollPages()` / `term.scrollToTop()` / `term.scrollToBottom()` | Same |
| `term.options` | Proxy object — runtime changes trigger updates (fontSize, fontFamily, cursorBlink, etc.) |
| `term.unicode.activeVersion` | Returns "15.1" |

### Different / Additional APIs

| API | Difference |
|---|---|
| `init()` | **New** — must be called before creating Terminal (or pass `ghostty` option) |
| `Ghostty.load(wasmPath?)` | **New** — explicit WASM loading with path control |
| `new Terminal({ ghostty })` | **New option** — pass pre-loaded Ghostty instance |
| `term.input(data, wasUserInput?)` | **New** — input as if typed by user |
| `term.renderer` | **Exposed** — CanvasRenderer is public (xterm.js hides this) |
| `term.wasmTerm` | **Exposed** — direct access to WASM terminal |
| `term.attachCustomWheelEventHandler()` | **New** — custom scroll handling |
| `term.smoothScrollTo()` | **New** — animated scrolling |
| `FitAddon.observeResize()` | **New** — auto-fit on container resize via ResizeObserver |
| `term.getMode()` / `term.hasBracketedPaste()` / `term.hasFocusEvents()` / `term.hasMouseTracking()` | **New** — terminal mode queries |

### Missing APIs (present in xterm.js, absent in ghostty-web)

| API | Impact |
|---|---|
| `term.registerMarker()` | Not available — used by some addons internally |
| `term.registerDecoration()` | Not available — used by SearchAddon for highlighting |
| `term._core` | Not available — internal access used by many addons |

---

## 10. Summary & Recommendations

| Addon | Status | Action |
|---|---|---|
| **FitAddon** | Native in ghostty-web | Use `FitAddon` from ghostty-web. Drop `xterm-addon-fit.js`. |
| **WebLinksAddon** | Native in ghostty-web | Built-in `LinkDetector` + `UrlRegexProvider`. Drop `xterm-addon-web-links.js`. |
| **SearchAddon** | Incompatible | Reimplement using `terminal.buffer` public API + `terminal.select()`. ~50-80 lines. |
| **ImageAddon** | Incompatible | Drop for now. Monitor ghostty-web for future graphics protocol support. |

**Migration blockers:** None. All four addons have a path forward.

**Critical init change:** Must call `await Ghostty.load('/vendor/ghostty-vt.wasm')` before creating Terminal instances. This is the only breaking change vs xterm.js's synchronous `new Terminal()`.