ecc6c6979c
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
275 lines
12 KiB
Markdown
275 lines
12 KiB
Markdown
# 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()`. |