Files
muxplex/docs/superpowers/plans/ghostty-web-addon-research.md
T
Ken ecc6c6979c docs: ghostty-web addon compatibility research
Research findings from inspecting ghostty-web@0.4.0 npm package:
- UMD build exports 16 symbols (Terminal, FitAddon, Ghostty, init, etc.)
- FitAddon: native in ghostty-web, API-compatible, use directly
- WebLinksAddon: not needed, ghostty-web has built-in LinkDetector
- SearchAddon: incompatible (needs _core internals), reimplement via buffer API
- ImageAddon: incompatible (different render pipeline), drop for now
- init()/Ghostty.load(wasmPath): supports explicit WASM path for vendoring
- No CSS required (canvas-only rendering)
- Core Terminal API is highly compatible with xterm.js
2026-05-28 06:06:42 +00:00

12 KiB

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:

// 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:

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

Interface contract (from TypeScript definitions):

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:

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

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

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:

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().