Something went wrong. Try again.
Ghostty for the web with xterm.js API compatibility
Something went wrong. Try again.
ghostty-web index.html
12 kB · 361 lines
HTML
at init
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>WASM Terminal Demo - xterm.js API Compatible</title> <style> body { font-family: system-ui, -apple-system, sans-serif; max-width: 1000px; margin: 40px auto; padding: 0 20px; background: #1e1e1e; color: #fff; } h1 { color: #fff; } #terminal { border: 2px solid #333; border-radius: 4px; overflow: hidden; margin: 20px 0; } .controls { margin: 20px 0; display: flex; gap: 10px; flex-wrap: wrap; } button { background: #0066cc; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-size: 14px; } button:hover { background: #0052a3; } input[type="text"] { padding: 10px; font-size: 14px; border: 1px solid #555; border-radius: 4px; background: #2a2a2a; color: #fff; flex: 1; min-width: 200px; } .info { background: #2a2a2a; padding: 15px; border-radius: 4px; margin: 20px 0; font-family: 'Courier New', monospace; font-size: 12px; } .status { color: #4a9eff; } </style></head><body> <h1>🚀 WASM Terminal Demo</h1> <p>xterm.js-compatible API using libghostty-vt WASM + TypeScript</p> <div id="terminal"></div> <div class="controls"> <input type="text" id="input" placeholder="Type something and press Enter..." /> <button onclick="writeHello()">Write "Hello, World!"</button> <button onclick="clearTerminal()">Clear</button> <button onclick="resizeTerminal()">Resize 100x30</button> <button onclick="writeColors()">Test Colors</button> </div> <div class="info"> <div class="status">Status: <span id="status">Initializing...</span></div> <div>Terminal Size: <span id="size">80x24</span></div> <div>WASM Module: <span id="wasm-status">Loading...</span></div> </div>
<script type="module"> // Inline TypeScript terminal implementation (normally would import) class EventEmitter { constructor() { this.listeners = []; } fire(data) { this.listeners.forEach(listener => listener(data)); } event(listener) { this.listeners.push(listener); return { dispose: () => { const index = this.listeners.indexOf(listener); if (index >= 0) this.listeners.splice(index, 1); } }; } }
class Terminal { constructor(options = {}) { this.cols = options.cols || 80; this.rows = options.rows || 24; this.scrollback = options.scrollback || 1000; this.buffer = this.createBuffer(this.rows); this.cursorX = 0; this.cursorY = 0; this.container = null; this.element = null; this.renderRequested = false; this.onDataEmitter = new EventEmitter(); this.onResizeEmitter = new EventEmitter(); }
createBuffer(rows) { const buffer = []; for (let y = 0; y < rows; y++) { buffer.push(this.createRow()); } return buffer; }
createRow() { const row = []; for (let x = 0; x < this.cols; x++) { row.push({ char: ' ', fg: 7, bg: 0, bold: false, italic: false, underline: false }); } return row; }
open(parent) { this.container = parent; this.element = document.createElement('div'); this.element.className = 'terminal-container'; this.element.style.cssText = ` font-family: 'Courier New', monospace; font-size: 14px; line-height: 1.2; background: #000; color: #fff; padding: 10px; overflow: hidden; white-space: pre; min-height: 400px; `; parent.appendChild(this.element); this.requestRender(); }
write(data) { for (let i = 0; i < data.length; i++) { this.writeChar(data[i]); } this.requestRender(); }
writeChar(char) { const code = char.charCodeAt(0);
if (code === 10) { // \n this.cursorY++; this.cursorX = 0; if (this.cursorY >= this.rows) { this.scroll(); this.cursorY = this.rows - 1; } return; }
if (code === 13) { // \r this.cursorX = 0; return; }
if (code === 8) { // backspace if (this.cursorX > 0) { this.cursorX--; } return; }
if (code >= 32) { const cell = this.buffer[this.cursorY][this.cursorX]; cell.char = char;
this.cursorX++; if (this.cursorX >= this.cols) { this.cursorX = 0; this.cursorY++; if (this.cursorY >= this.rows) { this.scroll(); this.cursorY = this.rows - 1; } } } }
scroll() { this.buffer.shift(); this.buffer.push(this.createRow()); }
requestRender() { if (!this.renderRequested) { this.renderRequested = true; requestAnimationFrame(() => this.render()); } }
render() { this.renderRequested = false; if (!this.element) return;
let html = ''; for (let y = 0; y < this.rows; y++) { const row = this.buffer[y]; let line = ''; for (let x = 0; x < this.cols; x++) { line += row[x].char; } html += line + '\n'; }
this.element.textContent = html; }
resize(cols, rows) { this.cols = cols; this.rows = rows;
while (this.buffer.length < rows) { this.buffer.push(this.createRow()); } while (this.buffer.length > rows) { this.buffer.pop(); }
for (const row of this.buffer) { while (row.length < cols) { row.push({ char: ' ', fg: 7, bg: 0, bold: false, italic: false, underline: false }); } while (row.length > cols) { row.pop(); } }
if (this.cursorX >= cols) this.cursorX = cols - 1; if (this.cursorY >= rows) this.cursorY = rows - 1;
this.onResizeEmitter.fire({ cols, rows }); this.requestRender(); }
clear() { this.buffer = this.createBuffer(this.rows); this.cursorX = 0; this.cursorY = 0; this.requestRender(); }
reset() { this.clear(); }
dispose() { if (this.element && this.container) { this.container.removeChild(this.element); } this.element = null; this.container = null; }
get onData() { return this.onDataEmitter.event.bind(this.onDataEmitter); }
get onResize() { return this.onResizeEmitter.event.bind(this.onResizeEmitter); } }
// Initialize terminal const term = new Terminal({ cols: 80, rows: 24 }); term.open(document.getElementById('terminal')); // Try to load WASM module let wasmModule = null; fetch('./zig/zig-out/bin/ghostty-terminal.wasm') .then(response => response.arrayBuffer()) .then(bytes => WebAssembly.instantiate(bytes, {})) .then(result => { wasmModule = result.instance.exports; document.getElementById('wasm-status').textContent = 'Loaded ✓'; console.log('WASM module loaded:', wasmModule); // Test WASM function const sum = wasmModule.add(5, 3); console.log('WASM test: 5 + 3 =', sum); }) .catch(err => { document.getElementById('wasm-status').textContent = 'Failed (optional)'; console.warn('WASM not loaded (optional):', err); }); // Write welcome message term.write('Welcome to WASM Terminal!\n'); term.write('This is a minimal xterm.js-compatible terminal implementation.\n'); term.write('Type in the input box above and press Enter.\n'); term.write('\n'); term.write('$ '); document.getElementById('status').textContent = 'Ready'; document.getElementById('size').textContent = `${term.cols}x${term.rows}`; // Handle input const input = document.getElementById('input'); input.addEventListener('keypress', (e) => { if (e.key === 'Enter') { const text = input.value; term.write(text + '\n$ '); input.value = ''; } }); // Global functions for buttons window.writeHello = () => { term.write('Hello, World!\n'); term.write('$ '); }; window.clearTerminal = () => { term.clear(); term.write('$ '); }; window.resizeTerminal = () => { term.resize(100, 30); document.getElementById('size').textContent = `${term.cols}x${term.rows}`; }; window.writeColors = () => { term.write('Terminal is ready for color support!\n'); term.write('(ANSI color parsing coming soon)\n'); term.write('$ '); }; // Export for debugging window.term = term; window.wasmModule = () => wasmModule; </script></body></html>