mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-06 07:38:21 +00:00
607 lines
26 KiB
HTML
607 lines
26 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;
|
|
}
|
|
.effects-log {
|
|
background: #1a1a2e;
|
|
color: #e0e0e0;
|
|
border: 1px solid #444;
|
|
border-radius: 4px;
|
|
padding: 15px;
|
|
margin: 20px 0;
|
|
font-family: 'Courier New', monospace;
|
|
white-space: pre-wrap;
|
|
font-size: 13px;
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
}
|
|
.effects-log .effect-label {
|
|
color: #00d9ff;
|
|
font-weight: bold;
|
|
}
|
|
.effects-log .effect-data {
|
|
color: #a0ffa0;
|
|
}
|
|
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\n\x07\x1b]2;Hello Effects\x1b\\\x1b[?7$p</textarea>
|
|
<p style="font-size: 13px; color: #666; margin-top: 5px;">Use \x1b for ESC, \r\n for CR+LF, \x07 for BEL. Press "Run" to process.</p>
|
|
<button id="runBtn" disabled>Run</button>
|
|
</div>
|
|
|
|
<h3>Effects Log</h3>
|
|
<div id="effectsLog" class="effects-log">No effects triggered yet.</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;
|
|
let typeLayout = null;
|
|
let effectsLog = [];
|
|
|
|
function logEffect(label, data) {
|
|
effectsLog.push({ label, data });
|
|
}
|
|
|
|
// Patch a WASM binary to: (1) make the function table growable by
|
|
// removing its max limit, and (2) export it as "tbl" so JS can add
|
|
// effect callback entries. Zig's WASM linker sets a fixed max on
|
|
// the table and does not export it by default.
|
|
function patchWasmForEffects(buffer) {
|
|
let bytes = new Uint8Array(buffer);
|
|
|
|
function readLEB128(arr, offset) {
|
|
let result = 0, shift = 0, bytesRead = 0;
|
|
while (true) {
|
|
const byte = arr[offset + bytesRead];
|
|
result |= (byte & 0x7f) << shift;
|
|
bytesRead++;
|
|
if ((byte & 0x80) === 0) break;
|
|
shift += 7;
|
|
}
|
|
return { value: result, bytesRead };
|
|
}
|
|
|
|
function encodeLEB128(value) {
|
|
const out = [];
|
|
do {
|
|
let b = value & 0x7f;
|
|
value >>>= 7;
|
|
if (value !== 0) b |= 0x80;
|
|
out.push(b);
|
|
} while (value !== 0);
|
|
return out;
|
|
}
|
|
|
|
// Rebuild a section from parts: [before_section | id | new_size | new_payload | after_section]
|
|
function rebuildSection(buf, sectionPos, sectionStart, oldSectionSize, newPayload) {
|
|
const newSize = encodeLEB128(newPayload.length);
|
|
const afterStart = sectionStart + oldSectionSize;
|
|
const result = new Uint8Array(sectionPos + 1 + newSize.length + newPayload.length + (buf.length - afterStart));
|
|
result.set(buf.subarray(0, sectionPos));
|
|
let w = sectionPos;
|
|
result[w++] = buf[sectionPos]; // section id
|
|
for (const b of newSize) result[w++] = b;
|
|
for (const b of newPayload) result[w++] = b;
|
|
result.set(buf.subarray(afterStart), w);
|
|
return result;
|
|
}
|
|
|
|
// Pass 1: Patch table section (id=4) to remove max limit so the table is growable.
|
|
let pos = 8;
|
|
while (pos < bytes.length) {
|
|
const sectionId = bytes[pos];
|
|
const { value: sectionSize, bytesRead: sizeLen } = readLEB128(bytes, pos + 1);
|
|
const sectionStart = pos + 1 + sizeLen;
|
|
if (sectionId === 4) {
|
|
const { value: tableCount, bytesRead: countLen } = readLEB128(bytes, sectionStart);
|
|
let tpos = sectionStart + countLen;
|
|
const elemType = bytes[tpos++]; // 0x70 = funcref
|
|
const flags = bytes[tpos];
|
|
if (flags & 1) {
|
|
// Has max — rebuild without it: flag=0, keep only min
|
|
const { value: min, bytesRead: minLen } = readLEB128(bytes, tpos + 1);
|
|
const { value: max, bytesRead: maxLen } = readLEB128(bytes, tpos + 1 + minLen);
|
|
const afterLimits = tpos + 1 + minLen + maxLen;
|
|
const newPayload = [
|
|
...encodeLEB128(tableCount),
|
|
elemType,
|
|
0x00, // flags: no max
|
|
...encodeLEB128(min),
|
|
...bytes.slice(afterLimits, sectionStart + sectionSize),
|
|
];
|
|
bytes = rebuildSection(bytes, pos, sectionStart, sectionSize, new Uint8Array(newPayload));
|
|
}
|
|
break;
|
|
}
|
|
pos = sectionStart + sectionSize;
|
|
}
|
|
|
|
// Pass 2: Add table export to export section (id=7) if not already present.
|
|
const mod = new WebAssembly.Module(bytes.buffer);
|
|
if (WebAssembly.Module.exports(mod).some(e => e.kind === 'table')) {
|
|
return bytes.buffer;
|
|
}
|
|
const exportEntry = [0x03, 0x74, 0x62, 0x6c, 0x01, 0x00]; // name="tbl", kind=table, index=0
|
|
pos = 8;
|
|
while (pos < bytes.length) {
|
|
const sectionId = bytes[pos];
|
|
const { value: sectionSize, bytesRead: sizeLen } = readLEB128(bytes, pos + 1);
|
|
const sectionStart = pos + 1 + sizeLen;
|
|
if (sectionId === 7) {
|
|
const { value: count, bytesRead: countLen } = readLEB128(bytes, sectionStart);
|
|
const restStart = sectionStart + countLen;
|
|
const restLen = sectionSize - countLen;
|
|
const newPayload = new Uint8Array([
|
|
...encodeLEB128(count + 1),
|
|
...bytes.slice(restStart, restStart + restLen),
|
|
...exportEntry,
|
|
]);
|
|
bytes = rebuildSection(bytes, pos, sectionStart, sectionSize, newPayload);
|
|
break;
|
|
}
|
|
pos = sectionStart + sectionSize;
|
|
}
|
|
|
|
return bytes.buffer;
|
|
}
|
|
|
|
async function loadWasm() {
|
|
try {
|
|
const response = await fetch('../../zig-out/bin/ghostty-vt.wasm');
|
|
const wasmBytes = patchWasmForEffects(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;
|
|
|
|
// Load the type layout JSON from the library
|
|
const jsonPtr = wasmInstance.exports.ghostty_type_json();
|
|
const jsonStr = new TextDecoder().decode(
|
|
new Uint8Array(wasmMemory.buffer, jsonPtr, wasmMemory.buffer.byteLength - jsonPtr)
|
|
).split('\0')[0];
|
|
typeLayout = JSON.parse(jsonStr);
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
// Look up a field's offset and DataView setter from the type layout JSON.
|
|
function fieldInfo(structName, fieldName) {
|
|
const field = typeLayout[structName].fields[fieldName];
|
|
return field;
|
|
}
|
|
|
|
// Set a field in a DataView using the type layout JSON metadata.
|
|
function setField(view, structName, fieldName, value) {
|
|
const field = fieldInfo(structName, fieldName);
|
|
switch (field.type) {
|
|
case 'u8': case 'bool': view.setUint8(field.offset, value); break;
|
|
case 'u16': view.setUint16(field.offset, value, true); break;
|
|
case 'u32': case 'enum': view.setUint32(field.offset, value, true); break;
|
|
case 'u64': view.setBigUint64(field.offset, BigInt(value), true); break;
|
|
default: throw new Error(`Unsupported field type: ${field.type}`);
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
// GhosttyTerminalOption enum values
|
|
const GHOSTTY_TERMINAL_OPT_WRITE_PTY = 1;
|
|
const GHOSTTY_TERMINAL_OPT_BELL = 2;
|
|
const GHOSTTY_TERMINAL_OPT_TITLE_CHANGED = 5;
|
|
|
|
// Allocate slots in the WASM indirect function table for JS callbacks.
|
|
// Returns the table index (i.e. the function pointer value in WASM).
|
|
let effectTableIndices = [];
|
|
function addToWasmTable(func) {
|
|
const table = wasmInstance.exports.tbl || wasmInstance.exports.__indirect_function_table;
|
|
const idx = table.length;
|
|
table.grow(1);
|
|
table.set(idx, func);
|
|
effectTableIndices.push(idx);
|
|
return idx;
|
|
}
|
|
|
|
// Build a tiny WASM trampoline module that imports JS callbacks and
|
|
// re-exports them as properly typed WASM functions. This is needed
|
|
// because adding JS functions to a WASM table requires them to be
|
|
// wrapped as WebAssembly function objects with the correct signature.
|
|
// WebAssembly.Function is not supported in all browsers (e.g. Safari),
|
|
// so we compile a minimal module instead.
|
|
let effectWrappers = null;
|
|
async function buildEffectWrappers() {
|
|
if (effectWrappers) return effectWrappers;
|
|
|
|
// Hand-coded WASM module with:
|
|
// Type 0: (i32, i32, i32, i32) -> void [write_pty]
|
|
// Type 1: (i32, i32) -> void [bell, title_changed]
|
|
// Imports: env.a (type 0), env.b (type 1), env.c (type 1)
|
|
// Functions 3,4,5 wrap imports 0,1,2 respectively
|
|
// Exports: "a" -> func 3, "b" -> func 4, "c" -> func 5
|
|
const bytes = new Uint8Array([
|
|
0x00, 0x61, 0x73, 0x6d, // magic
|
|
0x01, 0x00, 0x00, 0x00, // version
|
|
|
|
// Type section (id=1)
|
|
0x01, 0x0d, // section id, size=13
|
|
0x02, // 2 types
|
|
// type 0: (i32, i32, i32, i32) -> ()
|
|
0x60, 0x04, 0x7f, 0x7f, 0x7f, 0x7f, 0x00,
|
|
// type 1: (i32, i32) -> ()
|
|
0x60, 0x02, 0x7f, 0x7f, 0x00,
|
|
|
|
// Import section (id=2)
|
|
0x02, 0x19, // section id, size=25
|
|
0x03, // 3 imports
|
|
// import 0: env.a type 0
|
|
0x03, 0x65, 0x6e, 0x76, // "env"
|
|
0x01, 0x61, // "a"
|
|
0x00, 0x00, // func, type 0
|
|
// import 1: env.b type 1
|
|
0x03, 0x65, 0x6e, 0x76, // "env"
|
|
0x01, 0x62, // "b"
|
|
0x00, 0x01, // func, type 1
|
|
// import 2: env.c type 1
|
|
0x03, 0x65, 0x6e, 0x76, // "env"
|
|
0x01, 0x63, // "c"
|
|
0x00, 0x01, // func, type 1
|
|
|
|
// Function section (id=3)
|
|
0x03, 0x04, // section id, size=4
|
|
0x03, // 3 functions
|
|
0x00, 0x01, 0x01, // types: 0, 1, 1
|
|
|
|
// Export section (id=7)
|
|
0x07, 0x0d, // section id, size=13
|
|
0x03, // 3 exports
|
|
0x01, 0x61, 0x00, 0x03, // "a" -> func 3
|
|
0x01, 0x62, 0x00, 0x04, // "b" -> func 4
|
|
0x01, 0x63, 0x00, 0x05, // "c" -> func 5
|
|
|
|
// Code section (id=10)
|
|
0x0a, 0x20, // section id, size=32
|
|
0x03, // 3 function bodies
|
|
// func 3: call import 0 with 4 args
|
|
0x0c, 0x00, // body size=12, 0 locals
|
|
0x20, 0x00, 0x20, 0x01, 0x20, 0x02, 0x20, 0x03, 0x10, 0x00, 0x0b,
|
|
// func 4: call import 1 with 2 args
|
|
0x08, 0x00, // body size=8, 0 locals
|
|
0x20, 0x00, 0x20, 0x01, 0x10, 0x01, 0x0b,
|
|
// func 5: call import 2 with 2 args
|
|
0x08, 0x00, // body size=8, 0 locals
|
|
0x20, 0x00, 0x20, 0x01, 0x10, 0x02, 0x0b,
|
|
]);
|
|
|
|
const mod = await WebAssembly.instantiate(bytes, {
|
|
env: {
|
|
a: (terminal, userdata, dataPtr, len) => {
|
|
const b = new Uint8Array(getBuffer(), dataPtr, len);
|
|
const text = new TextDecoder().decode(b.slice());
|
|
const hex = Array.from(b).map(v => v.toString(16).padStart(2, '0')).join(' ');
|
|
logEffect('write_pty', `${len} bytes: ${hex} "${text}"`);
|
|
},
|
|
b: (terminal, userdata) => {
|
|
logEffect('bell', 'BEL received!');
|
|
},
|
|
c: (terminal, userdata) => {
|
|
logEffect('title_changed', 'Terminal title changed');
|
|
},
|
|
},
|
|
});
|
|
|
|
effectWrappers = {
|
|
writePtyWrapper: mod.instance.exports.a,
|
|
bellWrapper: mod.instance.exports.b,
|
|
titleChangedWrapper: mod.instance.exports.c,
|
|
};
|
|
return effectWrappers;
|
|
}
|
|
|
|
async 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);
|
|
|
|
const TERM_OPTS_SIZE = typeLayout['GhosttyTerminalOptions'].size;
|
|
const optsPtr = wasmInstance.exports.ghostty_wasm_alloc_u8_array(TERM_OPTS_SIZE);
|
|
new Uint8Array(getBuffer(), optsPtr, TERM_OPTS_SIZE).fill(0);
|
|
const optsView = new DataView(getBuffer(), optsPtr, TERM_OPTS_SIZE);
|
|
setField(optsView, 'GhosttyTerminalOptions', 'cols', cols);
|
|
setField(optsView, 'GhosttyTerminalOptions', 'rows', rows);
|
|
setField(optsView, 'GhosttyTerminalOptions', '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);
|
|
|
|
// Register effect callbacks
|
|
effectsLog = [];
|
|
const wrappers = await buildEffectWrappers();
|
|
const writePtyIdx = addToWasmTable(wrappers.writePtyWrapper);
|
|
const bellIdx = addToWasmTable(wrappers.bellWrapper);
|
|
const titleIdx = addToWasmTable(wrappers.titleChangedWrapper);
|
|
|
|
wasmInstance.exports.ghostty_terminal_set(termPtr, GHOSTTY_TERMINAL_OPT_WRITE_PTY, writePtyIdx);
|
|
wasmInstance.exports.ghostty_terminal_set(termPtr, GHOSTTY_TERMINAL_OPT_BELL, bellIdx);
|
|
wasmInstance.exports.ghostty_terminal_set(termPtr, GHOSTTY_TERMINAL_OPT_TITLE_CHANGED, titleIdx);
|
|
|
|
// 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
|
|
const FMT_OPTS_SIZE = typeLayout['GhosttyFormatterTerminalOptions'].size;
|
|
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);
|
|
setField(fmtOptsView, 'GhosttyFormatterTerminalOptions', 'size', FMT_OPTS_SIZE);
|
|
setField(fmtOptsView, 'GhosttyFormatterTerminalOptions', 'emit', GHOSTTY_FORMATTER_FORMAT_PLAIN);
|
|
setField(fmtOptsView, 'GhosttyFormatterTerminalOptions', 'unwrap', 0);
|
|
setField(fmtOptsView, 'GhosttyFormatterTerminalOptions', 'trim', 1);
|
|
|
|
// Set the nested sized-struct `size` fields for extra and extra.screen
|
|
const extraOffset = fieldInfo('GhosttyFormatterTerminalOptions', 'extra').offset;
|
|
const extraSize = typeLayout['GhosttyFormatterTerminalExtra'].size;
|
|
const extraSizeField = fieldInfo('GhosttyFormatterTerminalExtra', 'size');
|
|
fmtOptsView.setUint32(extraOffset + extraSizeField.offset, extraSize, true);
|
|
|
|
const screenOffset = fieldInfo('GhosttyFormatterTerminalExtra', 'screen').offset;
|
|
const screenSize = typeLayout['GhosttyFormatterScreenExtra'].size;
|
|
const screenSizeField = fieldInfo('GhosttyFormatterScreenExtra', 'size');
|
|
fmtOptsView.setUint32(extraOffset + screenOffset + screenSizeField.offset, screenSize, 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;
|
|
|
|
// Render effects log
|
|
const effectsDiv = document.getElementById('effectsLog');
|
|
if (effectsLog.length === 0) {
|
|
effectsDiv.textContent = 'No effects triggered.';
|
|
} else {
|
|
effectsDiv.innerHTML = effectsLog.map(e =>
|
|
`<span class="effect-label">[${e.label}]</span> <span class="effect-data">${e.data}</span>`
|
|
).join('\n');
|
|
}
|
|
|
|
// 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
|
|
await run();
|
|
return;
|
|
} catch (e) {
|
|
statusDiv.textContent = `Error: ${e.message}`;
|
|
statusDiv.style.color = '#c00';
|
|
}
|
|
}
|
|
|
|
window.addEventListener('DOMContentLoaded', init);
|
|
</script>
|
|
</body>
|
|
</html>
|