mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
Replace custom renderer with XTerm.js and consolidate tests
- Remove old custom ANSI renderer (src/client/renderer.ts) - Rename XTermRenderer to Renderer and move to renderer.ts - Update renderer-entry.ts to export single Renderer class - Rename and update all test files: - test-xterm-renderer.html → test-renderer.html - simple-xterm-test.html → simple-test.html - debug-xterm.html → debug-renderer.html - Remove obsolete custom renderer tests - Update tests/index.html with new test descriptions - All tests now use single XTerm.js-based Renderer 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
e4b4a37c86
commit
c7c8a605c2
9 changed files with 413 additions and 1619 deletions
|
|
@ -56,50 +56,50 @@
|
||||||
<div id="log"></div>
|
<div id="log"></div>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { XTermRenderer } from '../bundle/renderer.js';
|
import { Renderer } from '../bundle/renderer.js';
|
||||||
|
|
||||||
function log(message) {
|
function log(message) {
|
||||||
const logDiv = document.getElementById('log');
|
const logDiv = document.getElementById('log');
|
||||||
logDiv.textContent += new Date().toLocaleTimeString() + ': ' + message + '\n';
|
logDiv.textContent += new Date().toLocaleTimeString() + ': ' + message + '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test XTerm renderer from bundle
|
// Test renderer from bundle
|
||||||
log('Testing XTerm Renderer from bundle...');
|
log('Testing Renderer from bundle...');
|
||||||
log('XTermRenderer available: ' + (typeof XTermRenderer !== 'undefined'));
|
log('Renderer available: ' + (typeof Renderer !== 'undefined'));
|
||||||
|
|
||||||
window.testXTerm = function() {
|
window.testXTerm = function() {
|
||||||
try {
|
try {
|
||||||
log('Testing XTerm Renderer...');
|
log('Testing Renderer...');
|
||||||
const container = document.getElementById('terminal');
|
const container = document.getElementById('terminal');
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
const renderer = new XTermRenderer(container, 80, 24);
|
const renderer = new Renderer(container, 80, 24);
|
||||||
renderer.processOutput('Hello from bundled XTerm.js!\r\n');
|
renderer.processOutput('Hello from bundled XTerm.js!\r\n');
|
||||||
renderer.processOutput('\x1b[31mRed text\x1b[0m\r\n');
|
renderer.processOutput('\x1b[31mRed text\x1b[0m\r\n');
|
||||||
renderer.processOutput('\x1b[32mGreen text\x1b[0m\r\n');
|
renderer.processOutput('\x1b[32mGreen text\x1b[0m\r\n');
|
||||||
renderer.processOutput('\x1b[1;33mBold yellow text\x1b[0m\r\n');
|
renderer.processOutput('\x1b[1;33mBold yellow text\x1b[0m\r\n');
|
||||||
|
|
||||||
log('XTerm Renderer test successful!');
|
log('Renderer test successful!');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log('XTerm Renderer test failed: ' + error.message);
|
log('Renderer test failed: ' + error.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.testRenderer = function() {
|
window.testRenderer = function() {
|
||||||
try {
|
try {
|
||||||
log('Testing XTerm Renderer advanced features...');
|
log('Testing Renderer advanced features...');
|
||||||
const container = document.getElementById('terminal');
|
const container = document.getElementById('terminal');
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
const renderer = new XTermRenderer(container, 80, 24);
|
const renderer = new Renderer(container, 80, 24);
|
||||||
renderer.processOutput('Hello from XTerm Renderer!\r\n');
|
renderer.processOutput('Hello from XTerm Renderer!\r\n');
|
||||||
renderer.processOutput('\x1b[32mGreen text from renderer\x1b[0m\r\n');
|
renderer.processOutput('\x1b[32mGreen text from renderer\x1b[0m\r\n');
|
||||||
renderer.processOutput('\x1b[1;34mBold blue text\x1b[0m\r\n');
|
renderer.processOutput('\x1b[1;34mBold blue text\x1b[0m\r\n');
|
||||||
renderer.processOutput('\x1b[43mYellow background\x1b[0m\r\n');
|
renderer.processOutput('\x1b[43mYellow background\x1b[0m\r\n');
|
||||||
|
|
||||||
log('XTerm Renderer advanced test successful!');
|
log('Renderer advanced test successful!');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log('XTerm Renderer advanced test failed: ' + error.message);
|
log('Renderer advanced test failed: ' + error.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -79,43 +79,27 @@
|
||||||
|
|
||||||
<div class="test-grid">
|
<div class="test-grid">
|
||||||
<div class="test-card">
|
<div class="test-card">
|
||||||
<div class="test-title">Custom Renderer Test</div>
|
<div class="test-title">Terminal Renderer Test</div>
|
||||||
<div class="test-description">
|
<div class="test-description">
|
||||||
Test the original custom ANSI renderer implementation with various escape sequences and color codes.
|
Comprehensive test of the XTerm.js-based renderer with cast file playback, streaming, and manual input testing.
|
||||||
</div>
|
</div>
|
||||||
<a href="test-renderer.html" class="test-link">Run Test</a>
|
<a href="test-renderer.html" class="test-link">Run Test</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="test-card">
|
|
||||||
<div class="test-title">XTerm Renderer Test</div>
|
|
||||||
<div class="test-description">
|
|
||||||
Test the new XTerm.js-based renderer with comprehensive terminal features and cast file playback.
|
|
||||||
</div>
|
|
||||||
<a href="test-xterm-renderer.html" class="test-link">Run Test</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="test-card">
|
|
||||||
<div class="test-title">Side-by-Side Comparison</div>
|
|
||||||
<div class="test-description">
|
|
||||||
Compare custom renderer vs XTerm.js renderer side-by-side with the same input data.
|
|
||||||
</div>
|
|
||||||
<a href="simple-xterm-test.html" class="test-link">Run Test</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="test-card">
|
<div class="test-card">
|
||||||
<div class="test-title">Simple Renderer Test</div>
|
<div class="test-title">Simple Renderer Test</div>
|
||||||
<div class="test-description">
|
<div class="test-description">
|
||||||
Basic test of the custom renderer with simple ANSI sequences and text output.
|
Basic dual-terminal test comparing performance and rendering with simple ANSI sequences.
|
||||||
</div>
|
</div>
|
||||||
<a href="simple-test.html" class="test-link">Run Test</a>
|
<a href="simple-test.html" class="test-link">Run Test</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="test-card">
|
<div class="test-card">
|
||||||
<div class="test-title">XTerm Debug Test</div>
|
<div class="test-title">Debug Renderer Test</div>
|
||||||
<div class="test-description">
|
<div class="test-description">
|
||||||
Debug version of XTerm renderer with additional logging and development features.
|
Debug version with detailed logging for troubleshooting import issues and basic functionality.
|
||||||
</div>
|
</div>
|
||||||
<a href="debug-xterm.html" class="test-link">Run Test</a>
|
<a href="debug-renderer.html" class="test-link">Run Test</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="test-card">
|
<div class="test-card">
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,11 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Simple Terminal Renderer Test</title>
|
<title>Simple XTerm Renderer Test</title>
|
||||||
|
|
||||||
|
<!-- XTerm.js CSS -->
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.css" />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
background: #1a1a1a;
|
background: #1a1a1a;
|
||||||
|
|
@ -18,10 +22,8 @@
|
||||||
background: #000;
|
background: #000;
|
||||||
height: 400px;
|
height: 400px;
|
||||||
width: 800px;
|
width: 800px;
|
||||||
overflow-y: scroll;
|
overflow: hidden;
|
||||||
padding: 10px;
|
margin-bottom: 20px;
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
|
|
@ -41,37 +43,69 @@
|
||||||
button:hover {
|
button:hover {
|
||||||
background: #444;
|
background: #444;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comparison {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comparison .terminal {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: #4CAF50;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Simple Terminal Renderer Debug</h1>
|
<h1>Simple XTerm Renderer Test</h1>
|
||||||
|
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<button onclick="testBasic()">Test Basic Colors</button>
|
<button onclick="testBasic()">Test Basic Colors</button>
|
||||||
|
<button onclick="testAdvanced()">Test Advanced Features</button>
|
||||||
<button onclick="testScrollback()">Test Scrollback</button>
|
<button onclick="testScrollback()">Test Scrollback</button>
|
||||||
<button onclick="clearTerminal()">Clear</button>
|
<button onclick="testPerformance()">Test Performance</button>
|
||||||
<button onclick="debugOutput()">Debug Output</button>
|
<button onclick="clearTerminals()">Clear All</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="terminal" id="terminal"></div>
|
<div class="comparison">
|
||||||
|
<div>
|
||||||
|
<h2>Custom Renderer</h2>
|
||||||
|
<div class="terminal" id="custom-terminal"></div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2>XTerm.js Renderer</h2>
|
||||||
|
<div class="terminal" id="xterm-terminal"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="debug" style="margin-top: 20px; background: #333; padding: 10px; border-radius: 4px;">
|
<div id="debug" style="margin-top: 20px; background: #333; padding: 10px; border-radius: 4px;">
|
||||||
<h3>Debug Output:</h3>
|
<h3>Performance Comparison:</h3>
|
||||||
<pre id="debugText"></pre>
|
<pre id="debugText">Run performance test to see timing comparison</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { TerminalRenderer } from './renderer.js';
|
import { Renderer } from '../bundle/renderer.js';
|
||||||
|
|
||||||
const terminal = new TerminalRenderer(document.getElementById('terminal'), 80, 20);
|
const terminal1 = new Renderer(document.getElementById('custom-terminal'), 80, 20);
|
||||||
|
const terminal2 = new Renderer(document.getElementById('xterm-terminal'), 80, 20);
|
||||||
|
|
||||||
|
function runOnBoth(callback) {
|
||||||
|
callback(terminal1, 'Terminal 1');
|
||||||
|
callback(terminal2, 'Terminal 2');
|
||||||
|
}
|
||||||
|
|
||||||
window.testBasic = function() {
|
window.testBasic = function() {
|
||||||
console.log('Testing basic colors...');
|
console.log('Testing basic colors...');
|
||||||
|
|
||||||
|
runOnBoth((terminal, name) => {
|
||||||
terminal.clear();
|
terminal.clear();
|
||||||
|
|
||||||
// Test individual ANSI sequences
|
|
||||||
const tests = [
|
const tests = [
|
||||||
'Hello World (no color)\n',
|
`${name} Renderer Test\n`,
|
||||||
'\x1b[31mRed Text\x1b[0m\n',
|
'\x1b[31mRed Text\x1b[0m\n',
|
||||||
'\x1b[32mGreen Text\x1b[0m\n',
|
'\x1b[32mGreen Text\x1b[0m\n',
|
||||||
'\x1b[1;33mBold Yellow\x1b[0m\n',
|
'\x1b[1;33mBold Yellow\x1b[0m\n',
|
||||||
|
|
@ -80,54 +114,99 @@
|
||||||
'\x1b[1;32m[002]\x1b[0m Test line 2\n'
|
'\x1b[1;32m[002]\x1b[0m Test line 2\n'
|
||||||
];
|
];
|
||||||
|
|
||||||
tests.forEach((test, i) => {
|
tests.forEach(test => terminal.processOutput(test));
|
||||||
console.log(`Processing test ${i}:`, JSON.stringify(test));
|
|
||||||
terminal.processOutput(test);
|
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
updateDebug();
|
window.testAdvanced = function() {
|
||||||
|
console.log('Testing advanced features...');
|
||||||
|
|
||||||
|
runOnBoth((terminal, name) => {
|
||||||
|
terminal.clear();
|
||||||
|
|
||||||
|
const tests = [
|
||||||
|
`${name} Advanced Features\n`,
|
||||||
|
'\x1b[38;2;255;100;50mRGB Orange\x1b[0m\n',
|
||||||
|
'\x1b[38;2;100;255;100mRGB Lime\x1b[0m\n',
|
||||||
|
'\x1b[38;5;196m256-color Red\x1b[0m\n',
|
||||||
|
'\x1b[38;5;46m256-color Green\x1b[0m\n',
|
||||||
|
'\x1b[1;3;4mBold Italic Underline\x1b[0m\n',
|
||||||
|
'\x1b[7mInverse Video\x1b[0m\n',
|
||||||
|
'\x1b[9mStrikethrough\x1b[0m\n',
|
||||||
|
'Unicode: 🚀 ✨ 🎉 ♦ ♠ ♥ ♣\n',
|
||||||
|
'Box: ┌─┬─┐ │ │ │ ├─┼─┤\n'
|
||||||
|
];
|
||||||
|
|
||||||
|
tests.forEach(test => terminal.processOutput(test));
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
window.testScrollback = function() {
|
window.testScrollback = function() {
|
||||||
console.log('Testing scrollback...');
|
console.log('Testing scrollback...');
|
||||||
|
|
||||||
|
runOnBoth((terminal, name) => {
|
||||||
terminal.clear();
|
terminal.clear();
|
||||||
|
|
||||||
for (let i = 1; i <= 30; i++) {
|
for (let i = 1; i <= 30; i++) {
|
||||||
const color = 31 + (i % 6);
|
const color = 31 + (i % 6);
|
||||||
const line = `\x1b[1;${color}m[${i.toString().padStart(3, '0')}]\x1b[0m Line ${i}: Scrollback test content\n`;
|
const line = `\x1b[1;${color}m[${i.toString().padStart(3, '0')}]\x1b[0m ${name} Line ${i}\n`;
|
||||||
console.log(`Line ${i}:`, JSON.stringify(line));
|
|
||||||
terminal.processOutput(line);
|
terminal.processOutput(line);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
updateDebug();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
window.clearTerminal = function() {
|
window.testPerformance = function() {
|
||||||
terminal.clear();
|
console.log('Testing performance...');
|
||||||
updateDebug();
|
|
||||||
};
|
|
||||||
|
|
||||||
window.debugOutput = function() {
|
const results = {};
|
||||||
updateDebug();
|
|
||||||
};
|
|
||||||
|
|
||||||
function updateDebug() {
|
// Test terminal 1
|
||||||
const terminalEl = document.getElementById('terminal');
|
const terminal1Start = performance.now();
|
||||||
const debugEl = document.getElementById('debugText');
|
terminal1.clear();
|
||||||
|
for (let i = 1; i <= 1000; i++) {
|
||||||
const info = {
|
const color = 31 + (i % 6);
|
||||||
innerHTML: terminalEl.innerHTML.substring(0, 500) + (terminalEl.innerHTML.length > 500 ? '...' : ''),
|
const line = `\x1b[1;${color}m[${i.toString().padStart(4, '0')}]\x1b[0m Performance test line ${i} with \x1b[4munderline\x1b[0m and \x1b[1mbold\x1b[0m\n`;
|
||||||
childCount: terminalEl.children.length,
|
terminal1.processOutput(line);
|
||||||
scrollHeight: terminalEl.scrollHeight,
|
|
||||||
clientHeight: terminalEl.clientHeight
|
|
||||||
};
|
|
||||||
|
|
||||||
debugEl.textContent = JSON.stringify(info, null, 2);
|
|
||||||
}
|
}
|
||||||
|
const terminal1End = performance.now();
|
||||||
|
results.terminal1 = terminal1End - terminal1Start;
|
||||||
|
|
||||||
|
// Test terminal 2
|
||||||
|
const terminal2Start = performance.now();
|
||||||
|
terminal2.clear();
|
||||||
|
for (let i = 1; i <= 1000; i++) {
|
||||||
|
const color = 31 + (i % 6);
|
||||||
|
const line = `\x1b[1;${color}m[${i.toString().padStart(4, '0')}]\x1b[0m Performance test line ${i} with \x1b[4munderline\x1b[0m and \x1b[1mbold\x1b[0m\n`;
|
||||||
|
terminal2.processOutput(line);
|
||||||
|
}
|
||||||
|
const terminal2End = performance.now();
|
||||||
|
results.terminal2 = terminal2End - terminal2Start;
|
||||||
|
|
||||||
|
// Display results
|
||||||
|
const debugEl = document.getElementById('debugText');
|
||||||
|
const comparison = {
|
||||||
|
'Terminal 1': `${results.terminal1.toFixed(2)}ms`,
|
||||||
|
'Terminal 2': `${results.terminal2.toFixed(2)}ms`,
|
||||||
|
'Performance Ratio': `${(results.terminal1 / results.terminal2).toFixed(2)}x`,
|
||||||
|
'Winner': results.terminal2 < results.terminal1 ? 'Terminal 2' : 'Terminal 1',
|
||||||
|
'Lines Processed': '1000 lines each',
|
||||||
|
'Test Date': new Date().toLocaleString()
|
||||||
|
};
|
||||||
|
|
||||||
|
debugEl.textContent = JSON.stringify(comparison, null, 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.clearTerminals = function() {
|
||||||
|
terminal1.clear();
|
||||||
|
terminal2.clear();
|
||||||
|
|
||||||
|
const debugEl = document.getElementById('debugText');
|
||||||
|
debugEl.textContent = 'Terminals cleared. Run tests to see comparison.';
|
||||||
|
};
|
||||||
|
|
||||||
// Initial test
|
// Initial test
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
console.log('Running initial test...');
|
console.log('Running initial comparison...');
|
||||||
testBasic();
|
testBasic();
|
||||||
}, 100);
|
}, 100);
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,214 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Simple XTerm 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: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal {
|
|
||||||
border: 2px solid #333;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #000;
|
|
||||||
height: 400px;
|
|
||||||
width: 800px;
|
|
||||||
overflow: hidden;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
background: #333;
|
|
||||||
color: #fff;
|
|
||||||
border: 1px solid #555;
|
|
||||||
padding: 8px 16px;
|
|
||||||
margin-right: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background: #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comparison {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comparison .terminal {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
color: #4CAF50;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Simple XTerm Renderer Test</h1>
|
|
||||||
|
|
||||||
<div class="controls">
|
|
||||||
<button onclick="testBasic()">Test Basic Colors</button>
|
|
||||||
<button onclick="testAdvanced()">Test Advanced Features</button>
|
|
||||||
<button onclick="testScrollback()">Test Scrollback</button>
|
|
||||||
<button onclick="testPerformance()">Test Performance</button>
|
|
||||||
<button onclick="clearTerminals()">Clear All</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="comparison">
|
|
||||||
<div>
|
|
||||||
<h2>Custom Renderer</h2>
|
|
||||||
<div class="terminal" id="custom-terminal"></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2>XTerm.js Renderer</h2>
|
|
||||||
<div class="terminal" id="xterm-terminal"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="debug" style="margin-top: 20px; background: #333; padding: 10px; border-radius: 4px;">
|
|
||||||
<h3>Performance Comparison:</h3>
|
|
||||||
<pre id="debugText">Run performance test to see timing comparison</pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script type="module">
|
|
||||||
import { Renderer, XTermRenderer } from '../bundle/renderer.js';
|
|
||||||
|
|
||||||
const customTerminal = new Renderer(document.getElementById('custom-terminal'), 80, 20);
|
|
||||||
const xtermTerminal = new XTermRenderer(document.getElementById('xterm-terminal'), 80, 20);
|
|
||||||
|
|
||||||
function runOnBoth(callback) {
|
|
||||||
callback(customTerminal, 'Custom');
|
|
||||||
callback(xtermTerminal, 'XTerm');
|
|
||||||
}
|
|
||||||
|
|
||||||
window.testBasic = function() {
|
|
||||||
console.log('Testing basic colors...');
|
|
||||||
|
|
||||||
runOnBoth((terminal, name) => {
|
|
||||||
terminal.clear();
|
|
||||||
|
|
||||||
const tests = [
|
|
||||||
`${name} Renderer Test\n`,
|
|
||||||
'\x1b[31mRed Text\x1b[0m\n',
|
|
||||||
'\x1b[32mGreen Text\x1b[0m\n',
|
|
||||||
'\x1b[1;33mBold Yellow\x1b[0m\n',
|
|
||||||
'\x1b[4;34mUnderline Blue\x1b[0m\n',
|
|
||||||
'\x1b[1;31m[001]\x1b[0m Test line 1\n',
|
|
||||||
'\x1b[1;32m[002]\x1b[0m Test line 2\n'
|
|
||||||
];
|
|
||||||
|
|
||||||
tests.forEach(test => terminal.processOutput(test));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
window.testAdvanced = function() {
|
|
||||||
console.log('Testing advanced features...');
|
|
||||||
|
|
||||||
runOnBoth((terminal, name) => {
|
|
||||||
terminal.clear();
|
|
||||||
|
|
||||||
const tests = [
|
|
||||||
`${name} Advanced Features\n`,
|
|
||||||
'\x1b[38;2;255;100;50mRGB Orange\x1b[0m\n',
|
|
||||||
'\x1b[38;2;100;255;100mRGB Lime\x1b[0m\n',
|
|
||||||
'\x1b[38;5;196m256-color Red\x1b[0m\n',
|
|
||||||
'\x1b[38;5;46m256-color Green\x1b[0m\n',
|
|
||||||
'\x1b[1;3;4mBold Italic Underline\x1b[0m\n',
|
|
||||||
'\x1b[7mInverse Video\x1b[0m\n',
|
|
||||||
'\x1b[9mStrikethrough\x1b[0m\n',
|
|
||||||
'Unicode: 🚀 ✨ 🎉 ♦ ♠ ♥ ♣\n',
|
|
||||||
'Box: ┌─┬─┐ │ │ │ ├─┼─┤\n'
|
|
||||||
];
|
|
||||||
|
|
||||||
tests.forEach(test => terminal.processOutput(test));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
window.testScrollback = function() {
|
|
||||||
console.log('Testing scrollback...');
|
|
||||||
|
|
||||||
runOnBoth((terminal, name) => {
|
|
||||||
terminal.clear();
|
|
||||||
|
|
||||||
for (let i = 1; i <= 30; i++) {
|
|
||||||
const color = 31 + (i % 6);
|
|
||||||
const line = `\x1b[1;${color}m[${i.toString().padStart(3, '0')}]\x1b[0m ${name} Line ${i}\n`;
|
|
||||||
terminal.processOutput(line);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
window.testPerformance = function() {
|
|
||||||
console.log('Testing performance...');
|
|
||||||
|
|
||||||
const results = {};
|
|
||||||
|
|
||||||
// Test custom renderer
|
|
||||||
const customStart = performance.now();
|
|
||||||
customTerminal.clear();
|
|
||||||
for (let i = 1; i <= 1000; i++) {
|
|
||||||
const color = 31 + (i % 6);
|
|
||||||
const line = `\x1b[1;${color}m[${i.toString().padStart(4, '0')}]\x1b[0m Performance test line ${i} with \x1b[4munderline\x1b[0m and \x1b[1mbold\x1b[0m\n`;
|
|
||||||
customTerminal.processOutput(line);
|
|
||||||
}
|
|
||||||
const customEnd = performance.now();
|
|
||||||
results.custom = customEnd - customStart;
|
|
||||||
|
|
||||||
// Test XTerm renderer
|
|
||||||
const xtermStart = performance.now();
|
|
||||||
xtermTerminal.clear();
|
|
||||||
for (let i = 1; i <= 1000; i++) {
|
|
||||||
const color = 31 + (i % 6);
|
|
||||||
const line = `\x1b[1;${color}m[${i.toString().padStart(4, '0')}]\x1b[0m Performance test line ${i} with \x1b[4munderline\x1b[0m and \x1b[1mbold\x1b[0m\n`;
|
|
||||||
xtermTerminal.processOutput(line);
|
|
||||||
}
|
|
||||||
const xtermEnd = performance.now();
|
|
||||||
results.xterm = xtermEnd - xtermStart;
|
|
||||||
|
|
||||||
// Display results
|
|
||||||
const debugEl = document.getElementById('debugText');
|
|
||||||
const comparison = {
|
|
||||||
'Custom Renderer': `${results.custom.toFixed(2)}ms`,
|
|
||||||
'XTerm.js Renderer': `${results.xterm.toFixed(2)}ms`,
|
|
||||||
'Performance Ratio': `${(results.custom / results.xterm).toFixed(2)}x`,
|
|
||||||
'Winner': results.xterm < results.custom ? 'XTerm.js' : 'Custom',
|
|
||||||
'Lines Processed': '1000 lines each',
|
|
||||||
'Test Date': new Date().toLocaleString()
|
|
||||||
};
|
|
||||||
|
|
||||||
debugEl.textContent = JSON.stringify(comparison, null, 2);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.clearTerminals = function() {
|
|
||||||
customTerminal.clear();
|
|
||||||
xtermTerminal.clear();
|
|
||||||
|
|
||||||
const debugEl = document.getElementById('debugText');
|
|
||||||
debugEl.textContent = 'Terminals cleared. Run tests to see comparison.';
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initial test
|
|
||||||
setTimeout(() => {
|
|
||||||
console.log('Running initial comparison...');
|
|
||||||
testBasic();
|
|
||||||
}, 100);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -3,7 +3,11 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Terminal Renderer Test</title>
|
<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>
|
<style>
|
||||||
body {
|
body {
|
||||||
background: #1a1a1a;
|
background: #1a1a1a;
|
||||||
|
|
@ -54,44 +58,11 @@
|
||||||
background: #000;
|
background: #000;
|
||||||
height: 300px;
|
height: 300px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow-y: scroll;
|
overflow: hidden;
|
||||||
overflow-x: auto;
|
|
||||||
resize: both;
|
resize: both;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Make sure scrollbars are visible */
|
|
||||||
.terminal-container::-webkit-scrollbar {
|
|
||||||
width: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-container::-webkit-scrollbar-track {
|
|
||||||
background: #222;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-container::-webkit-scrollbar-thumb {
|
|
||||||
background: #555;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-container::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: #777;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollback-line {
|
|
||||||
opacity: 0.8;
|
|
||||||
border-left: 2px solid #444;
|
|
||||||
padding-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-line {
|
|
||||||
min-height: 1.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.current-line {
|
|
||||||
background-color: rgba(255, 255, 255, 0.02);
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
background: #333;
|
background: #333;
|
||||||
|
|
@ -125,11 +96,28 @@
|
||||||
background: #5a2d2d;
|
background: #5a2d2d;
|
||||||
color: #FFB6C1;
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>Terminal Renderer Test</h1>
|
<h1>XTerm Terminal Renderer Test</h1>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>1. Test with Pre-recorded Cast File</h2>
|
<h2>1. Test with Pre-recorded Cast File</h2>
|
||||||
|
|
@ -168,27 +156,46 @@
|
||||||
<div class="terminal-container" id="terminal3"></div>
|
<div class="terminal-container" id="terminal3"></div>
|
||||||
</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">
|
<div class="info">
|
||||||
<h2>Usage Instructions:</h2>
|
<h2>XTerm.js Features:</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>Cast File Test:</strong> The default URL points to your test cast file. Click "Load Cast File" to render it.</li>
|
<li>✅ Full VT100/VT220/VT320 terminal emulation</li>
|
||||||
<li><strong>Stream Test:</strong> Enter a session ID and click "Connect to Stream" to watch live terminal output.</li>
|
<li>✅ Professional-grade ANSI escape sequence parsing</li>
|
||||||
<li><strong>Manual Test:</strong> Enter raw ANSI escape sequences to test specific rendering features.</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>
|
</ul>
|
||||||
|
|
||||||
<h2>Features Supported:</h2>
|
<h2>Usage Instructions:</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>✅ ANSI color codes (standard 16 colors + 256-color + RGB)</li>
|
<li><strong>Cast File Test:</strong> Load asciinema cast files with full compatibility</li>
|
||||||
<li>✅ Text formatting (bold, italic, underline, strikethrough)</li>
|
<li><strong>Stream Test:</strong> Connect to live terminal sessions with real-time rendering</li>
|
||||||
<li>✅ Cursor movement and positioning</li>
|
<li><strong>Manual Test:</strong> Test specific ANSI sequences directly</li>
|
||||||
<li>✅ Screen clearing and line erasing</li>
|
<li><strong>Comparison:</strong> Compare custom renderer vs XTerm.js side by side</li>
|
||||||
<li>✅ Alternate screen buffer support</li>
|
|
||||||
<li>✅ Scrollback buffer (up to 1000 lines)</li>
|
|
||||||
<li>✅ Scroll regions</li>
|
|
||||||
<li>✅ Auto-wrap mode</li>
|
|
||||||
<li>✅ Inverse video</li>
|
|
||||||
<li>✅ Cast file format v2 parsing</li>
|
|
||||||
<li>✅ Server-Sent Events streaming</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -200,6 +207,8 @@
|
||||||
const terminal1 = new Renderer(document.getElementById('terminal1'));
|
const terminal1 = new Renderer(document.getElementById('terminal1'));
|
||||||
const terminal2 = new Renderer(document.getElementById('terminal2'));
|
const terminal2 = new Renderer(document.getElementById('terminal2'));
|
||||||
const terminal3 = new Renderer(document.getElementById('terminal3'));
|
const terminal3 = new Renderer(document.getElementById('terminal3'));
|
||||||
|
const terminal4 = new Renderer(document.getElementById('terminal4'));
|
||||||
|
const terminal5 = new Renderer(document.getElementById('terminal5'));
|
||||||
|
|
||||||
let currentStream = null;
|
let currentStream = null;
|
||||||
|
|
||||||
|
|
@ -246,23 +255,25 @@
|
||||||
showStatus(1, 'Loading sample ANSI data...');
|
showStatus(1, 'Loading sample ANSI data...');
|
||||||
terminal1.clear();
|
terminal1.clear();
|
||||||
|
|
||||||
// Sample ANSI sequences to demonstrate features
|
// Sample ANSI sequences to demonstrate XTerm features
|
||||||
const samples = [
|
const samples = [
|
||||||
'{"version":2,"width":80,"height":24}\n',
|
'{"version":2,"width":80,"height":24}\n',
|
||||||
'[0,"o","\\u001b[2J\\u001b[H"]\n', // Clear screen and home cursor
|
'[0,"o","\\u001b[2J\\u001b[H"]\n', // Clear screen and home cursor
|
||||||
'[0.1,"o","\\u001b[1;31m╔════════════════════════════════════════════════════════════════════════════╗\\u001b[0m\\r\\n"]\n',
|
'[0.1,"o","\\u001b[1;31m╔════════════════════════════════════════════════════════════════════════════╗\\u001b[0m\\r\\n"]\n',
|
||||||
'[0.2,"o","\\u001b[1;31m║\\u001b[0m \\u001b[1;33mTerminal Renderer Demo\\u001b[0m \\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.3,"o","\\u001b[1;31m╚════════════════════════════════════════════════════════════════════════════╝\\u001b[0m\\r\\n"]\n',
|
||||||
'[0.4,"o","\\r\\n"]\n',
|
'[0.4,"o","\\r\\n"]\n',
|
||||||
'[0.5,"o","\\u001b[1mColors:\\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.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[1mStyles:\\u001b[0m \\u001b[1mBold\\u001b[0m \\u001b[3mItalic\\u001b[0m \\u001b[4mUnderline\\u001b[0m \\u001b[9mStrikethrough\\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[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;255mLightBlue\\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[1mInverse:\\u001b[0m \\u001b[7mWhite on Black\\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","\\r\\n\\u001b[32m$\\u001b[0m ls -la\\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","drwxr-xr-x 5 user staff 160 Dec 16 10:30 \\u001b[34m.\\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","drwxr-xr-x 10 user staff 320 Dec 16 10:25 \\u001b[34m..\\u001b[0m\\r\\n"]\n',
|
'[1.1,"o","\\u001b[1mUnicode:\\u001b[0m 🚀 ✨ 🎉 ♦ ♠ ♥ ♣ 中文 日本語 العربية\\r\\n"]\n',
|
||||||
'[1.2,"o","-rw-r--r-- 1 user staff 1024 Dec 16 10:30 \\u001b[32mrenderer.ts\\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 \\u001b[7m \\u001b[0m"]\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(''));
|
terminal1.parseCastFile(samples.join(''));
|
||||||
|
|
@ -343,35 +354,63 @@
|
||||||
showStatus(1, 'Generating scrollback test...');
|
showStatus(1, 'Generating scrollback test...');
|
||||||
terminal1.clear();
|
terminal1.clear();
|
||||||
|
|
||||||
// Generate lots of output to test scrollback - enough to definitely overflow
|
// Generate lots of output to test scrollback
|
||||||
for (let i = 1; i <= 100; i++) {
|
for (let i = 1; i <= 100; i++) {
|
||||||
const colors = [31, 32, 33, 34, 35, 36]; // red, green, yellow, blue, magenta, cyan
|
const colors = [31, 32, 33, 34, 35, 36];
|
||||||
const color = colors[i % colors.length];
|
const color = colors[i % colors.length];
|
||||||
const line = `\x1b[1;${color}m[${i.toString().padStart(3, '0')}]\x1b[0m Line ${i}: This is test content with \x1b[4munderline\x1b[0m and \x1b[1mbold\x1b[0m text to test scrollback!\n`;
|
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(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a final message
|
terminal1.processOutput('\x1b[1;37m=== End of XTerm scrollback test ===\x1b[0m\n');
|
||||||
terminal1.processOutput('\x1b[1;37m=== End of scrollback test ===\x1b[0m\n');
|
|
||||||
terminal1.processOutput('\x1b[32m$\x1b[0m \x1b[7m \x1b[0m');
|
terminal1.processOutput('\x1b[32m$\x1b[0m \x1b[7m \x1b[0m');
|
||||||
|
|
||||||
showStatus(1, 'Generated 100 lines! Scroll up to see the full history.');
|
showStatus(1, 'Generated 100 lines! XTerm.js handles scrollback smoothly.');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error generating scrollback test:', error);
|
console.error('Error generating scrollback test:', error);
|
||||||
showStatus(1, `Error generating scrollback test: ${error.message}`, true);
|
showStatus(1, `Error generating scrollback test: ${error.message}`, true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Test the renderer with some sample ANSI sequences on load
|
window.runComparisonTest = function() {
|
||||||
setTimeout(() => {
|
const testData = [
|
||||||
const testSequences = [
|
'\x1b[2J\x1b[H', // Clear and home
|
||||||
'\x1b[1;31m♦ \x1b[1;32m♦ \x1b[1;33m♦ \x1b[1;34m♦ \x1b[1;35m♦ \x1b[1;36m♦ \x1b[0m\n',
|
'\x1b[1;31m=== Renderer Comparison Test ===\x1b[0m\n',
|
||||||
'\x1b[1mBold\x1b[0m \x1b[3mItalic\x1b[0m \x1b[4mUnderline\x1b[0m \x1b[9mStrikethrough\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[7mInverse\x1b[0m \x1b[38;2;255;100;50mRGB Color\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',
|
||||||
'Terminal Renderer Test Ready!\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'
|
||||||
];
|
];
|
||||||
|
|
||||||
testSequences.forEach(seq => terminal3.processOutput(seq));
|
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);
|
}, 100);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
|
@ -1,419 +0,0 @@
|
||||||
<!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 asciinema 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 { XTermRenderer, Renderer } from '../bundle/renderer.js';
|
|
||||||
|
|
||||||
// Initialize XTerm terminals
|
|
||||||
const terminal1 = new XTermRenderer(document.getElementById('terminal1'));
|
|
||||||
const terminal2 = new XTermRenderer(document.getElementById('terminal2'));
|
|
||||||
const terminal3 = new XTermRenderer(document.getElementById('terminal3'));
|
|
||||||
const terminal5 = new XTermRenderer(document.getElementById('terminal5'));
|
|
||||||
|
|
||||||
// Initialize custom renderer for comparison
|
|
||||||
const terminal4 = new Renderer(document.getElementById('terminal4'));
|
|
||||||
|
|
||||||
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>
|
|
||||||
|
|
@ -1,6 +1,2 @@
|
||||||
// Entry point for renderer bundle - exports both renderers for tests
|
// Entry point for renderer bundle - exports XTerm-based renderer
|
||||||
export { TerminalRenderer } from './renderer.js';
|
export { Renderer } from './renderer.js';
|
||||||
export { XTermRenderer } from './xterm-renderer.js';
|
|
||||||
|
|
||||||
// Also export with shorter alias for convenience
|
|
||||||
export { TerminalRenderer as Renderer } from './renderer.js';
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
// Terminal renderer for asciinema cast format with DOM rendering
|
// Terminal renderer for asciinema cast format using XTerm.js
|
||||||
// Supports complete cast files and streaming events
|
// Professional-grade terminal emulation with full VT compatibility
|
||||||
|
|
||||||
|
import { Terminal } from '@xterm/xterm';
|
||||||
|
import { FitAddon } from '@xterm/addon-fit';
|
||||||
|
import { WebLinksAddon } from '@xterm/addon-web-links';
|
||||||
|
|
||||||
interface CastHeader {
|
interface CastHeader {
|
||||||
version: number;
|
version: number;
|
||||||
|
|
@ -15,553 +19,92 @@ interface CastEvent {
|
||||||
data: string;
|
data: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TerminalCell {
|
export class Renderer {
|
||||||
char: string;
|
|
||||||
fg: string;
|
|
||||||
bg: string;
|
|
||||||
bold: boolean;
|
|
||||||
italic: boolean;
|
|
||||||
underline: boolean;
|
|
||||||
strikethrough: boolean;
|
|
||||||
inverse: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TerminalState {
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
cursorX: number;
|
|
||||||
cursorY: number;
|
|
||||||
currentFg: string;
|
|
||||||
currentBg: string;
|
|
||||||
bold: boolean;
|
|
||||||
italic: boolean;
|
|
||||||
underline: boolean;
|
|
||||||
strikethrough: boolean;
|
|
||||||
inverse: boolean;
|
|
||||||
alternateScreen: boolean;
|
|
||||||
scrollRegionTop: number;
|
|
||||||
scrollRegionBottom: number;
|
|
||||||
originMode: boolean;
|
|
||||||
autowrap: boolean;
|
|
||||||
insertMode: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TerminalRenderer {
|
|
||||||
private container: HTMLElement;
|
private container: HTMLElement;
|
||||||
private state: TerminalState;
|
private terminal: Terminal;
|
||||||
private primaryBuffer: TerminalCell[][];
|
private fitAddon: FitAddon;
|
||||||
private alternateBuffer: TerminalCell[][];
|
private webLinksAddon: WebLinksAddon;
|
||||||
private scrollbackBuffer: TerminalCell[][];
|
|
||||||
private maxScrollback: number = 1000;
|
|
||||||
private ansiColorMap: string[] = [
|
|
||||||
'#000000', '#cc241d', '#98971a', '#d79921', // Standard colors (0-7) - brighter
|
|
||||||
'#458588', '#b16286', '#689d6a', '#a89984',
|
|
||||||
'#928374', '#fb4934', '#b8bb26', '#fabd2f', // Bright colors (8-15) - very bright
|
|
||||||
'#83a598', '#d3869b', '#8ec07c', '#ebdbb2'
|
|
||||||
];
|
|
||||||
|
|
||||||
constructor(container: HTMLElement, width: number = 80, height: number = 20) {
|
constructor(container: HTMLElement, width: number = 80, height: number = 20) {
|
||||||
this.container = container;
|
this.container = container;
|
||||||
this.state = {
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
cursorX: 0,
|
|
||||||
cursorY: 0,
|
|
||||||
currentFg: '#ffffff',
|
|
||||||
currentBg: '#000000',
|
|
||||||
bold: false,
|
|
||||||
italic: false,
|
|
||||||
underline: false,
|
|
||||||
strikethrough: false,
|
|
||||||
inverse: false,
|
|
||||||
alternateScreen: false,
|
|
||||||
scrollRegionTop: 0,
|
|
||||||
scrollRegionBottom: height - 1,
|
|
||||||
originMode: false,
|
|
||||||
autowrap: true,
|
|
||||||
insertMode: false
|
|
||||||
};
|
|
||||||
|
|
||||||
this.primaryBuffer = this.createBuffer(width, height);
|
// Create terminal with options similar to the custom renderer
|
||||||
this.alternateBuffer = this.createBuffer(width, height);
|
this.terminal = new Terminal({
|
||||||
this.scrollbackBuffer = [];
|
cols: width,
|
||||||
|
rows: height,
|
||||||
|
fontFamily: 'Monaco, "Lucida Console", monospace',
|
||||||
|
fontSize: 14,
|
||||||
|
lineHeight: 1.2,
|
||||||
|
theme: {
|
||||||
|
background: '#000000',
|
||||||
|
foreground: '#ffffff',
|
||||||
|
cursor: '#ffffff',
|
||||||
|
cursorAccent: '#000000',
|
||||||
|
selectionBackground: '#ffffff30',
|
||||||
|
// Standard ANSI colors (matching the custom renderer)
|
||||||
|
black: '#000000',
|
||||||
|
red: '#cc241d',
|
||||||
|
green: '#98971a',
|
||||||
|
yellow: '#d79921',
|
||||||
|
blue: '#458588',
|
||||||
|
magenta: '#b16286',
|
||||||
|
cyan: '#689d6a',
|
||||||
|
white: '#a89984',
|
||||||
|
// Bright ANSI colors
|
||||||
|
brightBlack: '#928374',
|
||||||
|
brightRed: '#fb4934',
|
||||||
|
brightGreen: '#b8bb26',
|
||||||
|
brightYellow: '#fabd2f',
|
||||||
|
brightBlue: '#83a598',
|
||||||
|
brightMagenta: '#d3869b',
|
||||||
|
brightCyan: '#8ec07c',
|
||||||
|
brightWhite: '#ebdbb2'
|
||||||
|
},
|
||||||
|
allowProposedApi: true,
|
||||||
|
scrollback: 1000,
|
||||||
|
convertEol: true,
|
||||||
|
altClickMovesCursor: false,
|
||||||
|
rightClickSelectsWord: false,
|
||||||
|
disableStdin: true // We handle input separately
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add addons
|
||||||
|
this.fitAddon = new FitAddon();
|
||||||
|
this.webLinksAddon = new WebLinksAddon();
|
||||||
|
|
||||||
|
this.terminal.loadAddon(this.fitAddon);
|
||||||
|
this.terminal.loadAddon(this.webLinksAddon);
|
||||||
|
|
||||||
this.setupDOM();
|
this.setupDOM();
|
||||||
}
|
}
|
||||||
|
|
||||||
private createBuffer(width: number, height: number): TerminalCell[][] {
|
|
||||||
const buffer: TerminalCell[][] = [];
|
|
||||||
for (let y = 0; y < height; y++) {
|
|
||||||
buffer[y] = [];
|
|
||||||
for (let x = 0; x < width; x++) {
|
|
||||||
buffer[y][x] = {
|
|
||||||
char: ' ',
|
|
||||||
fg: '#ffffff',
|
|
||||||
bg: '#000000',
|
|
||||||
bold: false,
|
|
||||||
italic: false,
|
|
||||||
underline: false,
|
|
||||||
strikethrough: false,
|
|
||||||
inverse: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupDOM(): void {
|
private setupDOM(): void {
|
||||||
this.container.style.fontFamily = 'Monaco, "Lucida Console", monospace';
|
// Clear container and add CSS
|
||||||
this.container.style.fontSize = '14px';
|
|
||||||
this.container.style.lineHeight = '1.2';
|
|
||||||
this.container.style.backgroundColor = '#000000';
|
|
||||||
this.container.style.color = '#ffffff';
|
|
||||||
this.container.style.padding = '10px';
|
|
||||||
this.container.style.overflow = 'auto';
|
|
||||||
this.container.style.whiteSpace = 'pre';
|
|
||||||
this.container.innerHTML = '';
|
this.container.innerHTML = '';
|
||||||
|
this.container.style.padding = '10px';
|
||||||
|
this.container.style.backgroundColor = '#000000';
|
||||||
|
this.container.style.overflow = 'hidden';
|
||||||
|
|
||||||
|
// Create terminal wrapper
|
||||||
|
const terminalWrapper = document.createElement('div');
|
||||||
|
terminalWrapper.style.width = '100%';
|
||||||
|
terminalWrapper.style.height = '100%';
|
||||||
|
this.container.appendChild(terminalWrapper);
|
||||||
|
|
||||||
|
// Open terminal in the wrapper
|
||||||
|
this.terminal.open(terminalWrapper);
|
||||||
|
|
||||||
|
// Fit terminal to container
|
||||||
|
this.fitAddon.fit();
|
||||||
|
|
||||||
|
// Handle container resize
|
||||||
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
this.fitAddon.fit();
|
||||||
|
});
|
||||||
|
resizeObserver.observe(this.container);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCurrentBuffer(): TerminalCell[][] {
|
// Public API methods - maintain compatibility with custom renderer
|
||||||
return this.state.alternateScreen ? this.alternateBuffer : this.primaryBuffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderBuffer(): void {
|
|
||||||
const buffer = this.getCurrentBuffer();
|
|
||||||
const allLines: string[] = [];
|
|
||||||
|
|
||||||
// Render scrollback buffer first (only for primary screen)
|
|
||||||
if (!this.state.alternateScreen && this.scrollbackBuffer.length > 0) {
|
|
||||||
for (let i = 0; i < this.scrollbackBuffer.length; i++) {
|
|
||||||
const line = this.renderLine(this.scrollbackBuffer[i]);
|
|
||||||
allLines.push(`<div class="scrollback-line">${line}</div>`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render current buffer
|
|
||||||
for (let y = 0; y < this.state.height; y++) {
|
|
||||||
const line = this.renderLine(buffer[y]);
|
|
||||||
const isCurrentLine = y === this.state.cursorY;
|
|
||||||
allLines.push(`<div class="terminal-line ${isCurrentLine ? 'current-line' : ''}">${line}</div>`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.container.innerHTML = allLines.join('');
|
|
||||||
|
|
||||||
// Auto-scroll to bottom unless user has scrolled up
|
|
||||||
if (this.container.scrollTop + this.container.clientHeight >= this.container.scrollHeight - 10) {
|
|
||||||
this.container.scrollTop = this.container.scrollHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderLine(lineBuffer: TerminalCell[]): string {
|
|
||||||
let line = '';
|
|
||||||
let lastBg = '';
|
|
||||||
let lastFg = '';
|
|
||||||
let lastStyles = '';
|
|
||||||
let spanOpen = false;
|
|
||||||
|
|
||||||
for (let x = 0; x < lineBuffer.length; x++) {
|
|
||||||
const cell = lineBuffer[x];
|
|
||||||
const fg = cell.inverse ? cell.bg : cell.fg;
|
|
||||||
const bg = cell.inverse ? cell.fg : cell.bg;
|
|
||||||
|
|
||||||
let styles = '';
|
|
||||||
if (cell.bold) styles += 'font-weight: bold; ';
|
|
||||||
if (cell.italic) styles += 'font-style: italic; ';
|
|
||||||
if (cell.underline) styles += 'text-decoration: underline; ';
|
|
||||||
if (cell.strikethrough) styles += 'text-decoration: line-through; ';
|
|
||||||
|
|
||||||
if (fg !== lastFg || bg !== lastBg || styles !== lastStyles) {
|
|
||||||
if (spanOpen) {
|
|
||||||
line += '</span>';
|
|
||||||
spanOpen = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always add span for consistent rendering
|
|
||||||
line += `<span style="color: ${fg}; background-color: ${bg}; ${styles}">`;
|
|
||||||
spanOpen = true;
|
|
||||||
|
|
||||||
lastFg = fg;
|
|
||||||
lastBg = bg;
|
|
||||||
lastStyles = styles;
|
|
||||||
}
|
|
||||||
|
|
||||||
const char = cell.char || ' ';
|
|
||||||
line += char === ' ' ? ' ' : this.escapeHtml(char);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close any open span
|
|
||||||
if (spanOpen) {
|
|
||||||
line += '</span>';
|
|
||||||
}
|
|
||||||
|
|
||||||
return line || ' '; // Ensure empty lines have height
|
|
||||||
}
|
|
||||||
|
|
||||||
private escapeHtml(text: string): string {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.textContent = text;
|
|
||||||
return div.innerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
private parseAnsiSequence(data: string): void {
|
|
||||||
let i = 0;
|
|
||||||
while (i < data.length) {
|
|
||||||
const char = data[i];
|
|
||||||
|
|
||||||
if (char === '\x1b' && i + 1 < data.length && data[i + 1] === '[') {
|
|
||||||
// CSI sequence
|
|
||||||
i += 2;
|
|
||||||
let params = '';
|
|
||||||
let finalChar = '';
|
|
||||||
|
|
||||||
while (i < data.length) {
|
|
||||||
const c = data[i];
|
|
||||||
if ((c >= '0' && c <= '9') || c === ';' || c === ':' || c === '?') {
|
|
||||||
params += c;
|
|
||||||
} else {
|
|
||||||
finalChar = c;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debug problematic sequences
|
|
||||||
if (params.includes('2004')) {
|
|
||||||
console.log(`CSI sequence: ESC[${params}${finalChar}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.handleCSI(params, finalChar);
|
|
||||||
} else if (char === '\x1b' && i + 1 < data.length && data[i + 1] === ']') {
|
|
||||||
// OSC sequence - skip for now
|
|
||||||
i += 2;
|
|
||||||
while (i < data.length && data[i] !== '\x07' && data[i] !== '\x1b') {
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
if (i < data.length && data[i] === '\x1b' && i + 1 < data.length && data[i + 1] === '\\') {
|
|
||||||
i++; // Skip the backslash too
|
|
||||||
}
|
|
||||||
} else if (char === '\x1b' && i + 1 < data.length && data[i + 1] === '=') {
|
|
||||||
// Application keypad mode - skip
|
|
||||||
i++;
|
|
||||||
} else if (char === '\x1b' && i + 1 < data.length && data[i + 1] === '>') {
|
|
||||||
// Normal keypad mode - skip
|
|
||||||
i++;
|
|
||||||
} else if (char === '\r') {
|
|
||||||
this.state.cursorX = 0;
|
|
||||||
} else if (char === '\n') {
|
|
||||||
this.newline();
|
|
||||||
} else if (char === '\t') {
|
|
||||||
this.state.cursorX = Math.min(this.state.width - 1, (Math.floor(this.state.cursorX / 8) + 1) * 8);
|
|
||||||
} else if (char === '\b') {
|
|
||||||
if (this.state.cursorX > 0) {
|
|
||||||
this.state.cursorX--;
|
|
||||||
}
|
|
||||||
} else if (char >= ' ' || char === '\x00') {
|
|
||||||
this.writeChar(char === '\x00' ? ' ' : char);
|
|
||||||
}
|
|
||||||
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleCSI(params: string, finalChar: string): void {
|
|
||||||
const paramList = params ? params.split(';').map(p => parseInt(p) || 0) : [0];
|
|
||||||
|
|
||||||
switch (finalChar) {
|
|
||||||
case 'A': // Cursor Up
|
|
||||||
this.state.cursorY = Math.max(this.state.scrollRegionTop, this.state.cursorY - (paramList[0] || 1));
|
|
||||||
break;
|
|
||||||
case 'B': // Cursor Down
|
|
||||||
this.state.cursorY = Math.min(this.state.scrollRegionBottom, this.state.cursorY + (paramList[0] || 1));
|
|
||||||
break;
|
|
||||||
case 'C': // Cursor Forward
|
|
||||||
this.state.cursorX = Math.min(this.state.width - 1, this.state.cursorX + (paramList[0] || 1));
|
|
||||||
break;
|
|
||||||
case 'D': // Cursor Backward
|
|
||||||
this.state.cursorX = Math.max(0, this.state.cursorX - (paramList[0] || 1));
|
|
||||||
break;
|
|
||||||
case 'H': // Cursor Position
|
|
||||||
case 'f':
|
|
||||||
this.state.cursorY = Math.min(this.state.height - 1, Math.max(0, (paramList[0] || 1) - 1));
|
|
||||||
this.state.cursorX = Math.min(this.state.width - 1, Math.max(0, (paramList[1] || 1) - 1));
|
|
||||||
break;
|
|
||||||
case 'J': // Erase Display
|
|
||||||
this.eraseDisplay(paramList[0] || 0);
|
|
||||||
break;
|
|
||||||
case 'K': // Erase Line
|
|
||||||
this.eraseLine(paramList[0] || 0);
|
|
||||||
break;
|
|
||||||
case 'm': // Set Graphics Rendition
|
|
||||||
this.handleSGR(paramList);
|
|
||||||
break;
|
|
||||||
case 'r': // Set Scroll Region
|
|
||||||
this.state.scrollRegionTop = Math.max(0, (paramList[0] || 1) - 1);
|
|
||||||
this.state.scrollRegionBottom = Math.min(this.state.height - 1, (paramList[1] || this.state.height) - 1);
|
|
||||||
this.state.cursorX = 0;
|
|
||||||
this.state.cursorY = this.state.scrollRegionTop;
|
|
||||||
break;
|
|
||||||
case 's': // Save Cursor Position
|
|
||||||
// TODO: Implement cursor save/restore
|
|
||||||
break;
|
|
||||||
case 'u': // Restore Cursor Position
|
|
||||||
// TODO: Implement cursor save/restore
|
|
||||||
break;
|
|
||||||
case 'h': // Set Mode
|
|
||||||
if (params === '?1049' || params === '?47') {
|
|
||||||
this.state.alternateScreen = true;
|
|
||||||
} else if (params === '?2004') {
|
|
||||||
// Bracketed paste mode - ignore (should not display)
|
|
||||||
console.log('Bracketed paste mode enabled');
|
|
||||||
} else if (params === '?1') {
|
|
||||||
// Application cursor keys mode
|
|
||||||
} else {
|
|
||||||
console.log(`Unhandled set mode: ${params}h`);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'l': // Reset Mode
|
|
||||||
if (params === '?1049' || params === '?47') {
|
|
||||||
this.state.alternateScreen = false;
|
|
||||||
} else if (params === '?2004') {
|
|
||||||
// Bracketed paste mode - ignore (should not display)
|
|
||||||
console.log('Bracketed paste mode disabled');
|
|
||||||
} else if (params === '?1') {
|
|
||||||
// Normal cursor keys mode
|
|
||||||
} else {
|
|
||||||
console.log(`Unhandled reset mode: ${params}l`);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleSGR(params: number[]): void {
|
|
||||||
for (let i = 0; i < params.length; i++) {
|
|
||||||
const param = params[i];
|
|
||||||
|
|
||||||
if (param === 0) {
|
|
||||||
// Reset
|
|
||||||
this.state.currentFg = '#ffffff';
|
|
||||||
this.state.currentBg = '#000000';
|
|
||||||
this.state.bold = false;
|
|
||||||
this.state.italic = false;
|
|
||||||
this.state.underline = false;
|
|
||||||
this.state.strikethrough = false;
|
|
||||||
this.state.inverse = false;
|
|
||||||
} else if (param === 1) {
|
|
||||||
this.state.bold = true;
|
|
||||||
} else if (param === 3) {
|
|
||||||
this.state.italic = true;
|
|
||||||
} else if (param === 4) {
|
|
||||||
this.state.underline = true;
|
|
||||||
} else if (param === 7) {
|
|
||||||
this.state.inverse = true;
|
|
||||||
} else if (param === 9) {
|
|
||||||
this.state.strikethrough = true;
|
|
||||||
} else if (param === 22) {
|
|
||||||
this.state.bold = false;
|
|
||||||
} else if (param === 23) {
|
|
||||||
this.state.italic = false;
|
|
||||||
} else if (param === 24) {
|
|
||||||
this.state.underline = false;
|
|
||||||
} else if (param === 27) {
|
|
||||||
this.state.inverse = false;
|
|
||||||
} else if (param === 29) {
|
|
||||||
this.state.strikethrough = false;
|
|
||||||
} else if (param === 39) {
|
|
||||||
// Default foreground color
|
|
||||||
this.state.currentFg = '#ffffff';
|
|
||||||
} else if (param === 49) {
|
|
||||||
// Default background color
|
|
||||||
this.state.currentBg = '#000000';
|
|
||||||
} else if (param >= 30 && param <= 37) {
|
|
||||||
// Standard foreground colors
|
|
||||||
this.state.currentFg = this.ansiColorMap[param - 30];
|
|
||||||
} else if (param >= 40 && param <= 47) {
|
|
||||||
// Standard background colors
|
|
||||||
this.state.currentBg = this.ansiColorMap[param - 40];
|
|
||||||
} else if (param >= 90 && param <= 97) {
|
|
||||||
// Bright foreground colors
|
|
||||||
this.state.currentFg = this.ansiColorMap[param - 90 + 8];
|
|
||||||
} else if (param >= 100 && param <= 107) {
|
|
||||||
// Bright background colors
|
|
||||||
this.state.currentBg = this.ansiColorMap[param - 100 + 8];
|
|
||||||
} else if (param === 38) {
|
|
||||||
// Extended foreground color
|
|
||||||
if (i + 1 < params.length && params[i + 1] === 2 && i + 4 < params.length) {
|
|
||||||
// RGB: 38;2;r;g;b
|
|
||||||
const r = params[i + 2];
|
|
||||||
const g = params[i + 3];
|
|
||||||
const b = params[i + 4];
|
|
||||||
this.state.currentFg = `rgb(${r},${g},${b})`;
|
|
||||||
i += 4;
|
|
||||||
} else if (i + 1 < params.length && params[i + 1] === 5 && i + 2 < params.length) {
|
|
||||||
// 256-color: 38;5;n
|
|
||||||
this.state.currentFg = this.get256Color(params[i + 2]);
|
|
||||||
i += 2;
|
|
||||||
}
|
|
||||||
} else if (param === 48) {
|
|
||||||
// Extended background color
|
|
||||||
if (i + 1 < params.length && params[i + 1] === 2 && i + 4 < params.length) {
|
|
||||||
// RGB: 48;2;r;g;b
|
|
||||||
const r = params[i + 2];
|
|
||||||
const g = params[i + 3];
|
|
||||||
const b = params[i + 4];
|
|
||||||
this.state.currentBg = `rgb(${r},${g},${b})`;
|
|
||||||
i += 4;
|
|
||||||
} else if (i + 1 < params.length && params[i + 1] === 5 && i + 2 < params.length) {
|
|
||||||
// 256-color: 48;5;n
|
|
||||||
this.state.currentBg = this.get256Color(params[i + 2]);
|
|
||||||
i += 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private get256Color(index: number): string {
|
|
||||||
if (index < 16) {
|
|
||||||
return this.ansiColorMap[index];
|
|
||||||
} else if (index < 232) {
|
|
||||||
// 216 color cube
|
|
||||||
const n = index - 16;
|
|
||||||
const r = Math.floor(n / 36);
|
|
||||||
const g = Math.floor((n % 36) / 6);
|
|
||||||
const b = n % 6;
|
|
||||||
|
|
||||||
const values = [0, 95, 135, 175, 215, 255];
|
|
||||||
return `rgb(${values[r]},${values[g]},${values[b]})`;
|
|
||||||
} else {
|
|
||||||
// Grayscale
|
|
||||||
const gray = 8 + (index - 232) * 10;
|
|
||||||
return `rgb(${gray},${gray},${gray})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private eraseDisplay(mode: number): void {
|
|
||||||
const buffer = this.getCurrentBuffer();
|
|
||||||
|
|
||||||
switch (mode) {
|
|
||||||
case 0: // Erase from cursor to end of screen
|
|
||||||
this.eraseLine(0);
|
|
||||||
for (let y = this.state.cursorY + 1; y < this.state.height; y++) {
|
|
||||||
for (let x = 0; x < this.state.width; x++) {
|
|
||||||
buffer[y][x] = this.createEmptyCell();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 1: // Erase from beginning of screen to cursor
|
|
||||||
for (let y = 0; y < this.state.cursorY; y++) {
|
|
||||||
for (let x = 0; x < this.state.width; x++) {
|
|
||||||
buffer[y][x] = this.createEmptyCell();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.eraseLine(1);
|
|
||||||
break;
|
|
||||||
case 2: // Erase entire screen
|
|
||||||
case 3: // Erase entire screen and scrollback
|
|
||||||
for (let y = 0; y < this.state.height; y++) {
|
|
||||||
for (let x = 0; x < this.state.width; x++) {
|
|
||||||
buffer[y][x] = this.createEmptyCell();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (mode === 3) {
|
|
||||||
this.scrollbackBuffer = [];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private eraseLine(mode: number): void {
|
|
||||||
const buffer = this.getCurrentBuffer();
|
|
||||||
const y = this.state.cursorY;
|
|
||||||
|
|
||||||
switch (mode) {
|
|
||||||
case 0: // Erase from cursor to end of line
|
|
||||||
for (let x = this.state.cursorX; x < this.state.width; x++) {
|
|
||||||
buffer[y][x] = this.createEmptyCell();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 1: // Erase from beginning of line to cursor
|
|
||||||
for (let x = 0; x <= this.state.cursorX; x++) {
|
|
||||||
buffer[y][x] = this.createEmptyCell();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 2: // Erase entire line
|
|
||||||
for (let x = 0; x < this.state.width; x++) {
|
|
||||||
buffer[y][x] = this.createEmptyCell();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private createEmptyCell(): TerminalCell {
|
|
||||||
return {
|
|
||||||
char: ' ',
|
|
||||||
fg: this.state.currentFg,
|
|
||||||
bg: this.state.currentBg,
|
|
||||||
bold: false,
|
|
||||||
italic: false,
|
|
||||||
underline: false,
|
|
||||||
strikethrough: false,
|
|
||||||
inverse: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private writeChar(char: string): void {
|
|
||||||
const buffer = this.getCurrentBuffer();
|
|
||||||
|
|
||||||
if (this.state.cursorX >= this.state.width) {
|
|
||||||
if (this.state.autowrap) {
|
|
||||||
this.newline();
|
|
||||||
} else {
|
|
||||||
this.state.cursorX = this.state.width - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer[this.state.cursorY][this.state.cursorX] = {
|
|
||||||
char,
|
|
||||||
fg: this.state.currentFg,
|
|
||||||
bg: this.state.currentBg,
|
|
||||||
bold: this.state.bold,
|
|
||||||
italic: this.state.italic,
|
|
||||||
underline: this.state.underline,
|
|
||||||
strikethrough: this.state.strikethrough,
|
|
||||||
inverse: this.state.inverse
|
|
||||||
};
|
|
||||||
|
|
||||||
this.state.cursorX++;
|
|
||||||
}
|
|
||||||
|
|
||||||
private newline(): void {
|
|
||||||
this.state.cursorX = 0;
|
|
||||||
if (this.state.cursorY >= this.state.scrollRegionBottom) {
|
|
||||||
this.scrollUp();
|
|
||||||
} else {
|
|
||||||
this.state.cursorY++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private scrollUp(): void {
|
|
||||||
const buffer = this.getCurrentBuffer();
|
|
||||||
|
|
||||||
// Add the top line to scrollback if we're in primary buffer
|
|
||||||
if (!this.state.alternateScreen) {
|
|
||||||
this.scrollbackBuffer.push([...buffer[this.state.scrollRegionTop]]);
|
|
||||||
if (this.scrollbackBuffer.length > this.maxScrollback) {
|
|
||||||
this.scrollbackBuffer.shift();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scroll the region
|
|
||||||
for (let y = this.state.scrollRegionTop; y < this.state.scrollRegionBottom; y++) {
|
|
||||||
buffer[y] = [...buffer[y + 1]];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear the bottom line
|
|
||||||
for (let x = 0; x < this.state.width; x++) {
|
|
||||||
buffer[this.state.scrollRegionBottom][x] = this.createEmptyCell();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Public API methods
|
|
||||||
|
|
||||||
async loadCastFile(url: string): Promise<void> {
|
async loadCastFile(url: string): Promise<void> {
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
|
|
@ -573,6 +116,9 @@ export class TerminalRenderer {
|
||||||
const lines = content.trim().split('\n');
|
const lines = content.trim().split('\n');
|
||||||
let header: CastHeader | null = null;
|
let header: CastHeader | null = null;
|
||||||
|
|
||||||
|
// Clear terminal
|
||||||
|
this.terminal.clear();
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (!line.trim()) continue;
|
if (!line.trim()) continue;
|
||||||
|
|
||||||
|
|
@ -599,48 +145,38 @@ export class TerminalRenderer {
|
||||||
console.warn('Failed to parse cast line:', line);
|
console.warn('Failed to parse cast line:', line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.renderBuffer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
processOutput(data: string): void {
|
processOutput(data: string): void {
|
||||||
this.parseAnsiSequence(data);
|
// XTerm handles all ANSI escape sequences automatically
|
||||||
this.renderBuffer();
|
this.terminal.write(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
processEvent(event: CastEvent): void {
|
processEvent(event: CastEvent): void {
|
||||||
if (event.type === 'o') {
|
if (event.type === 'o') {
|
||||||
this.processOutput(event.data);
|
this.processOutput(event.data);
|
||||||
this.renderBuffer();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resize(width: number, height: number): void {
|
resize(width: number, height: number): void {
|
||||||
this.state.width = width;
|
this.terminal.resize(width, height);
|
||||||
this.state.height = height;
|
// Fit addon will handle the visual resize
|
||||||
this.state.scrollRegionBottom = height - 1;
|
setTimeout(() => {
|
||||||
|
this.fitAddon.fit();
|
||||||
this.primaryBuffer = this.createBuffer(width, height);
|
}, 0);
|
||||||
this.alternateBuffer = this.createBuffer(width, height);
|
|
||||||
|
|
||||||
this.state.cursorX = 0;
|
|
||||||
this.state.cursorY = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clear(): void {
|
clear(): void {
|
||||||
this.primaryBuffer = this.createBuffer(this.state.width, this.state.height);
|
this.terminal.clear();
|
||||||
this.alternateBuffer = this.createBuffer(this.state.width, this.state.height);
|
|
||||||
this.scrollbackBuffer = [];
|
|
||||||
this.state.cursorX = 0;
|
|
||||||
this.state.cursorY = 0;
|
|
||||||
this.state.alternateScreen = false;
|
|
||||||
this.renderBuffer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stream support - connect to SSE endpoint
|
// Stream support - connect to SSE endpoint
|
||||||
connectToStream(sessionId: string): EventSource {
|
connectToStream(sessionId: string): EventSource {
|
||||||
const eventSource = new EventSource(`/api/sessions/${sessionId}/stream`);
|
const eventSource = new EventSource(`/api/sessions/${sessionId}/stream`);
|
||||||
|
|
||||||
|
// Clear terminal when starting stream
|
||||||
|
this.terminal.clear();
|
||||||
|
|
||||||
eventSource.onmessage = (event) => {
|
eventSource.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
|
|
@ -668,4 +204,55 @@ export class TerminalRenderer {
|
||||||
|
|
||||||
return eventSource;
|
return eventSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Additional methods for terminal control
|
||||||
|
|
||||||
|
focus(): void {
|
||||||
|
this.terminal.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
blur(): void {
|
||||||
|
this.terminal.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
getTerminal(): Terminal {
|
||||||
|
return this.terminal;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose(): void {
|
||||||
|
this.terminal.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to fit terminal to container (useful for responsive layouts)
|
||||||
|
fit(): void {
|
||||||
|
this.fitAddon.fit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get terminal dimensions
|
||||||
|
getDimensions(): { cols: number; rows: number } {
|
||||||
|
return {
|
||||||
|
cols: this.terminal.cols,
|
||||||
|
rows: this.terminal.rows
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write raw data to terminal (useful for testing)
|
||||||
|
write(data: string): void {
|
||||||
|
this.terminal.write(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable/disable input (though we keep it disabled by default)
|
||||||
|
setInputEnabled(enabled: boolean): void {
|
||||||
|
// XTerm doesn't have a direct way to disable input, so we override onData
|
||||||
|
if (enabled) {
|
||||||
|
// Remove any existing handler first
|
||||||
|
this.terminal.onData(() => {
|
||||||
|
// Input is handled by the session component
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.terminal.onData(() => {
|
||||||
|
// Do nothing - input disabled
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,258 +0,0 @@
|
||||||
// XTerm-based terminal renderer for asciinema cast format
|
|
||||||
// Provides the same interface as the custom renderer but uses xterm.js
|
|
||||||
|
|
||||||
import { Terminal } from '@xterm/xterm';
|
|
||||||
import { FitAddon } from '@xterm/addon-fit';
|
|
||||||
import { WebLinksAddon } from '@xterm/addon-web-links';
|
|
||||||
|
|
||||||
interface CastHeader {
|
|
||||||
version: number;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
timestamp?: number;
|
|
||||||
env?: Record<string, string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CastEvent {
|
|
||||||
timestamp: number;
|
|
||||||
type: 'o' | 'i'; // output or input
|
|
||||||
data: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class XTermRenderer {
|
|
||||||
private container: HTMLElement;
|
|
||||||
private terminal: Terminal;
|
|
||||||
private fitAddon: FitAddon;
|
|
||||||
private webLinksAddon: WebLinksAddon;
|
|
||||||
|
|
||||||
constructor(container: HTMLElement, width: number = 80, height: number = 20) {
|
|
||||||
this.container = container;
|
|
||||||
|
|
||||||
// Create terminal with options similar to the custom renderer
|
|
||||||
this.terminal = new Terminal({
|
|
||||||
cols: width,
|
|
||||||
rows: height,
|
|
||||||
fontFamily: 'Monaco, "Lucida Console", monospace',
|
|
||||||
fontSize: 14,
|
|
||||||
lineHeight: 1.2,
|
|
||||||
theme: {
|
|
||||||
background: '#000000',
|
|
||||||
foreground: '#ffffff',
|
|
||||||
cursor: '#ffffff',
|
|
||||||
cursorAccent: '#000000',
|
|
||||||
selectionBackground: '#ffffff30',
|
|
||||||
// Standard ANSI colors (matching the custom renderer)
|
|
||||||
black: '#000000',
|
|
||||||
red: '#cc241d',
|
|
||||||
green: '#98971a',
|
|
||||||
yellow: '#d79921',
|
|
||||||
blue: '#458588',
|
|
||||||
magenta: '#b16286',
|
|
||||||
cyan: '#689d6a',
|
|
||||||
white: '#a89984',
|
|
||||||
// Bright ANSI colors
|
|
||||||
brightBlack: '#928374',
|
|
||||||
brightRed: '#fb4934',
|
|
||||||
brightGreen: '#b8bb26',
|
|
||||||
brightYellow: '#fabd2f',
|
|
||||||
brightBlue: '#83a598',
|
|
||||||
brightMagenta: '#d3869b',
|
|
||||||
brightCyan: '#8ec07c',
|
|
||||||
brightWhite: '#ebdbb2'
|
|
||||||
},
|
|
||||||
allowProposedApi: true,
|
|
||||||
scrollback: 1000,
|
|
||||||
convertEol: true,
|
|
||||||
altClickMovesCursor: false,
|
|
||||||
rightClickSelectsWord: false,
|
|
||||||
disableStdin: true // We handle input separately
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add addons
|
|
||||||
this.fitAddon = new FitAddon();
|
|
||||||
this.webLinksAddon = new WebLinksAddon();
|
|
||||||
|
|
||||||
this.terminal.loadAddon(this.fitAddon);
|
|
||||||
this.terminal.loadAddon(this.webLinksAddon);
|
|
||||||
|
|
||||||
this.setupDOM();
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupDOM(): void {
|
|
||||||
// Clear container and add CSS
|
|
||||||
this.container.innerHTML = '';
|
|
||||||
this.container.style.padding = '10px';
|
|
||||||
this.container.style.backgroundColor = '#000000';
|
|
||||||
this.container.style.overflow = 'hidden';
|
|
||||||
|
|
||||||
// Create terminal wrapper
|
|
||||||
const terminalWrapper = document.createElement('div');
|
|
||||||
terminalWrapper.style.width = '100%';
|
|
||||||
terminalWrapper.style.height = '100%';
|
|
||||||
this.container.appendChild(terminalWrapper);
|
|
||||||
|
|
||||||
// Open terminal in the wrapper
|
|
||||||
this.terminal.open(terminalWrapper);
|
|
||||||
|
|
||||||
// Fit terminal to container
|
|
||||||
this.fitAddon.fit();
|
|
||||||
|
|
||||||
// Handle container resize
|
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
|
||||||
this.fitAddon.fit();
|
|
||||||
});
|
|
||||||
resizeObserver.observe(this.container);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Public API methods - maintain compatibility with custom renderer
|
|
||||||
|
|
||||||
async loadCastFile(url: string): Promise<void> {
|
|
||||||
const response = await fetch(url);
|
|
||||||
const text = await response.text();
|
|
||||||
this.parseCastFile(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
parseCastFile(content: string): void {
|
|
||||||
const lines = content.trim().split('\n');
|
|
||||||
let header: CastHeader | null = null;
|
|
||||||
|
|
||||||
// Clear terminal
|
|
||||||
this.terminal.clear();
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
if (!line.trim()) continue;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(line);
|
|
||||||
|
|
||||||
if (parsed.version && parsed.width && parsed.height) {
|
|
||||||
// Header
|
|
||||||
header = parsed;
|
|
||||||
this.resize(parsed.width, parsed.height);
|
|
||||||
} else if (Array.isArray(parsed) && parsed.length >= 3) {
|
|
||||||
// Event: [timestamp, type, data]
|
|
||||||
const event: CastEvent = {
|
|
||||||
timestamp: parsed[0],
|
|
||||||
type: parsed[1],
|
|
||||||
data: parsed[2]
|
|
||||||
};
|
|
||||||
|
|
||||||
if (event.type === 'o') {
|
|
||||||
this.processOutput(event.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Failed to parse cast line:', line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
processOutput(data: string): void {
|
|
||||||
// XTerm handles all ANSI escape sequences automatically
|
|
||||||
this.terminal.write(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
processEvent(event: CastEvent): void {
|
|
||||||
if (event.type === 'o') {
|
|
||||||
this.processOutput(event.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resize(width: number, height: number): void {
|
|
||||||
this.terminal.resize(width, height);
|
|
||||||
// Fit addon will handle the visual resize
|
|
||||||
setTimeout(() => {
|
|
||||||
this.fitAddon.fit();
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
clear(): void {
|
|
||||||
this.terminal.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stream support - connect to SSE endpoint
|
|
||||||
connectToStream(sessionId: string): EventSource {
|
|
||||||
const eventSource = new EventSource(`/api/sessions/${sessionId}/stream`);
|
|
||||||
|
|
||||||
// Clear terminal when starting stream
|
|
||||||
this.terminal.clear();
|
|
||||||
|
|
||||||
eventSource.onmessage = (event) => {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(event.data);
|
|
||||||
|
|
||||||
if (data.version && data.width && data.height) {
|
|
||||||
// Header
|
|
||||||
this.resize(data.width, data.height);
|
|
||||||
} else if (Array.isArray(data) && data.length >= 3) {
|
|
||||||
// Event
|
|
||||||
const castEvent: CastEvent = {
|
|
||||||
timestamp: data[0],
|
|
||||||
type: data[1],
|
|
||||||
data: data[2]
|
|
||||||
};
|
|
||||||
this.processEvent(castEvent);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Failed to parse stream event:', event.data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
eventSource.onerror = (error) => {
|
|
||||||
console.error('Stream error:', error);
|
|
||||||
};
|
|
||||||
|
|
||||||
return eventSource;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Additional methods for terminal control
|
|
||||||
|
|
||||||
focus(): void {
|
|
||||||
this.terminal.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
blur(): void {
|
|
||||||
this.terminal.blur();
|
|
||||||
}
|
|
||||||
|
|
||||||
getTerminal(): Terminal {
|
|
||||||
return this.terminal;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispose(): void {
|
|
||||||
this.terminal.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method to fit terminal to container (useful for responsive layouts)
|
|
||||||
fit(): void {
|
|
||||||
this.fitAddon.fit();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get terminal dimensions
|
|
||||||
getDimensions(): { cols: number; rows: number } {
|
|
||||||
return {
|
|
||||||
cols: this.terminal.cols,
|
|
||||||
rows: this.terminal.rows
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write raw data to terminal (useful for testing)
|
|
||||||
write(data: string): void {
|
|
||||||
this.terminal.write(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable/disable input (though we keep it disabled by default)
|
|
||||||
setInputEnabled(enabled: boolean): void {
|
|
||||||
// XTerm doesn't have a direct way to disable input, so we override onData
|
|
||||||
if (enabled) {
|
|
||||||
// Remove any existing handler first
|
|
||||||
this.terminal.onData(() => {
|
|
||||||
// Input is handled by the session component
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.terminal.onData(() => {
|
|
||||||
// Do nothing - input disabled
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue