mirror-ghostty/example/wasm-sgr/index.html

458 lines
18 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 SGR Parser - 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;
}
.attribute {
padding: 8px;
margin: 5px 0;
background: white;
border-left: 3px solid #0066cc;
}
.attribute-name {
font-weight: bold;
color: #0066cc;
}
</style>
</head>
<body>
<h1>Ghostty VT SGR Parser - WebAssembly Example</h1>
<p>This example demonstrates parsing terminal SGR (Select Graphic Rendition) sequences using the Ghostty VT WebAssembly module.</p>
<div class="status" id="status">Loading WebAssembly module...</div>
<div class="input-section">
<h3>SGR Sequence</h3>
<label for="sequence">Enter SGR sequence (numbers separated by ':' or ';'):</label>
<textarea id="sequence" rows="2" disabled>4:3;38;2;51;51;51;48;2;170;170;170;58;2;255;97;136</textarea>
<p style="font-size: 13px; color: #666; margin-top: 5px;">The parser runs live as you type.</p>
</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;
}
// SGR attribute tag values from include/ghostty/vt/sgr.h
const SGR_ATTR_TAGS = {
UNSET: 0,
UNKNOWN: 1,
BOLD: 2,
RESET_BOLD: 3,
ITALIC: 4,
RESET_ITALIC: 5,
FAINT: 6,
UNDERLINE: 7,
RESET_UNDERLINE: 8,
UNDERLINE_COLOR: 9,
UNDERLINE_COLOR_256: 10,
RESET_UNDERLINE_COLOR: 11,
OVERLINE: 12,
RESET_OVERLINE: 13,
BLINK: 14,
RESET_BLINK: 15,
INVERSE: 16,
RESET_INVERSE: 17,
INVISIBLE: 18,
RESET_INVISIBLE: 19,
STRIKETHROUGH: 20,
RESET_STRIKETHROUGH: 21,
DIRECT_COLOR_FG: 22,
DIRECT_COLOR_BG: 23,
BG_8: 24,
FG_8: 25,
RESET_FG: 26,
RESET_BG: 27,
BRIGHT_BG_8: 28,
BRIGHT_FG_8: 29,
BG_256: 30,
FG_256: 31
};
// Underline style values
const UNDERLINE_STYLES = {
0: 'none',
1: 'single',
2: 'double',
3: 'curly',
4: 'dotted',
5: 'dashed'
};
function getTagName(tag) {
for (const [name, value] of Object.entries(SGR_ATTR_TAGS)) {
if (value === tag) return name;
}
return `UNKNOWN(${tag})`;
}
function parseSGR() {
const outputDiv = document.getElementById('output');
try {
const sequenceText = document.getElementById('sequence').value.trim();
if (!sequenceText) {
outputDiv.className = 'output';
outputDiv.textContent = 'Enter an SGR sequence to parse...';
return;
}
// Parse the raw sequence into parameters and separators
const params = [];
const separators = [];
let currentNum = '';
for (let i = 0; i < sequenceText.length; i++) {
const char = sequenceText[i];
if (char === ':' || char === ';') {
if (currentNum) {
const num = parseInt(currentNum, 10);
if (isNaN(num) || num < 0 || num > 65535) {
throw new Error(`Invalid parameter: ${currentNum}`);
}
params.push(num);
separators.push(char);
currentNum = '';
}
} else if (char >= '0' && char <= '9') {
currentNum += char;
} else if (char !== ' ' && char !== '\t' && char !== '\n') {
throw new Error(`Invalid character in sequence: '${char}'`);
}
}
// Don't forget the last number
if (currentNum) {
const num = parseInt(currentNum, 10);
if (isNaN(num) || num < 0 || num > 65535) {
throw new Error(`Invalid parameter: ${currentNum}`);
}
params.push(num);
}
if (params.length === 0) {
outputDiv.className = 'output error';
outputDiv.textContent = 'Error: No parameters found in sequence';
return;
}
// Create SGR parser
const parserPtrPtr = wasmInstance.exports.ghostty_wasm_alloc_opaque();
const result = wasmInstance.exports.ghostty_sgr_new(0, parserPtrPtr);
if (result !== 0) {
throw new Error(`ghostty_sgr_new failed with result ${result}`);
}
const parserPtr = new DataView(getBuffer()).getUint32(parserPtrPtr, true);
// Allocate and set parameters
const paramsPtr = wasmInstance.exports.ghostty_wasm_alloc_u16_array(params.length);
const paramsView = new Uint16Array(getBuffer(), paramsPtr, params.length);
params.forEach((p, i) => paramsView[i] = p);
// Allocate and set separators (or use null if empty)
let sepsPtr = 0;
if (separators.length > 0) {
sepsPtr = wasmInstance.exports.ghostty_wasm_alloc_u8_array(separators.length);
const sepsView = new Uint8Array(getBuffer(), sepsPtr, separators.length);
separators.forEach((s, i) => sepsView[i] = s.charCodeAt(0));
}
// Set parameters in parser
const setResult = wasmInstance.exports.ghostty_sgr_set_params(
parserPtr,
paramsPtr,
sepsPtr,
params.length
);
if (setResult !== 0) {
throw new Error(`ghostty_sgr_set_params failed with result ${setResult}`);
}
// Build output
let output = 'Parsing SGR sequence:\n';
output += 'ESC[';
params.forEach((p, i) => {
if (i > 0) output += separators[i - 1];
output += p;
});
output += 'm\n\n';
// Iterate through attributes
const attrPtr = wasmInstance.exports.ghostty_wasm_alloc_sgr_attribute();
let count = 0;
while (wasmInstance.exports.ghostty_sgr_next(parserPtr, attrPtr)) {
count++;
// Use the new ghostty_sgr_attribute_tag getter function
const tag = wasmInstance.exports.ghostty_sgr_attribute_tag(attrPtr);
// Use ghostty_sgr_attribute_value to get a pointer to the value union
const valuePtr = wasmInstance.exports.ghostty_sgr_attribute_value(attrPtr);
output += `Attribute ${count}: `;
switch (tag) {
case SGR_ATTR_TAGS.UNDERLINE: {
const view = new DataView(getBuffer(), valuePtr, 4);
const style = view.getUint32(0, true);
output += `Underline style = ${UNDERLINE_STYLES[style] || `unknown(${style})`}\n`;
break;
}
case SGR_ATTR_TAGS.DIRECT_COLOR_FG: {
// Use ghostty_color_rgb_get to extract RGB components
const rPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
const gPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
const bPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
wasmInstance.exports.ghostty_color_rgb_get(valuePtr, rPtr, gPtr, bPtr);
const r = new Uint8Array(getBuffer(), rPtr, 1)[0];
const g = new Uint8Array(getBuffer(), gPtr, 1)[0];
const b = new Uint8Array(getBuffer(), bPtr, 1)[0];
output += `Foreground RGB = (${r}, ${g}, ${b})\n`;
wasmInstance.exports.ghostty_wasm_free_u8(rPtr);
wasmInstance.exports.ghostty_wasm_free_u8(gPtr);
wasmInstance.exports.ghostty_wasm_free_u8(bPtr);
break;
}
case SGR_ATTR_TAGS.DIRECT_COLOR_BG: {
// Use ghostty_color_rgb_get to extract RGB components
const rPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
const gPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
const bPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
wasmInstance.exports.ghostty_color_rgb_get(valuePtr, rPtr, gPtr, bPtr);
const r = new Uint8Array(getBuffer(), rPtr, 1)[0];
const g = new Uint8Array(getBuffer(), gPtr, 1)[0];
const b = new Uint8Array(getBuffer(), bPtr, 1)[0];
output += `Background RGB = (${r}, ${g}, ${b})\n`;
wasmInstance.exports.ghostty_wasm_free_u8(rPtr);
wasmInstance.exports.ghostty_wasm_free_u8(gPtr);
wasmInstance.exports.ghostty_wasm_free_u8(bPtr);
break;
}
case SGR_ATTR_TAGS.UNDERLINE_COLOR: {
// Use ghostty_color_rgb_get to extract RGB components
const rPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
const gPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
const bPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
wasmInstance.exports.ghostty_color_rgb_get(valuePtr, rPtr, gPtr, bPtr);
const r = new Uint8Array(getBuffer(), rPtr, 1)[0];
const g = new Uint8Array(getBuffer(), gPtr, 1)[0];
const b = new Uint8Array(getBuffer(), bPtr, 1)[0];
output += `Underline color RGB = (${r}, ${g}, ${b})\n`;
wasmInstance.exports.ghostty_wasm_free_u8(rPtr);
wasmInstance.exports.ghostty_wasm_free_u8(gPtr);
wasmInstance.exports.ghostty_wasm_free_u8(bPtr);
break;
}
case SGR_ATTR_TAGS.FG_8:
case SGR_ATTR_TAGS.BG_8:
case SGR_ATTR_TAGS.FG_256:
case SGR_ATTR_TAGS.BG_256:
case SGR_ATTR_TAGS.UNDERLINE_COLOR_256: {
const view = new DataView(getBuffer(), valuePtr, 1);
const color = view.getUint8(0);
const colorType = tag === SGR_ATTR_TAGS.FG_8 ? 'Foreground 8-color' :
tag === SGR_ATTR_TAGS.BG_8 ? 'Background 8-color' :
tag === SGR_ATTR_TAGS.FG_256 ? 'Foreground 256-color' :
tag === SGR_ATTR_TAGS.BG_256 ? 'Background 256-color' :
'Underline 256-color';
output += `${colorType} = ${color}\n`;
break;
}
case SGR_ATTR_TAGS.BOLD:
output += 'Bold\n';
break;
case SGR_ATTR_TAGS.ITALIC:
output += 'Italic\n';
break;
case SGR_ATTR_TAGS.UNSET:
output += 'Reset all attributes\n';
break;
case SGR_ATTR_TAGS.UNKNOWN:
output += 'Unknown attribute\n';
break;
default:
output += `Other attribute (tag=${getTagName(tag)})\n`;
break;
}
}
output += `\nTotal attributes parsed: ${count}`;
outputDiv.className = 'output';
outputDiv.textContent = output;
// Cleanup
wasmInstance.exports.ghostty_wasm_free_sgr_attribute(attrPtr);
wasmInstance.exports.ghostty_sgr_free(parserPtr);
} catch (e) {
console.error('Parse 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 sequenceInput = document.getElementById('sequence');
try {
statusDiv.textContent = 'Loading WebAssembly module...';
const loaded = await loadWasm();
if (!loaded) {
throw new Error('Failed to load WebAssembly module');
}
statusDiv.textContent = '';
sequenceInput.disabled = false;
// Parse live as user types
sequenceInput.addEventListener('input', parseSGR);
// Parse the default example on load
parseSGR();
} catch (e) {
statusDiv.textContent = `Error: ${e.message}`;
statusDiv.style.color = '#c00';
}
}
window.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>