import { useEffect, useRef } from 'react'; import { Terminal } from '@xterm/xterm'; import { FitAddon } from '@xterm/addon-fit'; import '@xterm/xterm/css/xterm.css'; import { ChevronLeft, Radio, RotateCcw } from 'lucide-react'; import { useWs } from '../ws'; interface Props { onBack: () => void; } export default function SerialConsolePage({ onBack }: Props) { const containerRef = useRef(null); const termRef = useRef(null); const fitRef = useRef(null); const lineBuffer = useRef(''); const { status, send, subscribe } = useWs(); // Keep a stable ref to `send` so the xterm onData closure never goes stale const sendRef = useRef(send); useEffect(() => { sendRef.current = send; }, [send]); // Initialize xterm once on mount useEffect(() => { const el = containerRef.current; if (!el) return; const term = new Terminal({ cursorBlink: true, fontSize: 14, fontFamily: '"Cascadia Code","Fira Code",Menlo,Monaco,"Courier New",monospace', theme: { background: '#0a0a0a', foreground: '#d4d4d4', cursor: '#d4d4d4', selectionBackground: '#264f78', }, scrollback: 5000, convertEol: true, }); const fit = new FitAddon(); term.loadAddon(fit); term.open(el); fit.fit(); termRef.current = term; fitRef.current = fit; term.writeln('\x1b[2m── Meatloaf Serial Console ──\x1b[0m'); term.writeln(''); term.onData((data) => { for (const char of data) { if (char === '\r' || char === '\n') { term.write('\r\n'); sendRef.current(lineBuffer.current + '\r'); lineBuffer.current = ''; } else if (char === '\x7f' || char === '\b') { if (lineBuffer.current.length > 0) { lineBuffer.current = lineBuffer.current.slice(0, -1); term.write('\b \b'); } } else if (char === '\x03') { lineBuffer.current = ''; term.write('^C\r\n'); } else if (char >= ' ') { lineBuffer.current += char; term.write(char); } } }); const onResize = () => fitRef.current?.fit(); window.addEventListener('resize', onResize); return () => { window.removeEventListener('resize', onResize); term.dispose(); termRef.current = null; fitRef.current = null; }; }, []); // Forward all incoming WS messages to the terminal useEffect(() => { return subscribe((msg) => { termRef.current?.write(msg); }); }, [subscribe]); const statusColor = status === 'connected' ? 'text-green-400' : status === 'connecting' ? 'text-amber-400' : 'text-neutral-500'; const statusLabel = status === 'connected' ? 'Connected' : status === 'connecting' ? 'Connecting…' : 'Disconnected'; return (
{/* Header */}
Serial Console
{statusLabel}
{/* Terminal */}
); }