@slang25/ghostty-web
v0.4.1
Published
Web-based terminal emulator using Ghostty's VT100 parser via WebAssembly
Maintainers
Readme
Ghostty Web
A web-based terminal emulator that integrates Ghostty's VT100 parser via WebAssembly.
Installation
npm install @coder/ghostty-webOr install directly from GitHub (includes pre-built dist files):
# Latest from main branch
npm install github:coder/ghostty-web
# Specific commit or branch
npm install github:coder/ghostty-web#commit-sha
npm install github:rcarmo/ghostty-web # forkNote: GitHub installs work without requiring Zig because the repository includes pre-built
dist/files andghostty-vt.wasm.
Quick Start
import { Terminal } from '@coder/ghostty-web';
const term = new Terminal({ cols: 80, rows: 24 });
await term.open(document.getElementById('terminal'));
term.write('Hello, World!\r\n');See INSTALL.md for complete usage guide.
Features
- ✅ Full xterm.js-compatible API
- ✅ Production-tested VT100 parser (via Ghostty)
- ✅ ANSI colors (16, 256, RGB true color)
- ✅ Canvas rendering at 60 FPS
- ✅ Scrollback buffer
- ✅ Text selection & clipboard
- ✅ FitAddon for responsive sizing
- ✅ TypeScript declarations included
Development & Demos
Shell Terminal Demo
Requires server
# Terminal 1: Start PTY shell server
cd demo/server
bun install
bun run start
# Terminal 2: Start web server (from project root)
bun run dev
# Open: http://localhost:8000/demo/This provides a real persistent shell session! You can:
- Use
cdand it persists between commands - Run interactive programs like
vim,nano,top,htop - Use tab completion and command history (↑/↓)
- Use pipes, redirects, and background jobs
- Access all your shell aliases and environment
Alternative: Command-by-Command Mode
For the original file browser (executes each command separately):
cd demo/server
bun run file-browserRemote Access: If you're accessing via a forwarded hostname (e.g., mux.coder), make sure to forward both ports:
- Port 8000 (web server - Vite)
- Port 3001 (WebSocket server)
The terminal will automatically connect to the WebSocket using the same hostname you're accessing the page from.
Colors Demo (no server needed)
bun run dev
# Open: http://localhost:8000/demo/colors-demo.htmlSee all ANSI colors (16, 256, RGB) and text styles in action.
Usage
Basic Terminal
import { Terminal } from './lib/index.ts';
import { FitAddon } from './lib/addons/fit.ts';
// Create terminal
const term = new Terminal({
cols: 80,
rows: 24,
cursorBlink: true,
theme: {
background: '#1e1e1e',
foreground: '#d4d4d4',
},
});
// Add FitAddon for responsive sizing
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
// Open in container
await term.open(document.getElementById('terminal'));
fitAddon.fit();
// Write output (supports ANSI colors)
term.write('Hello, World!\r\n');
term.write('\x1b[1;32mGreen bold text\x1b[0m\r\n');
// Handle user input
term.onData((data) => {
console.log('User typed:', data);
// Send to backend, echo, etc.
});WebSocket Integration
const ws = new WebSocket('ws://localhost:3001/ws');
// Send user input to backend
term.onData((data) => {
ws.send(JSON.stringify({ type: 'input', data }));
});
// Display backend output
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
term.write(msg.data);
};URL Detection
Ghostty-web automatically detects and makes clickable:
- OSC 8 hyperlinks - Explicit terminal escape sequences (e.g., from
ls --hyperlink) - Plain text URLs - Common protocols detected via regex (https, http, mailto, ssh, git, ftp, tel, magnet)
URLs are detected on hover and can be opened with Ctrl/Cmd+Click.
// URL detection works automatically after opening terminal
await term.open(container);
// URLs in output become clickable automatically
term.write('Visit https://github.com for code\r\n');
term.write('Contact mailto:[email protected]\r\n');Custom Link Providers
Register custom providers to detect additional link types:
import { UrlRegexProvider } from '@coder/ghostty-web';
// Create custom provider
const myProvider = {
provideLinks(y, callback) {
// Your detection logic here
const links = detectCustomLinks(y);
callback(links);
},
};
// Register after opening terminal
term.registerLinkProvider(myProvider);See AGENTS.md for development guide and code patterns.
Custom Fonts
Ghostty-web supports custom font families. Font families with spaces are automatically quoted for proper CSS handling.
const term = new Terminal({
fontFamily: 'Fira Code, Consolas, monospace',
fontSize: 14,
});
await term.open(container);Loading Web Fonts
When using web fonts (e.g., Google Fonts, local .woff2 files), you must wait for the font to load before the terminal can measure it correctly:
// Option 1: Wait for specific font
await document.fonts.load('14px "Fira Code"');
term.loadFonts();
// Option 2: Wait for all fonts to be ready
await document.fonts.ready;
term.loadFonts();
// Option 3: Use FontFace API
const font = new FontFace('Fira Code', 'url(/fonts/FiraCode.woff2)');
await font.load();
document.fonts.add(font);
term.loadFonts();Changing Fonts at Runtime
// Change font family
term.options.fontFamily = 'JetBrains Mono, monospace';
// Change font size
term.options.fontSize = 16;
// If using a web font, wait for it to load
await document.fonts.load('16px "JetBrains Mono"');
term.loadFonts();Snapshot API (Playback Mode)
The Terminal supports a snapshot API for playback mode, enabling direct terminal state injection without re-parsing VT100 sequences. This is useful for terminal recordings and time-travel debugging.
import { Terminal, GhosttyCell } from '@coder/ghostty-web';
// Create cells array (flat row-major order: rows * cols cells)
const cells: GhosttyCell[] = recordedFrame.cells;
const cursor = { x: 10, y: 5 };
// Set snapshot - renderer will use this instead of WASM terminal
terminal.setSnapshot(cells, cursor);
// Check if in snapshot mode
if (terminal.hasSnapshot()) {
console.log('Playback mode active');
}
// Clear snapshot and return to normal rendering
terminal.clearSnapshot();Each GhosttyCell contains:
codepoint: Unicode codepoint (number)fg_r,fg_g,fg_b: Foreground RGB (0-255)bg_r,bg_g,bg_b: Background RGB (0-255)flags: Style flags (bold, italic, etc.)width: Character width (1 or 2 for wide chars)
Why This Approach?
DON'T re-implement VT100 parsing from scratch (years of work, thousands of edge cases).
DO use Ghostty's proven parser:
- ✅ Battle-tested by thousands of users
- ✅ Handles all VT100/ANSI quirks correctly
- ✅ Modern features (RGB colors, Kitty keyboard protocol)
- ✅ Get bug fixes and updates for free
You build: Screen buffer, rendering, UI (the "easy" parts in TypeScript)
Ghostty handles: VT100 parsing (the hard part via WASM)
Architecture
┌─────────────────────────────────────────┐
│ Terminal (lib/terminal.ts) │
│ - Public xterm.js-compatible API │
│ - Event handling (onData, onResize) │
└───────────┬─────────────────────────────┘
│
├─► ScreenBuffer (lib/buffer.ts)
│ - 2D grid, cursor, scrollback
│
├─► VTParser (lib/vt-parser.ts)
│ - ANSI escape sequence parsing
│ └─► Ghostty WASM (SGR parser)
│
├─► CanvasRenderer (lib/renderer.ts)
│ - Canvas-based rendering
│ - 60 FPS, supports all colors
│
└─► InputHandler (lib/input-handler.ts)
- Keyboard events → escape codes
└─► Ghostty WASM (Key encoder)
WebSocket Server (server/file-browser-server.ts)
└─► Executes shell commands (ls, cd, cat, etc.)Project Structure
├── lib/
│ ├── terminal.ts - Main Terminal class (xterm.js-compatible)
│ ├── buffer.ts - Screen buffer with scrollback
│ ├── vt-parser.ts - VT100/ANSI escape sequence parser
│ ├── renderer.ts - Canvas-based renderer
│ ├── input-handler.ts - Keyboard input handling
│ ├── ghostty.ts - Ghostty WASM wrapper
│ ├── types.ts - TypeScript type definitions
│ ├── interfaces.ts - xterm.js-compatible interfaces
│ └── addons/
│ └── fit.ts - FitAddon for responsive sizing
│
├── demo/
│ ├── index.html - File browser terminal
│ ├── colors-demo.html - ANSI colors showcase
│ └── server/
│ ├── file-browser-server.ts - WebSocket server
│ ├── package.json
│ └── start.sh - Startup script (auto-kills port conflicts)
│
├── docs/
│ └── API.md - Complete API documentation
│
└── ghostty-vt.wasm - Ghostty VT100 parser (122 KB)Building WASM
The WASM binary is built from source, not committed to the repo.
Requirements:
- Zig 0.15.2+
- Git submodules initialized
Build:
# Initialize submodule (first time only)
git submodule update --init --recursive
# Build WASM
./scripts/build-wasm.sh
# or
bun run build:wasmWhat it does:
- Initializes
ghostty/submodule (ghostty-org/ghostty) - Applies patches from
patches/ghostty-wasm-api.patch - Builds WASM with Zig (takes ~20 seconds)
- Outputs
ghostty-vt.wasm(404 KB) - Reverts patch to keep submodule clean
Updating Ghostty:
cd ghostty
git fetch origin
git checkout <commit-or-tag>
cd ..
./scripts/build-wasm.sh
# Test, then commit the updated submodule pointerCI: The WASM is built as part of the test and build jobs.
Testing
Run the test suite:
bun test # Run all tests
bun test --watch # Watch mode
bun run typecheck # Type checking
bun run build # Build distributionTest Coverage:
- ✅ ScreenBuffer (63 tests, 163 assertions)
- ✅ VTParser (45 tests)
- ✅ CanvasRenderer (11 tests)
- ✅ InputHandler (35 tests)
- ✅ Terminal integration (25 tests)
- ✅ FitAddon (12 tests)
Documentation
- AGENTS.md - Development guide for AI agents and developers
Links
License
See cmux LICENSE (AGPL-3.0)
