mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-24 14:47:39 +00:00
Improve DOM terminal renderer and add rich test content
- Fix DOM terminal rendering with proper innerHTML generation - Add comprehensive test content including logs, tables, git history, docker status - Implement proper ANSI color code handling for wide character compatibility - Add auto-scroll to bottom functionality - Replace individual line element management with bulk innerHTML updates - Fix \r\n line ending handling for proper terminal behavior 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
b4d63fc922
commit
5ca8acb676
2 changed files with 280 additions and 92 deletions
|
|
@ -134,36 +134,260 @@
|
|||
|
||||
console.log('Generating mock data from HTML...');
|
||||
|
||||
const contentCols = 120;
|
||||
const contentRows = 40;
|
||||
const numPages = 10;
|
||||
let content = '';
|
||||
|
||||
let lineNumber = 1;
|
||||
let content = "";
|
||||
// Header
|
||||
content += '\x1b[1;31m╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗\r\n';
|
||||
content += '\x1b[1;32m║\x1b[1;33m 🌈 DOM TERMINAL SHOWCASE 🌈 \x1b[1;32m║\r\n';
|
||||
content += '\x1b[1;34m╚══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝\x1b[0m\r\n';
|
||||
content += '\r\n';
|
||||
|
||||
for (let page = 1; page <= numPages; page++) {
|
||||
// Page header
|
||||
const headerLine = '◄' + '='.repeat(contentCols - 2) + '►';
|
||||
content += (`\x1b[43m${headerLine}\x1b[0m\n`);
|
||||
lineNumber++;
|
||||
// Welcome message
|
||||
content += '\x1b[1;36mWelcome to the DOM Terminal Demo!\x1b[0m\r\n';
|
||||
content += 'This showcase demonstrates various terminal features and rendering capabilities.\r\n';
|
||||
content += 'Scroll down to see different sections and test the scrolling functionality.\r\n\r\n';
|
||||
|
||||
// Fill the page with numbered lines
|
||||
const contentLines = contentRows - 2;
|
||||
for (let line = 1; line <= contentLines; line++) {
|
||||
content += (`Line ${lineNumber.toString().padStart(4, '0')}: Content originally sized for ${contentCols}x${contentRows} terminal - watch it reflow!\n`);
|
||||
lineNumber++;
|
||||
}
|
||||
// Separator
|
||||
content += '\x1b[90m' + '═'.repeat(100) + '\x1b[0m\r\n\r\n';
|
||||
|
||||
// Page footer
|
||||
const footerLine = '◄' + '='.repeat(contentCols - 2) + '►';
|
||||
content += (`\x1b[43m${footerLine}\x1b[0m\n`);
|
||||
lineNumber++;
|
||||
// Sample log output
|
||||
content += '\x1b[1;33m📋 Sample Log Output:\x1b[0m\r\n';
|
||||
for (let i = 1; i <= 15; i++) {
|
||||
const level = ['INFO', 'WARN', 'ERROR', 'DEBUG'][i % 4];
|
||||
const color = level === 'ERROR' ? '31' : level === 'WARN' ? '33' : level === 'DEBUG' ? '90' : '36';
|
||||
content += `\x1b[90m[2024-06-17 21:58:${(10 + i).toString().padStart(2, '0')}]\x1b[0m \x1b[${color}m${level.padEnd(5)}\x1b[0m Sample log message ${i} - testing terminal output\r\n`;
|
||||
}
|
||||
content += '\r\n';
|
||||
|
||||
// Data table
|
||||
content += '\x1b[1;35m📊 Sample Data Table:\x1b[0m\r\n';
|
||||
content += '\x1b[37m┌──────────────┬──────────────┬──────────────┬──────────────┐\x1b[0m\r\n';
|
||||
content += '\x1b[37m│\x1b[1m Name \x1b[0m\x1b[37m│\x1b[1m Status \x1b[0m\x1b[37m│\x1b[1m CPU% \x1b[0m\x1b[37m│\x1b[1m Memory% \x1b[0m\x1b[37m│\x1b[0m\r\n';
|
||||
content += '\x1b[37m├──────────────┼──────────────┼──────────────┼──────────────┤\x1b[0m\r\n';
|
||||
|
||||
const processes = [
|
||||
{ name: 'web-server', status: 'running', cpu: '12.3', mem: '45.2', statusColor: '32' },
|
||||
{ name: 'database', status: 'running', cpu: '8.7', mem: '62.1', statusColor: '32' },
|
||||
{ name: 'worker-1', status: 'stopped', cpu: '0.0', mem: '0.0', statusColor: '31' },
|
||||
{ name: 'cache', status: 'running', cpu: '2.1', mem: '18.9', statusColor: '32' },
|
||||
{ name: 'auth-svc', status: 'warning', cpu: '15.6', mem: '78.3', statusColor: '33' }
|
||||
];
|
||||
|
||||
processes.forEach(proc => {
|
||||
content += `\x1b[37m│\x1b[0m ${proc.name.padEnd(12)} \x1b[37m│\x1b[0m \x1b[${proc.statusColor}m${proc.status.padEnd(12)}\x1b[0m \x1b[37m│\x1b[0m ${proc.cpu.padStart(12)} \x1b[37m│\x1b[0m ${proc.mem.padStart(12)} \x1b[37m│\x1b[0m\r\n`;
|
||||
});
|
||||
content += '\x1b[37m└──────────────┴──────────────┴──────────────┴──────────────┘\x1b[0m\r\n\r\n';
|
||||
|
||||
// Network activity
|
||||
content += '\x1b[1;32m🌐 Network Activity:\x1b[0m\r\n';
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const method = ['GET', 'POST', 'PUT', 'DELETE'][i % 4];
|
||||
const status = [200, 201, 404, 500][i % 4];
|
||||
const statusColor = status < 300 ? '32' : status < 400 ? '33' : '31';
|
||||
const path = ['/api/users', '/api/orders', '/health', '/metrics', '/login'][i % 5];
|
||||
content += `\x1b[36m${method.padEnd(6)}\x1b[0m ${path.padEnd(15)} \x1b[${statusColor}m${status}\x1b[0m \x1b[90m${Math.floor(Math.random() * 500)}ms\x1b[0m\r\n`;
|
||||
}
|
||||
content += '\r\n';
|
||||
|
||||
// Separator
|
||||
content += '\x1b[90m' + '═'.repeat(100) + '\x1b[0m\r\n\r\n';
|
||||
|
||||
// Git log simulation
|
||||
content += '\x1b[1;31m🔄 Git Log:\x1b[0m\r\n';
|
||||
const commits = [
|
||||
{ hash: 'a1b2c3d', msg: 'feat: implement user authentication system', author: 'alice', time: '2 hours ago' },
|
||||
{ hash: 'e4f5g6h', msg: 'fix: resolve memory leak in websocket handler', author: 'bob', time: '4 hours ago' },
|
||||
{ hash: 'i7j8k9l', msg: 'docs: update API documentation', author: 'charlie', time: '6 hours ago' },
|
||||
{ hash: 'm1n2o3p', msg: 'refactor: optimize database queries', author: 'alice', time: '8 hours ago' },
|
||||
{ hash: 'q4r5s6t', msg: 'test: add unit tests for payment module', author: 'diana', time: '1 day ago' },
|
||||
{ hash: 'u7v8w9x', msg: 'chore: update dependencies to latest versions', author: 'bob', time: '2 days ago' },
|
||||
{ hash: 'y1z2a3b', msg: 'feat: add dark mode support', author: 'charlie', time: '3 days ago' }
|
||||
];
|
||||
|
||||
commits.forEach(commit => {
|
||||
content += `\x1b[33m${commit.hash}\x1b[0m \x1b[32m${commit.msg}\x1b[0m \x1b[90m(${commit.author}, ${commit.time})\x1b[0m\r\n`;
|
||||
});
|
||||
content += '\r\n';
|
||||
|
||||
// Docker container status
|
||||
content += '\x1b[1;34m🐳 Docker Containers:\x1b[0m\r\n';
|
||||
content += '\x1b[37mCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS\x1b[0m\r\n';
|
||||
const containers = [
|
||||
{ id: '8f2e1a9c4b5d', image: 'nginx:latest', cmd: '/docker-entrypoint.sh', created: '2 hours ago', status: 'Up 2 hours', ports: '0.0.0.0:80->80/tcp', statusColor: '32' },
|
||||
{ id: '3d7c9e1f8a2b', image: 'postgres:13', cmd: 'docker-entrypoint.sh', created: '3 hours ago', status: 'Up 3 hours', ports: '5432/tcp', statusColor: '32' },
|
||||
{ id: 'b4a8f2d9c1e7', image: 'redis:alpine', cmd: 'docker-entrypoint.sh', created: '1 hour ago', status: 'Exited (0)', ports: '', statusColor: '31' },
|
||||
{ id: '9c1e4b8f2a5d', image: 'node:16-alpine', cmd: 'npm start', created: '30 min ago', status: 'Up 30 minutes', ports: '0.0.0.0:3000->3000/tcp', statusColor: '32' }
|
||||
];
|
||||
|
||||
containers.forEach(container => {
|
||||
content += `${container.id.substring(0, 12)} ${container.image.padEnd(18)} ${container.cmd.padEnd(24)} ${container.created.padEnd(14)} \x1b[${container.statusColor}m${container.status.padEnd(24)}\x1b[0m ${container.ports}\r\n`;
|
||||
});
|
||||
content += '\r\n';
|
||||
|
||||
// System metrics
|
||||
content += '\x1b[1;35m📈 System Metrics:\x1b[0m\r\n';
|
||||
content += `\x1b[36mCPU Usage:\x1b[0m [\x1b[32m${'█'.repeat(15)}\x1b[90m${'░'.repeat(10)}\x1b[0m] 60%\r\n`;
|
||||
content += `\x1b[36mMemory:\x1b[0m [\x1b[33m${'█'.repeat(20)}\x1b[90m${'░'.repeat(5)}\x1b[0m] 80%\r\n`;
|
||||
content += `\x1b[36mDisk Usage:\x1b[0m [\x1b[31m${'█'.repeat(22)}\x1b[90m${'░'.repeat(3)}\x1b[0m] 88%\r\n`;
|
||||
content += `\x1b[36mNetwork In:\x1b[0m \x1b[32m↓ 1.2 MB/s\x1b[0m\r\n`;
|
||||
content += `\x1b[36mNetwork Out:\x1b[0m \x1b[31m↑ 850 KB/s\x1b[0m\r\n`;
|
||||
content += `\x1b[36mUptime:\x1b[0m \x1b[37m15 days, 7 hours, 23 minutes\x1b[0m\r\n\r\n`;
|
||||
|
||||
// Error messages
|
||||
content += '\x1b[1;31m❌ Recent Errors:\x1b[0m\r\n';
|
||||
const errors = [
|
||||
'Connection timeout to database server (timeout: 30s)',
|
||||
'Invalid JWT token provided in authorization header',
|
||||
'Rate limit exceeded for IP 192.168.1.100 (max: 1000/hour)',
|
||||
'File not found: /var/log/application.log',
|
||||
'SSL certificate expires in 7 days'
|
||||
];
|
||||
|
||||
errors.forEach((error, i) => {
|
||||
content += `\x1b[90m[${new Date().toISOString()}]\x1b[0m \x1b[31mERROR\x1b[0m ${error}\r\n`;
|
||||
});
|
||||
content += '\r\n';
|
||||
|
||||
// SQL queries
|
||||
content += '\x1b[1;36m🗄️ Database Activity:\x1b[0m\r\n';
|
||||
const queries = [
|
||||
{ query: 'SELECT * FROM users WHERE status = ?', time: '12ms', rows: '156' },
|
||||
{ query: 'UPDATE orders SET status = ? WHERE id = ?', time: '8ms', rows: '1' },
|
||||
{ query: 'INSERT INTO audit_log (action, user_id) VALUES (?, ?)', time: '4ms', rows: '1' },
|
||||
{ query: 'SELECT COUNT(*) FROM sessions WHERE expires_at > NOW()', time: '15ms', rows: '1' },
|
||||
{ query: 'DELETE FROM temp_files WHERE created_at < ?', time: '23ms', rows: '47' }
|
||||
];
|
||||
|
||||
queries.forEach(q => {
|
||||
content += `\x1b[37m[\x1b[32m${q.time.padStart(5)}\x1b[37m]\x1b[0m \x1b[35m${q.query}\x1b[0m \x1b[90m(${q.rows} rows)\x1b[0m\r\n`;
|
||||
});
|
||||
content += '\r\n';
|
||||
|
||||
// API endpoints
|
||||
content += '\x1b[1;33m🌍 API Endpoints:\x1b[0m\r\n';
|
||||
const endpoints = [
|
||||
{ method: 'GET', path: '/api/v1/users', description: 'List all users', auth: 'Bearer' },
|
||||
{ method: 'POST', path: '/api/v1/users', description: 'Create new user', auth: 'Bearer' },
|
||||
{ method: 'GET', path: '/api/v1/users/{id}', description: 'Get user by ID', auth: 'Bearer' },
|
||||
{ method: 'PUT', path: '/api/v1/users/{id}', description: 'Update user', auth: 'Bearer' },
|
||||
{ method: 'DELETE', path: '/api/v1/users/{id}', description: 'Delete user', auth: 'Admin' },
|
||||
{ method: 'GET', path: '/api/v1/orders', description: 'List orders', auth: 'Bearer' },
|
||||
{ method: 'POST', path: '/api/v1/orders', description: 'Create order', auth: 'Bearer' },
|
||||
{ method: 'GET', path: '/health', description: 'Health check', auth: 'None' }
|
||||
];
|
||||
|
||||
endpoints.forEach(ep => {
|
||||
const methodColor = ep.method === 'GET' ? '32' : ep.method === 'POST' ? '33' : ep.method === 'PUT' ? '34' : '31';
|
||||
const authColor = ep.auth === 'None' ? '90' : ep.auth === 'Admin' ? '31' : '36';
|
||||
content += `\x1b[${methodColor}m${ep.method.padEnd(6)}\x1b[0m \x1b[37m${ep.path.padEnd(25)}\x1b[0m ${ep.description.padEnd(20)} \x1b[${authColor}m[${ep.auth}]\x1b[0m\r\n`;
|
||||
});
|
||||
content += '\r\n';
|
||||
|
||||
// Package.json dependencies
|
||||
content += '\x1b[1;32m📦 Dependencies:\x1b[0m\r\n';
|
||||
const deps = [
|
||||
{ name: 'express', version: '^4.18.2', type: 'runtime' },
|
||||
{ name: 'typescript', version: '^5.0.4', type: 'dev' },
|
||||
{ name: 'react', version: '^18.2.0', type: 'runtime' },
|
||||
{ name: 'webpack', version: '^5.88.0', type: 'dev' },
|
||||
{ name: 'jest', version: '^29.5.0', type: 'dev' },
|
||||
{ name: 'lodash', version: '^4.17.21', type: 'runtime' },
|
||||
{ name: 'axios', version: '^1.4.0', type: 'runtime' }
|
||||
];
|
||||
|
||||
deps.forEach(dep => {
|
||||
const typeColor = dep.type === 'dev' ? '90' : '32';
|
||||
content += `\x1b[36m${dep.name.padEnd(15)}\x1b[0m \x1b[33m${dep.version.padEnd(12)}\x1b[0m \x1b[${typeColor}m${dep.type}\x1b[0m\r\n`;
|
||||
});
|
||||
content += '\r\n';
|
||||
|
||||
// Separator
|
||||
content += '\x1b[90m' + '═'.repeat(100) + '\x1b[0m\r\n\r\n';
|
||||
|
||||
// Text styles demo
|
||||
content += '\x1b[1mBold text\x1b[0m | \x1b[3mItalic text\x1b[0m | \x1b[4mUnderlined text\x1b[0m | \x1b[2mDim text\x1b[0m | \x1b[7mInverted text\x1b[0m\r\n\r\n';
|
||||
|
||||
// Progress bars
|
||||
content += '\x1b[1;33m📊 Progress Bars:\x1b[0m\r\n';
|
||||
for (let i = 0; i < 5; i++) {
|
||||
content += `\x1b[36m${['Loading', 'Parsing', 'Building', 'Testing', 'Deploy'][i]}:\x1b[0m `;
|
||||
const progress = Math.floor(Math.random() * 100);
|
||||
const filled = Math.floor(progress / 5);
|
||||
content += '\x1b[32m';
|
||||
for (let j = 0; j < filled; j++) content += '█';
|
||||
content += '\x1b[90m';
|
||||
for (let j = filled; j < 20; j++) content += '░';
|
||||
content += `\x1b[0m ${progress}%\r\n`;
|
||||
}
|
||||
content += '\r\n';
|
||||
|
||||
// File listing simulation
|
||||
content += '\x1b[1;35m📁 File Listing:\x1b[0m\r\n';
|
||||
const files = [
|
||||
{ name: 'package.json', size: '2.1K', icon: '📄', color: '33' },
|
||||
{ name: 'src/', size: '-', icon: '📁', color: '34' },
|
||||
{ name: 'README.md', size: '4.5K', icon: '📝', color: '36' },
|
||||
{ name: 'node_modules/', size: '-', icon: '📦', color: '90' },
|
||||
{ name: 'dist/', size: '-', icon: '🏗️', color: '32' }
|
||||
];
|
||||
|
||||
files.forEach(file => {
|
||||
content += `${file.icon} \x1b[${file.color}m${file.name.padEnd(20)}\x1b[0m \x1b[90m${file.size.padStart(8)}\x1b[0m\r\n`;
|
||||
});
|
||||
content += '\r\n';
|
||||
|
||||
// Code sample
|
||||
content += '\x1b[1;35m🎨 Code Sample:\x1b[0m\r\n';
|
||||
content += '\x1b[90m┌────────────────────────────────────────────────────────────────────────────────────────────────────────────┐\x1b[0m\r\n';
|
||||
content += '\x1b[90m│\x1b[0m \x1b[35mfunction\x1b[0m \x1b[33mhelloWorld\x1b[0m\x1b[37m(\x1b[36mname\x1b[37m: \x1b[32mstring\x1b[37m) {\x1b[0m \x1b[90m│\x1b[0m\r\n';
|
||||
content += '\x1b[90m│\x1b[0m \x1b[35mconst\x1b[0m \x1b[36mmessage\x1b[0m \x1b[37m=\x1b[0m \x1b[31m`Hello, \x1b[33m${name}\x1b[31m! Welcome to the DOM terminal.`\x1b[37m;\x1b[0m \x1b[90m│\x1b[0m\r\n';
|
||||
content += '\x1b[90m│\x1b[0m \x1b[36mconsole\x1b[37m.\x1b[33mlog\x1b[37m(\x1b[36mmessage\x1b[37m);\x1b[0m \x1b[90m│\x1b[0m\r\n';
|
||||
content += '\x1b[90m│\x1b[0m \x1b[35mreturn\x1b[0m \x1b[36mmessage\x1b[37m;\x1b[0m \x1b[90m│\x1b[0m\r\n';
|
||||
content += '\x1b[90m│\x1b[0m \x1b[37m}\x1b[0m \x1b[90m│\x1b[0m\r\n';
|
||||
content += '\x1b[90m└────────────────────────────────────────────────────────────────────────────────────────────────────────────┘\x1b[0m\r\n\r\n';
|
||||
|
||||
// System info
|
||||
content += '\x1b[1;32m🖥️ System Information:\x1b[0m\r\n';
|
||||
content += `\x1b[36mTerminal Size:\x1b[0m ${terminal.cols}x${terminal.rows} characters\r\n`;
|
||||
content += `\x1b[36mFont Family:\x1b[0m Fira Code, monospace\r\n`;
|
||||
content += `\x1b[36mFeatures:\x1b[0m Native text selection, smooth scrolling, touch support\r\n`;
|
||||
content += `\x1b[36mRendering:\x1b[0m DOM-based with virtual scrolling\r\n\r\n`;
|
||||
|
||||
// ASCII art
|
||||
content += '\x1b[1;31m🎭 ASCII Art:\x1b[0m\r\n';
|
||||
content += '\x1b[33m ╭────────────────────────────────────╮\r\n';
|
||||
content += '\x1b[33m │ \x1b[36m*\x1b[32m+\x1b[35m#\x1b[31mo \x1b[1;37mDOM Terminal Ready! \x1b[31mo\x1b[35m#\x1b[32m+\x1b[36m*\x1b[33m │\r\n';
|
||||
content += '\x1b[33m ╰────────────────────────────────────╯\x1b[0m\r\n\r\n';
|
||||
|
||||
// Interactive instructions
|
||||
content += '\x1b[1;33m🎮 Try These Features:\x1b[0m\r\n';
|
||||
content += '\x1b[32m▸\x1b[0m Select text with your mouse\r\n';
|
||||
content += '\x1b[32m▸\x1b[0m Scroll with mouse wheel or touch\r\n';
|
||||
content += '\x1b[32m▸\x1b[0m Change terminal size with buttons above\r\n';
|
||||
content += '\x1b[32m▸\x1b[0m Watch content reflow dynamically\r\n\r\n';
|
||||
|
||||
// Footer
|
||||
content += '\x1b[90m────────────────────────────────────────────────────────────────────────────────────────────────────────────\x1b[0m\r\n';
|
||||
content += '\x1b[1;35m✨ End of demonstration - scroll up to see all content! ✨\x1b[0m\r\n\r\n';
|
||||
|
||||
// Command prompt box
|
||||
content += '\x1b[37m╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\r\n';
|
||||
content += '\x1b[37m│ \x1b[32m>\x1b[0m \x1b[37m│\r\n';
|
||||
content += '\x1b[37m╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\x1b[0m\r\n';
|
||||
|
||||
content += ('\x1b[1;31m>>> END OF ALL CONTENT - THIS IS THE BOTTOM <<<\x1b[0m\n');
|
||||
content += ('\x1b[1;33mIf you can see this, you reached the end. Scroll up to see all pages.\x1b[0m\n');
|
||||
terminal.write(content);
|
||||
console.log('Mock data generation completed from HTML');
|
||||
|
||||
// Scroll to bottom after content is written
|
||||
setTimeout(() => {
|
||||
if (terminal.terminal && terminal.terminal.buffer) {
|
||||
const buffer = terminal.terminal.buffer.active;
|
||||
const maxScroll = Math.max(0, buffer.length - terminal.actualRows);
|
||||
terminal.viewportY = maxScroll;
|
||||
terminal.renderBuffer();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
console.log('Mock data generation completed!');
|
||||
}
|
||||
|
||||
function setupSizeControls() {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { Terminal } from '@xterm/xterm';
|
||||
import { Terminal, IBufferLine, IBufferCell } from '@xterm/xterm';
|
||||
|
||||
@customElement('dom-terminal')
|
||||
export class DomTerminal extends LitElement {
|
||||
|
|
@ -19,12 +19,10 @@ export class DomTerminal extends LitElement {
|
|||
@state() private actualRows = 24; // Rows that fit in viewport
|
||||
|
||||
private container: HTMLElement | null = null;
|
||||
private textContainer: HTMLElement | null = null;
|
||||
private resizeObserver: ResizeObserver | null = null;
|
||||
private resizeTimeout: NodeJS.Timeout | null = null;
|
||||
|
||||
// Virtual scrolling optimization
|
||||
private lineElements: HTMLElement[] = [];
|
||||
private renderPending = false;
|
||||
|
||||
connectedCallback() {
|
||||
|
|
@ -64,9 +62,8 @@ export class DomTerminal extends LitElement {
|
|||
this.requestUpdate();
|
||||
|
||||
this.container = this.querySelector('#dom-terminal-container') as HTMLElement;
|
||||
this.textContainer = this.querySelector('#dom-text-container') as HTMLElement;
|
||||
|
||||
if (!this.container || !this.textContainer) {
|
||||
if (!this.container) {
|
||||
throw new Error('Terminal container not found');
|
||||
}
|
||||
|
||||
|
|
@ -84,7 +81,7 @@ export class DomTerminal extends LitElement {
|
|||
if (this.terminal) {
|
||||
this.terminal.resize(this.cols, this.rows);
|
||||
this.fitTerminal();
|
||||
this.renderTerminalContent();
|
||||
this.renderBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -147,19 +144,19 @@ export class DomTerminal extends LitElement {
|
|||
}
|
||||
this.resizeTimeout = setTimeout(() => {
|
||||
this.fitTerminal();
|
||||
this.renderTerminalContent();
|
||||
this.renderBuffer();
|
||||
}, 50);
|
||||
});
|
||||
this.resizeObserver.observe(this.container);
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
this.fitTerminal();
|
||||
this.renderTerminalContent();
|
||||
this.renderBuffer();
|
||||
});
|
||||
}
|
||||
|
||||
private setupScrolling() {
|
||||
if (!this.container || !this.textContainer) return;
|
||||
if (!this.container) return;
|
||||
|
||||
// Handle wheel events
|
||||
this.container.addEventListener(
|
||||
|
|
@ -283,60 +280,48 @@ export class DomTerminal extends LitElement {
|
|||
if (!this.renderPending) {
|
||||
this.renderPending = true;
|
||||
requestAnimationFrame(() => {
|
||||
this.renderTerminalContent();
|
||||
this.renderBuffer();
|
||||
this.renderPending = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private renderTerminalContent() {
|
||||
if (!this.terminal || !this.textContainer) return;
|
||||
private renderBuffer() {
|
||||
if (!this.terminal || !this.container) return;
|
||||
|
||||
const buffer = this.terminal.buffer.active;
|
||||
const bufferLength = buffer.length;
|
||||
const startRow = Math.min(this.viewportY, Math.max(0, bufferLength - this.actualRows));
|
||||
// const endRow = Math.min(bufferLength, startRow + this.actualRows);
|
||||
|
||||
// Ensure we have enough line elements
|
||||
while (this.lineElements.length < this.actualRows) {
|
||||
const lineEl = document.createElement('div');
|
||||
lineEl.className = 'terminal-line';
|
||||
this.lineElements.push(lineEl);
|
||||
this.textContainer.appendChild(lineEl);
|
||||
}
|
||||
// Build complete innerHTML string
|
||||
let html = '';
|
||||
const cell = buffer.getNullCell();
|
||||
|
||||
// Hide extra line elements
|
||||
for (let i = this.actualRows; i < this.lineElements.length; i++) {
|
||||
this.lineElements[i].style.display = 'none';
|
||||
}
|
||||
|
||||
// Render visible lines
|
||||
for (let i = 0; i < this.actualRows; i++) {
|
||||
const row = startRow + i;
|
||||
const lineEl = this.lineElements[i];
|
||||
lineEl.style.display = 'block';
|
||||
|
||||
if (row >= bufferLength) {
|
||||
lineEl.innerHTML = '';
|
||||
html += '<div class="terminal-line"></div>';
|
||||
continue;
|
||||
}
|
||||
|
||||
const line = buffer.getLine(row);
|
||||
if (!line) {
|
||||
lineEl.innerHTML = '';
|
||||
html += '<div class="terminal-line"></div>';
|
||||
continue;
|
||||
}
|
||||
|
||||
// Always re-render the line
|
||||
const content = this.renderLine(line);
|
||||
lineEl.innerHTML = content || '';
|
||||
const lineContent = this.renderLine(line, cell);
|
||||
html += `<div class="terminal-line">${lineContent || ''}</div>`;
|
||||
}
|
||||
|
||||
// Set the complete innerHTML at once
|
||||
this.container.innerHTML = html;
|
||||
}
|
||||
|
||||
private renderLine(line: any): string {
|
||||
private renderLine(line: IBufferLine, cell: IBufferCell): string {
|
||||
let html = '';
|
||||
|
||||
let currentChars = '';
|
||||
let currentClasses = '';
|
||||
let currentStyle = '';
|
||||
|
|
@ -350,10 +335,10 @@ export class DomTerminal extends LitElement {
|
|||
|
||||
// Process each cell in the line
|
||||
for (let col = 0; col < line.length; col++) {
|
||||
const cell = line.getCell(col);
|
||||
line.getCell(col, cell);
|
||||
if (!cell) continue;
|
||||
|
||||
// XTerm.js cell API
|
||||
// XTerm.js cell API - use || ' ' to ensure we get a space for empty cells
|
||||
const char = cell.getChars() || ' ';
|
||||
const width = cell.getWidth();
|
||||
|
||||
|
|
@ -366,22 +351,14 @@ export class DomTerminal extends LitElement {
|
|||
|
||||
// Get foreground color
|
||||
const fg = cell.getFgColor();
|
||||
if (fg !== undefined) {
|
||||
if (typeof fg === 'number') {
|
||||
style += `color: var(--terminal-color-${fg});`;
|
||||
} else if (fg.css) {
|
||||
style += `color: ${fg.css};`;
|
||||
}
|
||||
if (fg !== undefined && typeof fg === 'number' && fg >= 0) {
|
||||
style += `color: var(--terminal-color-${fg});`;
|
||||
}
|
||||
|
||||
// Get background color
|
||||
const bg = cell.getBgColor();
|
||||
if (bg !== undefined) {
|
||||
if (typeof bg === 'number') {
|
||||
style += `background-color: var(--terminal-color-${bg});`;
|
||||
} else if (bg.css) {
|
||||
style += `background-color: ${bg.css};`;
|
||||
}
|
||||
if (bg !== undefined && typeof bg === 'number' && bg >= 0) {
|
||||
style += `background-color: var(--terminal-color-${bg});`;
|
||||
}
|
||||
|
||||
// Get text attributes/flags
|
||||
|
|
@ -416,7 +393,7 @@ export class DomTerminal extends LitElement {
|
|||
public write(data: string) {
|
||||
if (this.terminal) {
|
||||
this.terminal.write(data, () => {
|
||||
this.renderTerminalContent();
|
||||
this.renderBuffer();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -425,14 +402,10 @@ export class DomTerminal extends LitElement {
|
|||
if (this.terminal) {
|
||||
this.terminal.clear();
|
||||
this.viewportY = 0;
|
||||
this.renderTerminalContent();
|
||||
this.renderBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
public getTerminal(): Terminal | null {
|
||||
return this.terminal;
|
||||
}
|
||||
|
||||
public setViewportSize(cols: number, rows: number) {
|
||||
this.cols = cols;
|
||||
this.rows = rows;
|
||||
|
|
@ -440,7 +413,7 @@ export class DomTerminal extends LitElement {
|
|||
if (this.terminal) {
|
||||
this.terminal.resize(cols, rows);
|
||||
this.fitTerminal();
|
||||
this.renderTerminalContent();
|
||||
this.renderBuffer();
|
||||
}
|
||||
|
||||
this.requestUpdate();
|
||||
|
|
@ -474,6 +447,7 @@ export class DomTerminal extends LitElement {
|
|||
font-family: 'Fira Code', ui-monospace, SFMono-Regular, monospace;
|
||||
font-size: ${this.fontSize}px;
|
||||
line-height: ${this.fontSize}px;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.terminal-line {
|
||||
|
|
@ -484,6 +458,7 @@ export class DomTerminal extends LitElement {
|
|||
|
||||
.terminal-char {
|
||||
font-family: inherit;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.terminal-char.bold {
|
||||
|
|
@ -513,19 +488,8 @@ export class DomTerminal extends LitElement {
|
|||
.terminal-char.invisible {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Mobile touch improvements */
|
||||
@media (max-width: 768px) {
|
||||
.dom-terminal-container {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
touch-action: pan-y;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="dom-terminal-container" class="dom-terminal-container w-full h-full">
|
||||
<div id="dom-text-container" class="w-full h-full"></div>
|
||||
</div>
|
||||
<div id="dom-terminal-container" class="dom-terminal-container w-full h-full"></div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue