vibetunnel/web/public/tests/test-renderer.html
Mario Zechner e8bee03388 Replace asciinema with XTerm renderer and add key combination support
- Replace AsciinemaPlayer with XTerm.js renderer in session-view and session-list
- Add XTerm CSS for proper terminal styling and hide input textarea
- Implement resize event handling (r-type cast events) in renderer
- Add Ctrl+Enter and Shift+Enter key combination support
- Update tty-fwd to handle ctrl_enter and shift_enter keys
- Set TERM=xterm-256color in tty-fwd for proper Unicode box-drawing
- Add font scaling and preview sizing for session-list terminals
- Remove asciinema dependencies and update CSS accordingly

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-16 10:38:19 +02:00

417 lines
No EOL
17 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>XTerm Terminal Renderer Test</title>
<!-- XTerm.js CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.css" />
<style>
body {
background: #1a1a1a;
color: #fff;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
margin: 0;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.controls {
margin-bottom: 20px;
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.controls button {
background: #333;
color: #fff;
border: 1px solid #555;
padding: 8px 16px;
cursor: pointer;
border-radius: 4px;
}
.controls button:hover {
background: #444;
}
.controls input {
background: #333;
color: #fff;
border: 1px solid #555;
padding: 8px;
border-radius: 4px;
width: 400px;
}
.terminal-container {
border: 2px solid #333;
border-radius: 8px;
background: #000;
height: 300px;
width: 100%;
overflow: hidden;
resize: both;
box-sizing: border-box;
}
.info {
margin-top: 20px;
background: #333;
padding: 15px;
border-radius: 4px;
font-size: 14px;
}
.section {
margin-bottom: 30px;
}
h2 {
color: #4CAF50;
margin-bottom: 10px;
}
.status {
padding: 10px;
border-radius: 4px;
margin-top: 10px;
display: none;
}
.status.success {
background: #2d5a2d;
color: #90EE90;
}
.status.error {
background: #5a2d2d;
color: #FFB6C1;
}
.comparison {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-top: 20px;
}
.comparison .terminal-container {
height: 250px;
}
.comparison h3 {
color: #FFB86C;
margin-bottom: 10px;
text-align: center;
}
</style>
</head>
<body>
<div class="container">
<h1>XTerm Terminal Renderer Test</h1>
<div class="section">
<h2>1. Test with Pre-recorded Cast File</h2>
<div class="controls">
<input type="text" id="castUrl" placeholder="Enter cast file URL or use buttons below">
<button onclick="loadCastFile()">Load Cast File</button>
<button onclick="loadTestFile()">Load Test File via Server</button>
<button onclick="loadSampleData()">Load Sample ANSI</button>
<button onclick="generateScrollbackTest()">Generate Scrollback Test</button>
<button onclick="clearTerminal()">Clear</button>
</div>
<div class="terminal-container" id="terminal1"></div>
<div class="status" id="status1"></div>
</div>
<div class="section">
<h2>2. Test with Live Session Stream</h2>
<div class="controls">
<input type="text" id="sessionId" placeholder="Enter session ID">
<button onclick="connectToSession()">Connect to Stream</button>
<button onclick="disconnectStream()">Disconnect</button>
<button onclick="clearTerminal2()">Clear</button>
</div>
<div class="terminal-container" id="terminal2"></div>
<div class="status" id="status2"></div>
</div>
<div class="section">
<h2>3. Manual Test</h2>
<div class="controls">
<input type="text" id="testInput" placeholder="Enter raw ANSI escape sequences to test"
value="\x1b[31mRed text\x1b[0m \x1b[1;32mBold green\x1b[0m">
<button onclick="sendTestInput()">Send to Terminal</button>
<button onclick="clearTerminal3()">Clear</button>
</div>
<div class="terminal-container" id="terminal3"></div>
</div>
<div class="section">
<h2>4. Renderer Comparison</h2>
<div class="controls">
<button onclick="runComparisonTest()">Run Comparison Test</button>
<button onclick="clearComparison()">Clear Both</button>
</div>
<div class="comparison">
<div>
<h3>Custom Renderer</h3>
<div class="terminal-container" id="terminal4"></div>
</div>
<div>
<h3>XTerm Renderer</h3>
<div class="terminal-container" id="terminal5"></div>
</div>
</div>
</div>
<div class="info">
<h2>XTerm.js Features:</h2>
<ul>
<li>✅ Full VT100/VT220/VT320 terminal emulation</li>
<li>✅ Professional-grade ANSI escape sequence parsing</li>
<li>✅ Advanced cursor movement and screen manipulation</li>
<li>✅ True color (24-bit RGB) support</li>
<li>✅ Sixel graphics support (addon)</li>
<li>✅ Unicode and emoji support</li>
<li>✅ Configurable scrollback buffer</li>
<li>✅ Responsive resizing with fit addon</li>
<li>✅ Web links detection and clickable URLs</li>
<li>✅ Better performance for large outputs</li>
<li>✅ Industry-standard terminal behavior</li>
</ul>
<h2>Usage Instructions:</h2>
<ul>
<li><strong>Cast File Test:</strong> Load terminal cast files with full compatibility</li>
<li><strong>Stream Test:</strong> Connect to live terminal sessions with real-time rendering</li>
<li><strong>Manual Test:</strong> Test specific ANSI sequences directly</li>
<li><strong>Comparison:</strong> Compare custom renderer vs XTerm.js side by side</li>
</ul>
</div>
</div>
<script type="module">
import { Renderer } from '../bundle/renderer.js';
// Initialize terminals
const terminal1 = new Renderer(document.getElementById('terminal1'));
const terminal2 = new Renderer(document.getElementById('terminal2'));
const terminal3 = new Renderer(document.getElementById('terminal3'));
const terminal4 = new Renderer(document.getElementById('terminal4'));
const terminal5 = new Renderer(document.getElementById('terminal5'));
let currentStream = null;
function showStatus(terminalNum, message, isError = false) {
const status = document.getElementById(`status${terminalNum}`);
status.textContent = message;
status.className = `status ${isError ? 'error' : 'success'}`;
status.style.display = 'block';
setTimeout(() => {
status.style.display = 'none';
}, 3000);
}
window.loadCastFile = async function() {
const url = document.getElementById('castUrl').value.trim();
if (!url) {
showStatus(1, 'Please enter a cast file URL', true);
return;
}
try {
showStatus(1, 'Loading cast file...');
await terminal1.loadCastFile(url);
showStatus(1, 'Cast file loaded successfully!');
} catch (error) {
console.error('Error loading cast file:', error);
showStatus(1, `Error loading cast file: ${error.message}`, true);
}
};
window.loadTestFile = async function() {
try {
showStatus(1, 'Loading test cast file...');
await terminal1.loadCastFile('/api/test-cast');
showStatus(1, 'Test cast file loaded successfully!');
} catch (error) {
console.error('Error loading test file:', error);
showStatus(1, `Error loading test file: ${error.message}`, true);
}
};
window.loadSampleData = function() {
try {
showStatus(1, 'Loading sample ANSI data...');
terminal1.clear();
// Sample ANSI sequences to demonstrate XTerm features
const samples = [
'{"version":2,"width":80,"height":24}\n',
'[0,"o","\\u001b[2J\\u001b[H"]\n', // Clear screen and home cursor
'[0.1,"o","\\u001b[1;31m╔════════════════════════════════════════════════════════════════════════════╗\\u001b[0m\\r\\n"]\n',
'[0.2,"o","\\u001b[1;31m║\\u001b[0m \\u001b[1;33mXTerm.js Renderer Demo\\u001b[0m \\u001b[1;31m║\\u001b[0m\\r\\n"]\n',
'[0.3,"o","\\u001b[1;31m╚════════════════════════════════════════════════════════════════════════════╝\\u001b[0m\\r\\n"]\n',
'[0.4,"o","\\r\\n"]\n',
'[0.5,"o","\\u001b[1mStandard Colors:\\u001b[0m \\u001b[31mRed\\u001b[0m \\u001b[32mGreen\\u001b[0m \\u001b[33mYellow\\u001b[0m \\u001b[34mBlue\\u001b[0m \\u001b[35mMagenta\\u001b[0m \\u001b[36mCyan\\u001b[0m\\r\\n"]\n',
'[0.6,"o","\\u001b[1mBright Colors:\\u001b[0m \\u001b[91mBright Red\\u001b[0m \\u001b[92mBright Green\\u001b[0m \\u001b[93mBright Yellow\\u001b[0m\\r\\n"]\n',
'[0.7,"o","\\u001b[1m256 Colors:\\u001b[0m \\u001b[38;5;196mColor 196\\u001b[0m \\u001b[38;5;46mColor 46\\u001b[0m \\u001b[38;5;21mColor 21\\u001b[0m\\r\\n"]\n',
'[0.8,"o","\\u001b[1mRGB Colors:\\u001b[0m \\u001b[38;2;255;100;50mOrange\\u001b[0m \\u001b[38;2;100;255;100mLime\\u001b[0m \\u001b[38;2;100;100;255mLight Blue\\u001b[0m\\r\\n"]\n',
'[0.9,"o","\\u001b[1mStyles:\\u001b[0m \\u001b[1mBold\\u001b[0m \\u001b[3mItalic\\u001b[0m \\u001b[4mUnderline\\u001b[0m \\u001b[9mStrikethrough\\u001b[0m \\u001b[7mInverse\\u001b[0m\\r\\n"]\n',
'[1.0,"o","\\u001b[1mCombined:\\u001b[0m \\u001b[1;3;4;38;2;255;165;0mBold Italic Underline Orange\\u001b[0m\\r\\n"]\n',
'[1.1,"o","\\u001b[1mUnicode:\\u001b[0m 🚀 ✨ 🎉 ♦ ♠ ♥ ♣ 中文 日本語 العربية\\r\\n"]\n',
'[1.2,"o","\\u001b[1mBox Drawing:\\u001b[0m ┌─┬─┐ │ │ │ ├─┼─┤ │ │ │ └─┴─┘\\r\\n"]\n',
'[1.3,"o","\\r\\n\\u001b[32m$\\u001b[0m echo \\"XTerm.js handles complex sequences perfectly\\"\\r\\n"]\n',
'[1.4,"o","XTerm.js handles complex sequences perfectly\\r\\n"]\n',
'[1.5,"o","\\u001b[32m$\\u001b[0m \\u001b[7m \\u001b[0m"]\n'
];
terminal1.parseCastFile(samples.join(''));
showStatus(1, 'Sample ANSI data loaded!');
} catch (error) {
console.error('Error loading sample data:', error);
showStatus(1, `Error loading sample data: ${error.message}`, true);
}
};
window.connectToSession = function() {
const sessionId = document.getElementById('sessionId').value.trim();
if (!sessionId) {
showStatus(2, 'Please enter a session ID', true);
return;
}
try {
if (currentStream) {
currentStream.close();
}
terminal2.clear();
currentStream = terminal2.connectToStream(sessionId);
currentStream.onopen = () => {
showStatus(2, `Connected to session ${sessionId}`);
};
currentStream.onerror = (error) => {
showStatus(2, `Stream error: ${error.message || 'Connection failed'}`, true);
};
} catch (error) {
console.error('Error connecting to stream:', error);
showStatus(2, `Error connecting: ${error.message}`, true);
}
};
window.disconnectStream = function() {
if (currentStream) {
currentStream.close();
currentStream = null;
showStatus(2, 'Disconnected from stream');
}
};
window.sendTestInput = function() {
const input = document.getElementById('testInput').value;
if (!input) {
showStatus(3, 'Please enter some test input', true);
return;
}
// Decode escape sequences
const decoded = input.replace(/\\x1b/g, '\x1b').replace(/\\n/g, '\n').replace(/\\r/g, '\r');
terminal3.processOutput(decoded);
showStatus(3, 'Test input processed');
};
window.clearTerminal = function() {
terminal1.clear();
showStatus(1, 'Terminal cleared');
};
window.clearTerminal2 = function() {
terminal2.clear();
showStatus(2, 'Terminal cleared');
};
window.clearTerminal3 = function() {
terminal3.clear();
showStatus(3, 'Terminal cleared');
};
window.generateScrollbackTest = function() {
try {
showStatus(1, 'Generating scrollback test...');
terminal1.clear();
// Generate lots of output to test scrollback
for (let i = 1; i <= 100; i++) {
const colors = [31, 32, 33, 34, 35, 36];
const color = colors[i % colors.length];
const line = `\x1b[1;${color}m[${i.toString().padStart(3, '0')}]\x1b[0m Line ${i}: XTerm.js scrollback with \x1b[4munderline\x1b[0m and \x1b[1mbold\x1b[0m text!\n`;
terminal1.processOutput(line);
}
terminal1.processOutput('\x1b[1;37m=== End of XTerm scrollback test ===\x1b[0m\n');
terminal1.processOutput('\x1b[32m$\x1b[0m \x1b[7m \x1b[0m');
showStatus(1, 'Generated 100 lines! XTerm.js handles scrollback smoothly.');
} catch (error) {
console.error('Error generating scrollback test:', error);
showStatus(1, `Error generating scrollback test: ${error.message}`, true);
}
};
window.runComparisonTest = function() {
const testData = [
'\x1b[2J\x1b[H', // Clear and home
'\x1b[1;31m=== Renderer Comparison Test ===\x1b[0m\n',
'\x1b[1mColors:\x1b[0m \x1b[31mRed\x1b[0m \x1b[32mGreen\x1b[0m \x1b[33mYellow\x1b[0m \x1b[34mBlue\x1b[0m\n',
'\x1b[1mRGB:\x1b[0m \x1b[38;2;255;100;50mOrange RGB\x1b[0m \x1b[38;2;100;255;100mLime RGB\x1b[0m\n',
'\x1b[1mStyles:\x1b[0m \x1b[1mBold\x1b[0m \x1b[3mItalic\x1b[0m \x1b[4mUnderline\x1b[0m \x1b[7mInverse\x1b[0m\n',
'\x1b[1mUnicode:\x1b[0m 🚀 ✨ 🎉 ♦ ♠ ♥ ♣\n',
'\x1b[1mBox:\x1b[0m ┌─┬─┐\n │ │ │\n ├─┼─┤\n │ │ │\n └─┴─┘\n',
'\x1b[32m$\x1b[0m echo "Compare renderers"\n',
'Custom renderer vs XTerm.js\n',
'\x1b[32m$\x1b[0m \x1b[7m \x1b[0m'
];
const combined = testData.join('');
// Clear both terminals
terminal4.clear();
terminal5.clear();
// Process on both renderers
terminal4.processOutput(combined);
terminal5.processOutput(combined);
};
window.clearComparison = function() {
terminal4.clear();
terminal5.clear();
};
// Initialize with a welcome message showing XTerm capabilities
setTimeout(() => {
const welcomeSequences = [
'\x1b[1;36m♦ XTerm.js Renderer Ready! ♦\x1b[0m\n',
'\x1b[38;2;100;255;100mProfessional terminal emulation\x1b[0m\n',
'\x1b[1mTry the sample data or comparison test!\x1b[0m\n'
];
welcomeSequences.forEach(seq => terminal3.processOutput(seq));
}, 100);
</script>
</body>
</html>