mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-18 05:20:29 +00:00
319 lines
13 KiB
HTML
319 lines
13 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Ghostty VT Terminal - WebAssembly Example</title>
|
|
<style>
|
|
body {
|
|
font-family: system-ui, -apple-system, sans-serif;
|
|
max-width: 900px;
|
|
margin: 40px auto;
|
|
padding: 0 20px;
|
|
line-height: 1.6;
|
|
}
|
|
h1 {
|
|
color: #333;
|
|
}
|
|
.input-section {
|
|
background: #f9f9f9;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
padding: 15px;
|
|
margin: 20px 0;
|
|
}
|
|
.input-section h3 {
|
|
margin-top: 0;
|
|
margin-bottom: 10px;
|
|
font-size: 16px;
|
|
}
|
|
textarea {
|
|
width: 100%;
|
|
padding: 10px;
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 14px;
|
|
border: 1px solid #ccc;
|
|
border-radius: 4px;
|
|
box-sizing: border-box;
|
|
resize: vertical;
|
|
}
|
|
button {
|
|
background: #0066cc;
|
|
color: white;
|
|
border: none;
|
|
padding: 10px 20px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
margin-top: 10px;
|
|
}
|
|
button:hover {
|
|
background: #0052a3;
|
|
}
|
|
button:disabled {
|
|
background: #ccc;
|
|
cursor: not-allowed;
|
|
}
|
|
.output {
|
|
background: #f5f5f5;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
padding: 15px;
|
|
margin: 20px 0;
|
|
font-family: 'Courier New', monospace;
|
|
white-space: pre-wrap;
|
|
font-size: 14px;
|
|
}
|
|
.error {
|
|
background: #fee;
|
|
border-color: #faa;
|
|
color: #c00;
|
|
}
|
|
.status {
|
|
color: #666;
|
|
font-size: 14px;
|
|
margin: 10px 0;
|
|
}
|
|
.size-controls {
|
|
display: flex;
|
|
gap: 15px;
|
|
margin-bottom: 10px;
|
|
}
|
|
.size-controls label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
}
|
|
.size-controls input[type="number"] {
|
|
width: 60px;
|
|
padding: 4px;
|
|
border: 1px solid #ccc;
|
|
border-radius: 4px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Ghostty VT Terminal - WebAssembly Example</h1>
|
|
<p>This example demonstrates initializing a terminal, writing VT-encoded data to it, and formatting the output using the Ghostty VT WebAssembly module.</p>
|
|
|
|
<div class="status" id="status">Loading WebAssembly module...</div>
|
|
|
|
<div class="input-section">
|
|
<h3>Terminal Size</h3>
|
|
<div class="size-controls">
|
|
<label>Cols: <input type="number" id="cols" value="80" min="1" max="500" disabled></label>
|
|
<label>Rows: <input type="number" id="rows" value="24" min="1" max="500" disabled></label>
|
|
</div>
|
|
<h3>VT Input</h3>
|
|
<textarea id="vtInput" rows="6" disabled>Hello, World!\r\n\x1b[1;32mGreen Bold\x1b[0m and \x1b[4mUnderline\x1b[0m\r\nLine 3: placeholder\r\n\x1b[3;1H\x1b[2KLine 3: Overwritten!</textarea>
|
|
<p style="font-size: 13px; color: #666; margin-top: 5px;">Use \x1b for ESC, \r\n for CR+LF. Press "Run" to process.</p>
|
|
<button id="runBtn" disabled>Run</button>
|
|
</div>
|
|
|
|
<div id="output" class="output">Waiting for input...</div>
|
|
|
|
<p><strong>Note:</strong> This example must be served via HTTP (not opened directly as a file). See the README for instructions.</p>
|
|
|
|
<script>
|
|
let wasmInstance = null;
|
|
let wasmMemory = null;
|
|
|
|
async function loadWasm() {
|
|
try {
|
|
const response = await fetch('../../zig-out/bin/ghostty-vt.wasm');
|
|
const wasmBytes = await response.arrayBuffer();
|
|
|
|
const wasmModule = await WebAssembly.instantiate(wasmBytes, {
|
|
env: {
|
|
log: (ptr, len) => {
|
|
const bytes = new Uint8Array(wasmModule.instance.exports.memory.buffer, ptr, len);
|
|
const text = new TextDecoder().decode(bytes);
|
|
console.log('[wasm]', text);
|
|
}
|
|
}
|
|
});
|
|
|
|
wasmInstance = wasmModule.instance;
|
|
wasmMemory = wasmInstance.exports.memory;
|
|
|
|
return true;
|
|
} catch (e) {
|
|
console.error('Failed to load WASM:', e);
|
|
if (window.location.protocol === 'file:') {
|
|
throw new Error('Cannot load WASM from file:// protocol. Please serve via HTTP (see README)');
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function getBuffer() {
|
|
return wasmMemory.buffer;
|
|
}
|
|
|
|
// Parse escape sequences in the input string (e.g. \x1b, \r, \n)
|
|
function parseEscapes(str) {
|
|
return str
|
|
.replace(/\\x([0-9a-fA-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
|
|
.replace(/\\r/g, '\r')
|
|
.replace(/\\n/g, '\n')
|
|
.replace(/\\t/g, '\t')
|
|
.replace(/\\\\/g, '\\');
|
|
}
|
|
|
|
// GHOSTTY_FORMATTER_FORMAT_PLAIN = 0
|
|
const GHOSTTY_FORMATTER_FORMAT_PLAIN = 0;
|
|
// GHOSTTY_SUCCESS = 0
|
|
const GHOSTTY_SUCCESS = 0;
|
|
|
|
function run() {
|
|
const outputDiv = document.getElementById('output');
|
|
|
|
try {
|
|
const cols = parseInt(document.getElementById('cols').value, 10);
|
|
const rows = parseInt(document.getElementById('rows').value, 10);
|
|
const vtText = parseEscapes(document.getElementById('vtInput').value);
|
|
|
|
// GhosttyTerminalOptions: { u16 cols, u16 rows, u32(wasm) max_scrollback }
|
|
// On wasm32, size_t is u32. Struct layout: cols(u16) + rows(u16) + max_scrollback(u32) = 8 bytes.
|
|
const TERM_OPTS_SIZE = 8;
|
|
const optsPtr = wasmInstance.exports.ghostty_wasm_alloc_u8_array(TERM_OPTS_SIZE);
|
|
const optsView = new DataView(getBuffer(), optsPtr, TERM_OPTS_SIZE);
|
|
optsView.setUint16(0, cols, true);
|
|
optsView.setUint16(2, rows, true);
|
|
optsView.setUint32(4, 0, true); // max_scrollback = 0
|
|
|
|
// Allocate pointer to receive the terminal handle
|
|
const termPtrPtr = wasmInstance.exports.ghostty_wasm_alloc_opaque();
|
|
|
|
// Create terminal
|
|
const newResult = wasmInstance.exports.ghostty_terminal_new(0, termPtrPtr, optsPtr);
|
|
wasmInstance.exports.ghostty_wasm_free_u8_array(optsPtr, TERM_OPTS_SIZE);
|
|
|
|
if (newResult !== GHOSTTY_SUCCESS) {
|
|
throw new Error(`ghostty_terminal_new failed with result ${newResult}`);
|
|
}
|
|
|
|
const termPtr = new DataView(getBuffer()).getUint32(termPtrPtr, true);
|
|
wasmInstance.exports.ghostty_wasm_free_opaque(termPtrPtr);
|
|
|
|
// Write VT data to the terminal
|
|
const vtBytes = new TextEncoder().encode(vtText);
|
|
const dataPtr = wasmInstance.exports.ghostty_wasm_alloc_u8_array(vtBytes.length);
|
|
new Uint8Array(getBuffer()).set(vtBytes, dataPtr);
|
|
wasmInstance.exports.ghostty_terminal_vt_write(termPtr, dataPtr, vtBytes.length);
|
|
wasmInstance.exports.ghostty_wasm_free_u8_array(dataPtr, vtBytes.length);
|
|
|
|
// Create a plain-text formatter
|
|
// GhosttyFormatterTerminalOptions layout on wasm32 (extern struct):
|
|
// size: u32 @ 0 (= sizeof struct = 36)
|
|
// emit: u32 @ 4 (GhosttyFormatterFormat enum)
|
|
// unwrap: u8 @ 8
|
|
// trim: u8 @ 9
|
|
// pad: 2 bytes
|
|
// extra: GhosttyFormatterTerminalExtra @ 12 (24 bytes, all zeros)
|
|
// Total: 36 bytes
|
|
const FMT_OPTS_SIZE = 36;
|
|
const fmtOptsPtr = wasmInstance.exports.ghostty_wasm_alloc_u8_array(FMT_OPTS_SIZE);
|
|
new Uint8Array(getBuffer(), fmtOptsPtr, FMT_OPTS_SIZE).fill(0);
|
|
const fmtOptsView = new DataView(getBuffer(), fmtOptsPtr, FMT_OPTS_SIZE);
|
|
fmtOptsView.setUint32(0, FMT_OPTS_SIZE, true); // size
|
|
fmtOptsView.setUint32(4, GHOSTTY_FORMATTER_FORMAT_PLAIN, true); // emit
|
|
fmtOptsView.setUint8(8, 0); // unwrap = false
|
|
fmtOptsView.setUint8(9, 1); // trim = true
|
|
// extra.size @ offset 12 (GhosttyFormatterTerminalExtra = 24 bytes)
|
|
fmtOptsView.setUint32(12, 24, true);
|
|
// extra.screen.size @ offset 24 (GhosttyFormatterScreenExtra = 12 bytes)
|
|
fmtOptsView.setUint32(24, 12, true);
|
|
|
|
const fmtPtrPtr = wasmInstance.exports.ghostty_wasm_alloc_opaque();
|
|
const fmtResult = wasmInstance.exports.ghostty_formatter_terminal_new(
|
|
0, fmtPtrPtr, termPtr, fmtOptsPtr
|
|
);
|
|
wasmInstance.exports.ghostty_wasm_free_u8_array(fmtOptsPtr, FMT_OPTS_SIZE);
|
|
|
|
if (fmtResult !== GHOSTTY_SUCCESS) {
|
|
wasmInstance.exports.ghostty_terminal_free(termPtr);
|
|
throw new Error(`ghostty_formatter_terminal_new failed with result ${fmtResult}`);
|
|
}
|
|
|
|
const fmtPtr = new DataView(getBuffer()).getUint32(fmtPtrPtr, true);
|
|
wasmInstance.exports.ghostty_wasm_free_opaque(fmtPtrPtr);
|
|
|
|
// Format with alloc
|
|
const outPtrPtr = wasmInstance.exports.ghostty_wasm_alloc_opaque();
|
|
const outLenPtr = wasmInstance.exports.ghostty_wasm_alloc_usize();
|
|
const formatResult = wasmInstance.exports.ghostty_formatter_format_alloc(
|
|
fmtPtr, 0, outPtrPtr, outLenPtr
|
|
);
|
|
|
|
if (formatResult !== GHOSTTY_SUCCESS) {
|
|
wasmInstance.exports.ghostty_formatter_free(fmtPtr);
|
|
wasmInstance.exports.ghostty_terminal_free(termPtr);
|
|
throw new Error(`ghostty_formatter_format_alloc failed with result ${formatResult}`);
|
|
}
|
|
|
|
const outPtr = new DataView(getBuffer()).getUint32(outPtrPtr, true);
|
|
const outLen = new DataView(getBuffer()).getUint32(outLenPtr, true);
|
|
|
|
const outBytes = new Uint8Array(getBuffer(), outPtr, outLen);
|
|
const outText = new TextDecoder().decode(outBytes);
|
|
|
|
let output = `Terminal: ${cols}x${rows}\n`;
|
|
output += `Input: ${vtBytes.length} bytes\n`;
|
|
output += `Output: ${outLen} bytes\n\n`;
|
|
output += outText;
|
|
|
|
outputDiv.className = 'output';
|
|
outputDiv.textContent = output;
|
|
|
|
// Clean up
|
|
wasmInstance.exports.ghostty_free(0, outPtr, outLen);
|
|
wasmInstance.exports.ghostty_wasm_free_opaque(outPtrPtr);
|
|
wasmInstance.exports.ghostty_wasm_free_usize(outLenPtr);
|
|
wasmInstance.exports.ghostty_formatter_free(fmtPtr);
|
|
wasmInstance.exports.ghostty_terminal_free(termPtr);
|
|
|
|
} catch (e) {
|
|
console.error('Error:', e);
|
|
outputDiv.className = 'output error';
|
|
outputDiv.textContent = `Error: ${e.message}\n\nStack trace:\n${e.stack}`;
|
|
}
|
|
}
|
|
|
|
async function init() {
|
|
const statusDiv = document.getElementById('status');
|
|
const vtInput = document.getElementById('vtInput');
|
|
const colsInput = document.getElementById('cols');
|
|
const rowsInput = document.getElementById('rows');
|
|
const runBtn = document.getElementById('runBtn');
|
|
|
|
try {
|
|
statusDiv.textContent = 'Loading WebAssembly module...';
|
|
|
|
const loaded = await loadWasm();
|
|
if (!loaded) {
|
|
throw new Error('Failed to load WebAssembly module');
|
|
}
|
|
|
|
statusDiv.textContent = '';
|
|
vtInput.disabled = false;
|
|
colsInput.disabled = false;
|
|
rowsInput.disabled = false;
|
|
runBtn.disabled = false;
|
|
|
|
runBtn.addEventListener('click', run);
|
|
|
|
// Run the default example on load
|
|
run();
|
|
return;
|
|
} catch (e) {
|
|
statusDiv.textContent = `Error: ${e.message}`;
|
|
statusDiv.style.color = '#c00';
|
|
}
|
|
}
|
|
|
|
window.addEventListener('DOMContentLoaded', init);
|
|
</script>
|
|
</body>
|
|
</html>
|