PromptHub
Developer Tools WebAssembly

Stop Wrestling with xterm.js Bugs! ghostty-web Is the Terminal Fix You Need

B

Bright Coding

Author

13 min read
28 views
Stop Wrestling with xterm.js Bugs! ghostty-web Is the Terminal Fix You Need

Stop Wrestling with xterm.js Bugs! ghostty-web Is the Terminal Fix You Need

What if I told you that every terminal in your browser is built on a house of cards? VS Code, Hyper, that slick cloud IDE you love—they all share the same hidden vulnerability. When a user pastes complex Devanagari script or triggers an obscure XTPUSHSGR escape sequence, everything falls apart. Terminal corruption. Rendering glitches. Hours of debugging that leads nowhere.

Sound familiar? You've probably normalized these failures. "That's just how web terminals are," you mutter, refreshing the page again.

It doesn't have to be this way.

What if you could swap your terminal engine for the exact same code that powers one of the fastest, most correct terminal emulators on the planet? What if that swap required changing a single import? No rewrite. No architectural overhaul. Just @xterm/xtermghostty-web.

This isn't hypothetical. The ghostty-web project—created by the team at Coder and leveraging Mitchell Hashimoto's legendary Ghostty emulator—has done exactly that. A WASM-compiled terminal parser with zero runtime dependencies, ~400KB bundle size, and true xterm.js API compatibility. The same battle-tested VT100 implementation that runs natively on macOS and Linux, now running in your browser.

Ready to see why developers are quietly abandoning hand-coded JavaScript terminal emulation? Let's dive in.


What Is ghostty-web?

ghostty-web is a browser-ready terminal emulator that compiles Ghostty's core parsing and rendering engine to WebAssembly (WASM). Created by Coder—the company behind cloud development environments—it was originally built for Mux, their desktop app for isolated, parallel agentic development. But the team designed it for universal use from day one.

The project's elevator pitch is deceptively simple: "Ghostty for the web with xterm.js API compatibility." Behind that simplicity lies something revolutionary. While xterm.js reimplements terminal emulation in JavaScript—every escape sequence, every Unicode edge case, every VT100 quirk hand-coded by fallible humans—ghostty-web ships the actual Ghostty parser, compiled from Zig to WASM. This is the same code that terminal enthusiasts have been raving about for its speed and correctness.

Here's why this matters now:

  • Mitchell Hashimoto has been developing libghostty, making Ghostty's core embeddable. The ghostty-web patches are already minimal and shrinking.
  • The web terminal space has stagnated. xterm.js dominates by default, not by merit. Developers accept its limitations because alternatives seem impossible.
  • WASM has matured. Browser performance for compiled code now rivals native in many scenarios. The ~400KB bundle proves you don't need bloat for power.

The project carries the MIT license, publishes to npm as ghostty-web, and actively maintains a live demo on an ephemeral VM. It's not experimental—it's production-ready infrastructure that Coder themselves depend on.


Key Features That Change Everything

Let's dissect what makes ghostty-web technically superior, feature by feature:

Drop-In xterm.js Replacement

The API compatibility claim isn't marketing fluff. You literally change your import from @xterm/xterm to ghostty-web. The Terminal class, configuration options, event handlers, lifecycle methods—they all map directly. This means:

  • Zero migration cost for existing projects
  • No retraining for developers familiar with xterm.js patterns
  • Plugin ecosystem compatibility where APIs overlap

WASM-Compiled Ghostty Core

The terminal parser isn't rewritten—it's transpiled from Zig. Ghostty's emulator handles:

  • Proper grapheme clustering for complex scripts (Devanagari, Arabic, emoji sequences)
  • Correct VT100/ANSI escape sequence parsing without the edge-case bugs that plague hand-coded implementations
  • XTPUSHSGR/XTPOPSGR support for advanced color stacking—completely missing from xterm.js

This matters because terminal emulation is extraordinarily complex. The ECMA-48 standard, VT100 extensions, and decades of de-facto behaviors create a specification surface that rivals web browsers. Hand-coded JavaScript implementations accumulate bugs linearly with complexity. Compiled code from a mature project inherits years of bug fixes and real-world testing.

Zero Runtime Dependencies

No dependency tree to audit. No node_modules bloat. No version conflicts. The ~400KB WASM bundle is self-contained. For security-conscious deployments and performance-critical applications, this is transformative.

Battle-Tested Correctness

Ghostty's native reputation precedes it. Terminal enthusiasts benchmark it against vttest, esctest, and real-world applications. When that same engine powers your web terminal, you inherit that validation for free.


Use Cases Where ghostty-web Dominates

1. Cloud IDEs and Browser-Based Development

When your entire product is a terminal in a browser, terminal correctness is existential. Gitpod, CodeSandbox, Replit, Coder itself—any platform where developers live in browser terminals benefits immediately. Complex scripts from international users render correctly. Obscure terminal applications work without debugging sessions.

2. CI/CD Dashboards and Build Monitoring

Build logs with ANSI color codes, progress bars, and terminal UI elements frequently break in web renderers. ghostty-web's correct escape sequence handling means your Jenkins, GitHub Actions, or custom build dashboards actually display what happened.

3. SSH and Remote Access Web Clients

Projects like ShellHub or internal bastion hosts need faithful terminal reproduction. When users connect to diverse server environments, they encounter every terminal quirk in existence. A correct emulator prevents the "works in my terminal client, broken in the web version" support burden.

4. Educational Platforms and Coding Tutorials

Interactive Python notebooks, terminal-based coding courses, and browser-based Linux environments serve global audiences. Devanagari variable names, Arabic comments, emoji in output—ghostty-web handles these without special-casing.

5. Embedded Terminal Widgets

Documentation sites, status pages, and admin panels increasingly include terminal UIs. When space is constrained and correctness matters, a 400KB zero-dependency bundle beats a multi-megabyte dependency tree with known rendering bugs.


Step-by-Step Installation & Setup Guide

Getting started takes minutes. Here's the complete path from zero to running terminal:

Prerequisites

  • Node.js 18+ (for native WebAssembly support)
  • A modern browser (Chrome 89+, Firefox 78+, Safari 15+, Edge 89+)
  • For the demo server: Linux or macOS recommended (Windows WSL works)

Installation

# Standard npm installation
npm install ghostty-web

# Or with yarn
yarn add ghostty-web

# Or with pnpm
pnpm add ghostty-web

Quick Verification

Test the live demo without installation:

# This downloads and runs the demo automatically
npx @ghostty-web/demo@next

Then open http://localhost:8080—you'll have a real shell running through ghostty-web.

Basic Project Setup

Create your entry point:

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>My ghostty-web Terminal</title>
  <style>
    body { margin: 0; background: #1a1b26; height: 100vh; }
    #terminal { width: 100%; height: 100%; }
  </style>
</head>
<body>
  <div id="terminal"></div>
  <script type="module" src="./main.js"></script>
</body>
</html>

Build Configuration

For bundlers, ensure WASM files are handled as assets:

// vite.config.js
export default {
  server: {
    headers: {
      // Required for SharedArrayBuffer if using advanced features
      'Cross-Origin-Opener-Policy': 'same-origin',
      'Cross-Origin-Embedder-Policy': 'require-corp',
    },
  },
  assetsInclude: ['**/*.wasm'], // Ensure WASM is copied to dist
};

Development Build (Contributors)

If you're hacking on ghostty-web itself:

# Clone the repository
git clone https://github.com/coder/ghostty-web.git
cd ghostty-web

# Install dependencies
bun install

# Build the WASM bundle and TypeScript wrappers
bun run build

Note: Building from source requires Zig (for compiling Ghostty's core) and Bun (for the build toolchain). The build applies a minimal patch to Ghostty's source to expose WASM-friendly APIs.


REAL Code Examples from the Repository

Let's examine actual code from the ghostty-web repository, with detailed explanations of how each piece works.

Example 1: Basic Initialization and Terminal Creation

This is the canonical usage pattern from the README, showing the complete lifecycle:

// Import both the async initializer and Terminal class
import { init, Terminal } from 'ghostty-web';

// Initialize the WASM module before any terminal operations
// This loads and compiles the ~400KB WebAssembly bundle
await init();

// Create terminal instance with familiar xterm.js-style options
const term = new Terminal({
  fontSize: 14,
  theme: {
    background: '#1a1b26',   // Tokyo Night dark background
    foreground: '#a9b1d6',   // Soft blue-white text
  },
});

// Mount to DOM element
term.open(document.getElementById('terminal'));

// Wire bidirectional data flow with WebSocket
term.onData((data) => websocket.send(data));        // User keystrokes → server
websocket.onmessage = (e) => term.write(e.data);    // Server output → terminal

Critical details: The await init() call is mandatory and must precede any Terminal instantiation. This pattern mirrors how xterm.js addons initialize, but here it's loading an entire WASM runtime. The onData/write pairing is the standard terminal-in-browser pattern—your transport layer (WebSocket, WebRTC, fetch polling) plugs in cleanly.

Example 2: Demo Server Implementation

The repository's demo shows production-grade integration. Here's the relevant client-side pattern from demo/index.html:

// From demo/index.html - production WebSocket terminal pattern
const websocket = new WebSocket(`ws://${location.host}/pty`);

// Terminal dimensions must match PTY for correct TTY behavior
const term = new Terminal({
  cols: 80,
  rows: 24,
  fontFamily: 'JetBrains Mono, monospace',
  fontSize: 14,
  cursorBlink: true,
  theme: {
    background: '#1a1b26',
    foreground: '#a9b1d6',
    cursor: '#c0caf5',
    selectionBackground: '#283457',
  },
});

term.open(document.getElementById('terminal'));

// Handle resize: critical for proper shell line wrapping
term.onResize(({ cols, rows }) => {
  websocket.send(JSON.stringify({ type: 'resize', cols, rows }));
});

// BinaryType arraybuffer for efficient data transfer
websocket.binaryType = 'arraybuffer';
websocket.onmessage = (e) => {
  // Convert ArrayBuffer to Uint8Array for term.write
  const data = new Uint8Array(e.data);
  term.write(data);
};

term.onData((data) => {
  websocket.send(JSON.stringify({ type: 'input', data }));
});

Why this matters: The demo implements proper PTY semantics. Resize events propagate to the server, which resizes the pseudoterminal—preventing the garbled output that occurs when terminal and PTY dimensions mismatch. The binaryType = 'arraybuffer' optimization avoids base64 overhead for large output streams.

Example 3: Build System Integration

The build process itself reveals architectural decisions:

# From the repository's package.json scripts
bun run build

This single command executes a sophisticated pipeline:

// Conceptual: Ghostty core in Zig (simplified representation)
// The actual patch exposes these functions for WASM:

// ghostty-wasm-api.patch adds these exports:
// - ghostty_init(): Initialize terminal state machine
// - ghostty_process(input: [*]u8, len: usize): Parse escape sequences
// - ghostty_render(): Generate screen buffer for canvas/WebGL
// - ghostty_resize(cols: u16, rows: u16): Update dimensions

The patch file (./patches/ghostty-wasm-api.patch) is the project's secret sauce. It minimally extends Ghostty's public API for WASM boundaries without forking the core. As Mitchell Hashimoto's libghostty matures, these patches will shrink to near-zero—eventually consuming an official Ghostty WASM distribution directly.


Advanced Usage & Best Practices

Performance Optimization

  • Use term.write with Uint8Array for binary data instead of strings—avoids UTF-8 re-encoding
  • Batch rapid updates: If receiving many small WebSocket messages, buffer 16ms (one frame) before calling term.write
  • Implement flow control: For high-volume output, use term.onData to send XON/XOFF or implement backpressure

Memory Management

The WASM heap grows automatically but never shrinks. For long-running terminals:

// Periodically check memory usage (advanced)
const memory = term._core?._wasmMemory; // Internal, may change
// If implementing custom: reload terminal on excessive growth

Theme Customization Beyond Basics

ghostty-web inherits Ghostty's theming power:

const term = new Terminal({
  theme: {
    background: '#1a1b26',
    foreground: '#a9b1d6',
    // Extended palette support
    black: '#15161e',
    red: '#f7768e',
    green: '#9ece6a',
    yellow: '#e0af68',
    blue: '#7aa2f7',
    magenta: '#bb9af7',
    cyan: '#7dcfff',
    white: '#a9b1d6',
    // Bright variants...
  },
});

Debugging Escape Sequences

When migrating from xterm.js, use Ghostty's strict parsing to find latent bugs:

// Enable verbose logging (if available in your build)
term.onData((data) => {
  console.debug('Input bytes:', [...data].map(b => b.toString(16)));
});

Comparison with Alternatives

Dimension xterm.js ghostty-web Winner
Bundle size ~300KB + dependencies ~400KB, zero dependencies ghostty-web (predictable)
Complex script rendering Known bugs (Devanagari, Arabic) Correct grapheme handling ghostty-web
XTPUSHSGR/XTPOPSGR Not supported (issue #2570) Full support ghostty-web
Migration effort Baseline (you're already here) Single import change ghostty-web
Ecosystem maturity Massive (plugins, themes, docs) Growing, xterm.js-compatible xterm.js (for now)
Native code pedigree Hand-coded JavaScript Battle-tested Zig/Ghostty ghostty-web
Active development Maintained Rapid iteration with libghostty Tie (both healthy)

The verdict: For new projects prioritizing correctness and maintainability, ghostty-web is the clear choice. For existing projects with heavy xterm.js plugin investment, the drop-in compatibility makes migration low-risk. The only scenario favoring xterm.js is immediate need for niche plugins that haven't been ported.


FAQ

Q: Is ghostty-web a complete replacement for xterm.js?

A: For core terminal emulation, yes. The API compatibility covers the common surface. Some xterm.js addons may need adaptation, but the core Terminal class, options, and event system map directly.

Q: How does the WASM bundle affect load time?

A: The ~400KB gzips to ~150KB typical. With HTTP/2 and modern caching, first load is competitive with xterm.js's dependency tree. Subsequent loads are instant from cache.

Q: Can I use ghostty-web with React/Vue/Svelte?

A: Absolutely. Import and initialize in useEffect/onMount, store the terminal instance in a ref, and clean up on unmount. The imperative API wraps cleanly in reactive frameworks.

Q: What browsers are supported?

A: Any with WebAssembly MVP support: Chrome 89+, Firefox 78+, Safari 15+, Edge 89+. This covers ~95% of global usage.

Q: How do I report rendering differences from native Ghostty?

A: File issues on coder/ghostty-web. Include the escape sequence or input that triggers the difference—Ghostty's deterministic parser makes reproduction reliable.

Q: Is this production-ready for my SaaS?

A: Coder uses it in Mux. The npm package has consistent downloads. The MIT license removes legal friction. Evaluate against your specific needs, but it's past experimental stage.

Q: Will ghostty-web lag behind Ghostty updates?

A: The project aims to consume official libghostty WASM builds once available. Currently, patches are minimal and updates track Ghostty releases closely.


Conclusion

We've normalized broken terminal emulation for too long. Complex scripts that render as gibberish. Escape sequences that corrupt the display. Hours lost to debugging issues in a dependency you didn't choose.

ghostty-web exposes this as the false tradeoff it always was.

You don't need to sacrifice correctness for convenience, or native performance for web deployment. By compiling Ghostty's battle-tested core to WASM and wrapping it in familiar xterm.js APIs, Coder has created something rare: a genuine upgrade with zero migration friction.

The ~400KB bundle. The zero dependencies. The proper grapheme handling. The XTPUSHSGR support that xterm.js has left broken for years. These aren't incremental improvements—they're category differences that matter when your terminal is your product.

My recommendation? Try the demo. Run npx @ghostty-web/demo@next. Load complex scripts. Trigger edge cases. See what correct terminal emulation feels like when you've been conditioned to accept brokenness.

Then make the switch. Your users—and your future debugging self—will thank you.

⭐ Star the repository: github.com/coder/ghostty-web

📦 Install now: npm install ghostty-web

🚀 Try the live demo: ghostty.ondis.co


Built with respect for the Ghostty team and Mitchell Hashimoto's vision for libghostty. The future of terminal emulation is compiled, correct, and finally available in your browser.

Comments (0)

Comments are moderated before appearing.

No comments yet. Be the first to share your thoughts!

Support us! ☕