Something went wrong. Try again.
Ghostty for the web with xterm.js API compatibility
Something went wrong. Try again.
17 kB · 533 lines
HTML
at task6
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>VT100 Parser Demo - Task 3</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; padding: 20px; }
.container { max-width: 1200px; margin: 0 auto; }
header { text-align: center; color: white; margin-bottom: 30px; }
h1 { font-size: 2.5rem; margin-bottom: 10px; }
.subtitle { opacity: 0.9; font-size: 1.1rem; }
.demo-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px; }
@media (max-width: 768px) { .demo-grid { grid-template-columns: 1fr; } }
.card { background: white; border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.1); padding: 25px; }
.card h2 { color: #667eea; margin-bottom: 15px; font-size: 1.3rem; display: flex; align-items: center; gap: 10px; }
.terminal-output { background: #1e1e1e; color: #d4d4d4; padding: 20px; border-radius: 8px; font-family: 'Courier New', Courier, monospace; font-size: 14px; line-height: 1.6; min-height: 200px; max-height: 400px; overflow-y: auto; white-space: pre-wrap; word-wrap: break-word; }
.control-panel { display: flex; flex-direction: column; gap: 10px; }
button { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; padding: 12px 20px; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; }
button:hover { transform: translateY(-2px); box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4); }
button:active { transform: translateY(0); }
.input-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: 600; color: #333; }
input[type="text"], textarea { width: 100%; padding: 10px; border: 2px solid #e0e0e0; border-radius: 6px; font-family: 'Courier New', Courier, monospace; font-size: 13px; }
textarea { resize: vertical; min-height: 80px; }
input[type="text"]:focus, textarea:focus { outline: none; border-color: #667eea; }
.info-box { background: #f0f4ff; border-left: 4px solid #667eea; padding: 15px; border-radius: 6px; margin-bottom: 15px; font-size: 13px; line-height: 1.6; }
.status { padding: 10px 15px; border-radius: 6px; margin-bottom: 15px; font-weight: 600; }
.status.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
.status.error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
.buffer-stats { font-size: 12px; color: #666; margin-top: 10px; padding: 10px; background: #f8f8f8; border-radius: 6px; }
.buffer-stats div { margin-bottom: 5px; }
.color-sample { display: inline-block; width: 20px; height: 20px; border-radius: 4px; vertical-align: middle; margin-left: 5px; border: 1px solid #ddd; }
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-family: 'Courier New', Courier, monospace; font-size: 12px; } </style></head><body> <div class="container"> <header> <h1>🎨 VT100 Parser Demo</h1> <div class="subtitle">Task 3: State Machine Parser - Interactive Testing</div> </header>
<div id="status" class="status success" style="display: none;"> ✅ Parser loaded successfully </div>
<div class="demo-grid"> <!-- Left Column: Quick Tests --> <div class="card"> <h2>🚀 Quick Tests</h2> <div class="info-box"> Click buttons to test different VT100 features </div> <div class="control-panel"> <button onclick="testColors()">🎨 Test ANSI Colors</button> <button onclick="testBoldItalic()">✨ Test Bold/Italic</button> <button onclick="testTabStops()">→ Test Tab Stops (4 col)</button> <button onclick="testCursorMovement()">↔️ Test Cursor Movement</button> <button onclick="testRGBColors()">🌈 Test RGB Colors</button> <button onclick="testClearScreen()">🧹 Test Clear Screen</button> <button onclick="testScrollRegion()">📜 Test Scroll Region</button> <button onclick="testBellEvent()">🔔 Test Bell Event</button> <button onclick="testComplexOutput()">💎 Test Complex Output</button> <button onclick="clearOutput()" style="background: #dc3545;">🗑️ Clear Output</button> </div> </div>
<!-- Right Column: Custom Input --> <div class="card"> <h2>⌨️ Custom Input</h2> <div class="info-box"> Enter VT100 escape sequences. Use <code>\x1b</code> for ESC, <code>\t</code> for tab, <code>\n</code> for newline. </div>
<div class="input-group"> <label>Custom Sequence:</label> <textarea id="customInput" placeholder="Example: \x1b[31mRed Text\x1b[0m">Hello \x1b[1;34mWorld\x1b[0m!</textarea> </div>
<button onclick="testCustom()">▶️ Run Custom Sequence</button>
<div class="input-group" style="margin-top: 20px;"> <label>Quick Examples:</label> <button onclick="loadExample('colors')">Load Color Example</button> <button onclick="loadExample('vim')">Load Vim Example</button> <button onclick="loadExample('ls')">Load ls --color Example</button> </div> </div> </div>
<!-- Terminal Output --> <div class="card"> <h2>📺 Terminal Output</h2> <div id="terminal" class="terminal-output"></div> <div class="buffer-stats"> <div><strong>Cursor Position:</strong> <span id="cursorPos">-</span></div> <div><strong>Buffer Size:</strong> <span id="bufferSize">-</span></div> <div><strong>Last Sequence:</strong> <code id="lastSequence">-</code></div> </div> </div>
<!-- Buffer Inspector --> <div class="card"> <h2>🔍 Buffer Inspector</h2> <div class="info-box"> Inspect the actual buffer data (first 10 cells of current line) </div> <div id="bufferInspector" class="terminal-output" style="min-height: 100px;"></div> </div> </div>
<script type="module"> import { VTParser } from '../lib/vt-parser.js'; import { ScreenBuffer } from '../lib/buffer.js'; import { Ghostty } from '../lib/ghostty.js';
let parser, buffer, ghostty; let bellCount = 0;
// Initialize async function init() { try { ghostty = await Ghostty.load('../ghostty-vt.wasm'); buffer = new ScreenBuffer(80, 24, 100); parser = new VTParser(buffer, ghostty);
// Set up bell event listener parser.on('bell', () => { bellCount++; showStatus(`🔔 Bell event #${bellCount} received!`, 'success'); });
showStatus('✅ Parser loaded successfully', 'success'); updateDisplay(); } catch (error) { showStatus('❌ Error: ' + error.message, 'error'); console.error(error); } }
function showStatus(message, type = 'success') { const status = document.getElementById('status'); status.textContent = message; status.className = `status ${type}`; status.style.display = 'block'; setTimeout(() => { status.style.display = 'none'; }, 3000); }
function parseSequence(input) { // Convert string escapes to actual characters const processed = input .replace(/\\x1b/g, '\x1b') .replace(/\\t/g, '\t') .replace(/\\n/g, '\n') .replace(/\\r/g, '\r') .replace(/\\x07/g, '\x07'); parser.parse(processed); document.getElementById('lastSequence').textContent = input.substring(0, 50); updateDisplay(); }
function updateDisplay() { const terminal = document.getElementById('terminal'); const lines = buffer.getAllLines(); // Render terminal output with colors terminal.innerHTML = ''; for (let y = 0; y < lines.length; y++) { const line = lines[y]; let lineHtml = ''; for (let x = 0; x < line.length; x++) { const cell = line[x]; let styles = []; // Background color if (cell.bg.type === 'palette') { styles.push(`background-color: ${getAnsiColor(cell.bg.index, false)}`); } else if (cell.bg.type === 'rgb') { styles.push(`background-color: rgb(${cell.bg.r},${cell.bg.g},${cell.bg.b})`); } // Foreground color if (cell.fg.type === 'palette') { styles.push(`color: ${getAnsiColor(cell.fg.index, true)}`); } else if (cell.fg.type === 'rgb') { styles.push(`color: rgb(${cell.fg.r},${cell.fg.g},${cell.fg.b})`); } // Text styles if (cell.bold) styles.push('font-weight: bold'); if (cell.italic) styles.push('font-style: italic'); if (cell.underline) styles.push('text-decoration: underline'); if (cell.inverse) { styles.push('filter: invert(1)'); } const char = cell.char === ' ' ? ' ' : cell.char; const styleAttr = styles.length > 0 ? ` style="${styles.join(';')}"` : ''; lineHtml += `<span${styleAttr}>${char}</span>`; } terminal.innerHTML += lineHtml + '\n'; }
// Update stats const cursor = buffer.getCursor(); document.getElementById('cursorPos').textContent = `(${cursor.x}, ${cursor.y})`; document.getElementById('bufferSize').textContent = `${buffer.getDimensions().cols}x${buffer.getDimensions().rows}`;
// Update inspector updateInspector(); }
function updateInspector() { const inspector = document.getElementById('bufferInspector'); const cursor = buffer.getCursor(); const line = buffer.getLine(cursor.y); if (!line) { inspector.textContent = 'No data'; return; }
let output = `Line ${cursor.y} (first 10 cells):\n\n`; for (let i = 0; i < Math.min(10, line.length); i++) { const cell = line[i]; output += `[${i}] char: "${cell.char}" `; output += `fg: ${JSON.stringify(cell.fg).substring(0, 30)} `; output += `bg: ${JSON.stringify(cell.bg).substring(0, 30)}\n`; output += ` bold: ${cell.bold}, italic: ${cell.italic}, underline: ${cell.underline}\n`; } inspector.textContent = output; }
function getAnsiColor(index, isFg) { const colors = [ '#000000', '#cd3131', '#0dbc79', '#e5e510', // 0-3: black, red, green, yellow '#2472c8', '#bc3fbc', '#11a8cd', '#e5e5e5', // 4-7: blue, magenta, cyan, white '#666666', '#f14c4c', '#23d18b', '#f5f543', // 8-11: bright versions '#3b8eea', '#d670d6', '#29b8db', '#ffffff' // 12-15 ]; if (index < colors.length) { return colors[index]; } // 256-color palette approximation return isFg ? '#d4d4d4' : '#1e1e1e'; }
// Test functions window.testColors = () => { parseSequence('\\x1b[31mRed\\x1b[0m \\x1b[32mGreen\\x1b[0m \\x1b[33mYellow\\x1b[0m \\x1b[34mBlue\\x1b[0m\\n'); parseSequence('\\x1b[35mMagenta\\x1b[0m \\x1b[36mCyan\\x1b[0m \\x1b[37mWhite\\x1b[0m\\n'); showStatus('✅ ANSI colors test complete', 'success'); };
window.testBoldItalic = () => { parseSequence('\\x1b[1mBold\\x1b[0m \\x1b[3mItalic\\x1b[0m \\x1b[4mUnderline\\x1b[0m\\n'); parseSequence('\\x1b[1;31mBold Red\\x1b[0m \\x1b[3;34mItalic Blue\\x1b[0m\\n'); showStatus('✅ Text styles test complete', 'success'); };
window.testTabStops = () => { parseSequence('A\\tB\\tC\\tD\\tE\\n'); parseSequence('1\\t2\\t3\\t4\\t5\\n'); const line = buffer.getLine(buffer.getCursor().y - 2); const bPos = line.findIndex(c => c.char === 'B'); showStatus(`✅ Tab stops: B at column ${bPos} (expected 4)`, 'success'); };
window.testCursorMovement = () => { parseSequence('\\x1b[2J\\x1b[H'); // Clear and home parseSequence('\\x1b[5;10HX'); // Move to (5,10) parseSequence('\\x1b[10;20HY'); // Move to (10,20) parseSequence('\\x1b[15;30HZ'); // Move to (15,30) showStatus('✅ Cursor movement test complete', 'success'); };
window.testRGBColors = () => { parseSequence('\\x1b[38;2;255;0;0mRed RGB\\x1b[0m '); parseSequence('\\x1b[38;2;0;255;0mGreen RGB\\x1b[0m '); parseSequence('\\x1b[38;2;0;0;255mBlue RGB\\x1b[0m\\n'); parseSequence('\\x1b[38;2;255;165;0mOrange\\x1b[0m '); parseSequence('\\x1b[38;2;128;0;128mPurple\\x1b[0m\\n'); showStatus('✅ RGB colors test complete', 'success'); };
window.testClearScreen = () => { parseSequence('Before clear...\\n'); setTimeout(() => { parseSequence('\\x1b[2J\\x1b[H'); // Clear screen parseSequence('After clear!\\n'); showStatus('✅ Clear screen test complete', 'success'); }, 500); };
window.testScrollRegion = () => { parseSequence('\\x1b[2J\\x1b[H'); parseSequence('=== Header (stays fixed) ===\\n'); parseSequence('\\x1b[2;23r'); // Set scroll region lines 2-23 parseSequence('Scrollable content line 1\\n'); parseSequence('Scrollable content line 2\\n'); parseSequence('\\x1b[24;1H=== Footer (stays fixed) ==='); showStatus('✅ Scroll region test complete', 'success'); };
window.testBellEvent = () => { parseSequence('Ring! \\x07 Ring! \\x07 Ring! \\x07\\n'); setTimeout(() => { showStatus(`✅ Bell test complete (${bellCount} bells fired)`, 'success'); }, 100); };
window.testComplexOutput = () => { parseSequence('\\x1b[2J\\x1b[H'); parseSequence('\\x1b[1;36m╔════════════════════════════╗\\x1b[0m\\n'); parseSequence('\\x1b[1;36m║\\x1b[0m \\x1b[1mVT100 Parser Demo\\x1b[0m \\x1b[1;36m║\\x1b[0m\\n'); parseSequence('\\x1b[1;36m╚════════════════════════════╝\\x1b[0m\\n\\n'); parseSequence('\\x1b[32m✓\\x1b[0m Colors: \\x1b[31m■\\x1b[32m■\\x1b[33m■\\x1b[34m■\\x1b[35m■\\x1b[36m■\\x1b[0m\\n'); parseSequence('\\x1b[32m✓\\x1b[0m Styles: \\x1b[1mBold\\x1b[0m \\x1b[3mItalic\\x1b[0m \\x1b[4mUnderline\\x1b[0m\\n'); parseSequence('\\x1b[32m✓\\x1b[0m Tabs:\\tA\\tB\\tC\\n'); showStatus('✅ Complex output test complete', 'success'); };
window.clearOutput = () => { buffer = new ScreenBuffer(80, 24, 100); parser = new VTParser(buffer, ghostty); parser.on('bell', () => { bellCount++; showStatus(`🔔 Bell event #${bellCount} received!`, 'success'); }); updateDisplay(); showStatus('🗑️ Output cleared', 'success'); };
window.testCustom = () => { const input = document.getElementById('customInput').value; parseSequence(input); showStatus('✅ Custom sequence executed', 'success'); };
window.loadExample = (type) => { const examples = { colors: '\\x1b[31mRed\\x1b[0m \\x1b[32mGreen\\x1b[0m \\x1b[34mBlue\\x1b[0m\\n\\x1b[1;33mBold Yellow\\x1b[0m \\x1b[4;36mUnderline Cyan\\x1b[0m', vim: '\\x1b[2J\\x1b[H\\x1b[1;36m~ VIM Editor ~\\x1b[0m\\n\\x1b[24;1H\\x1b[7m-- INSERT --\\x1b[0m', ls: '\\x1b[34mfolder/\\x1b[0m \\x1b[32mscript.sh\\x1b[0m \\x1b[0mREADME.md\\x1b[0m \\x1b[31merror.log\\x1b[0m' }; document.getElementById('customInput').value = examples[type]; showStatus(`✅ Loaded ${type} example`, 'success'); };
// Initialize on load init(); </script></body></html>