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
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()readselement.clientWidth/clientHeightand computes{cols, rows}from font metrics — same pattern as xterm.js FitAddon.- Includes a debounced
ResizeObserverviaobserveResize()— 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._bufferServicefor buffer traversal - Uses
terminal._core._decorationServicefor match highlighting - Relies on xterm.js's internal
IBuffer/IBufferLinewithgetCell()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)returnsIBufferLinewithgetCell(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:
LinkDetectorclass handles URL detection internallyUrlRegexProviderprovides regex-based URL detection (same as WebLinksAddon)OSC8LinkProviderhandles 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:
- Data URL with embedded tiny WASM stub (for probing — this is just the WASM header)
file://protocol path (for Node/Bun environments)- URL relative to the JS file (via
import.meta.urlin ESM) ./ghostty-vt.wasm(relative to page)/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.wasmandpackage/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().