mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-07 11:35:53 +00:00
- 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>
417 lines
No EOL
17 KiB
HTML
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> |