'use client'; import { useEffect, useRef, useState } from 'react'; import { Terminal } from '@xterm/xterm'; import { FitAddon } from '@xterm/addon-fit'; import { io, Socket } from 'socket.io-client'; import '@xterm/xterm/css/xterm.css'; interface TerminalClientProps { vmid: string; token: string; } export default function TerminalClient({ vmid, token }: TerminalClientProps) { const terminalRef = useRef(null); const [status, setStatus] = useState<'connecting' | 'connected' | 'disconnected' | 'error'>('connecting'); const [error, setError] = useState(null); const termRef = useRef(null); const socketRef = useRef(null); const fitAddonRef = useRef(null); useEffect(() => { if (!terminalRef.current) return; // Initialize Terminal const term = new Terminal({ theme: { background: '#1a1b26', foreground: '#c0caf5', cursor: '#c0caf5', cursorAccent: '#1a1b26', selectionBackground: '#33467C', black: '#15161E', red: '#f7768e', green: '#9ece6a', yellow: '#e0af68', blue: '#7aa2f7', magenta: '#bb9af7', cyan: '#7dcfff', white: '#a9b1d6', brightBlack: '#414868', brightRed: '#f7768e', brightGreen: '#9ece6a', brightYellow: '#e0af68', brightBlue: '#7aa2f7', brightMagenta: '#bb9af7', brightCyan: '#7dcfff', brightWhite: '#c0caf5', }, fontSize: 14, fontFamily: 'Menlo, Monaco, Courier New, monospace', cursorBlink: true, cursorStyle: 'block', scrollback: 10000, allowProposedApi: true, }); const fitAddon = new FitAddon(); term.loadAddon(fitAddon); term.open(terminalRef.current); // Give the terminal time to render before fitting setTimeout(() => { fitAddon.fit(); console.log(`[Terminal] Terminal fitted to: ${term.cols}x${term.rows}`); }, 50); termRef.current = term; fitAddonRef.current = fitAddon; let handleResize: (() => void) | null = null; // Wait a bit for terminal to be properly sized, then connect setTimeout(() => { // Connect to WebSocket server via Nginx proxy console.log('[Terminal] Connecting to WebSocket server...'); console.log(`[Terminal] Initial terminal size: ${term.cols}x${term.rows}`); const socket = io({ auth: { token }, query: { vmid, cols: term.cols, rows: term.rows }, transports: ['websocket', 'polling'], path: '/socket.io/', }); socketRef.current = socket; socket.on('connect', () => { console.log('[Terminal] Connected to WebSocket'); setStatus('connecting'); }); socket.on('status', (newStatus: string) => { console.log('[Terminal] Status:', newStatus); if (newStatus === 'connected') { setStatus('connected'); setError(null); // Send initial terminal size when SSH is ready if (fitAddon) { fitAddon.fit(); const rows = term.rows; const cols = term.cols; console.log(`[Terminal] Sending initial size: ${cols}x${rows}`); socket.emit('resize', { rows, cols }); } } else if (newStatus === 'disconnected') { setStatus('disconnected'); } }); socket.on('data', (data: string) => { term.write(data); }); socket.on('error', (err: string) => { console.error('[Terminal] Error:', err); setError(err); setStatus('error'); }); socket.on('disconnect', () => { console.log('[Terminal] Disconnected from WebSocket'); setStatus('disconnected'); }); // Terminal → WebSocket term.onData((data) => { if (socket.connected) { socket.emit('data', data); } }); // Handle terminal resize handleResize = () => { if (fitAddon && socket.connected) { fitAddon.fit(); socket.emit('resize', { rows: term.rows, cols: term.cols, }); } }; window.addEventListener('resize', handleResize); // Fit terminal after a short delay setTimeout(handleResize, 100); }, 100); // Wait 100ms for terminal to render // Cleanup return () => { if (handleResize) { window.removeEventListener('resize', handleResize); } if (socketRef.current) { socketRef.current.disconnect(); } term.dispose(); }; }, [vmid, token]); return (
{/* Header */}

VM {vmid} Console

{status === 'connecting' && (
Connecting...
)} {status === 'connected' && (
Connected
)} {status === 'disconnected' && (
Disconnected
)} {status === 'error' && error && (
{error}
)}
{/* Terminal */}
); }