refactor(SerialConsolePage): streamline terminal data handling and improve code readability

This commit is contained in:
Jaime Idolpx 2026-06-11 01:53:25 -04:00
parent 648fb2778c
commit fc2ed1c321

View File

@ -13,13 +13,9 @@ export default function SerialConsolePage({ onBack }: Props) {
const fitRef = useRef<FitAddon | null>(null); const fitRef = useRef<FitAddon | null>(null);
const lineBuffer = useRef(''); const lineBuffer = useRef('');
const echoQueue = useRef<string[]>([]); const echoQueue = useRef<string[]>([]);
const { status, send, subscribe } = useWs(); 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(() => { useEffect(() => {
const el = containerRef.current; const el = containerRef.current;
if (!el) return; if (!el) return;
@ -28,12 +24,7 @@ export default function SerialConsolePage({ onBack }: Props) {
cursorBlink: true, cursorBlink: true,
fontSize: 14, fontSize: 14,
fontFamily: '"Cascadia Code","Fira Code",Menlo,Monaco,"Courier New",monospace', fontFamily: '"Cascadia Code","Fira Code",Menlo,Monaco,"Courier New",monospace',
theme: { theme: { background: '#0a0a0a', foreground: '#d4d4d4', cursor: '#d4d4d4', selectionBackground: '#264f78' },
background: '#0a0a0a',
foreground: '#d4d4d4',
cursor: '#d4d4d4',
selectionBackground: '#264f78',
},
scrollback: 5000, scrollback: 5000,
convertEol: true, convertEol: true,
}); });
@ -45,28 +36,29 @@ export default function SerialConsolePage({ onBack }: Props) {
termRef.current = term; termRef.current = term;
fitRef.current = fit; fitRef.current = fit;
term.writeln('\x1b[2m── Meatloaf Serial Console ──\x1b[0m'); term.write('\x1b[2m── Meatloaf Serial Console ──\x1b[0m\r\n\r\n');
term.writeln('');
term.onData((data) => { term.onData(data => {
for (const char of data) { for (const ch of data) {
if (char === '\r' || char === '\n') { if (ch === '\r' || ch === '\n') {
const line = lineBuffer.current + '\r'; const line = lineBuffer.current;
lineBuffer.current = ''; lineBuffer.current = '';
term.write('\r\n'); term.write('\r\n');
if (line) {
echoQueue.current.push(line); echoQueue.current.push(line);
sendRef.current(line); send(line);
} else if (char === '\x7f' || char === '\b') { }
} else if (ch === '\x7f' || ch === '\b') {
if (lineBuffer.current.length > 0) { if (lineBuffer.current.length > 0) {
lineBuffer.current = lineBuffer.current.slice(0, -1); lineBuffer.current = lineBuffer.current.slice(0, -1);
term.write('\b \b'); term.write('\b \b');
} }
} else if (char === '\x03') { } else if (ch === '\x03') {
lineBuffer.current = ''; lineBuffer.current = '';
term.write('^C\r\n'); term.write('^C\r\n');
} else if (char >= ' ') { } else if (ch >= ' ') {
lineBuffer.current += char; lineBuffer.current += ch;
term.write(char); term.write(ch);
} }
} }
}); });
@ -82,15 +74,13 @@ export default function SerialConsolePage({ onBack }: Props) {
}; };
}, []); }, []);
// Forward all incoming WS messages to the terminal, suppressing our own echoes
useEffect(() => { useEffect(() => {
return subscribe((msg) => { return subscribe((msg: string) => {
const term = termRef.current;
if (!term) return;
const idx = echoQueue.current.indexOf(msg); const idx = echoQueue.current.indexOf(msg);
if (idx !== -1) { if (idx !== -1) { echoQueue.current.splice(idx, 1); return; }
echoQueue.current.splice(idx, 1); term.write(msg.endsWith('\n') ? msg : msg + '\r\n');
return;
}
termRef.current?.write(msg);
}); });
}, [subscribe]); }, [subscribe]);
@ -105,7 +95,6 @@ export default function SerialConsolePage({ onBack }: Props) {
return ( return (
<div className="fixed inset-0 bg-[#0a0a0a] z-40 flex flex-col"> <div className="fixed inset-0 bg-[#0a0a0a] z-40 flex flex-col">
{/* Header */}
<div <div
className="flex-none flex items-center gap-2 px-3 py-2 bg-neutral-900 border-b border-neutral-800" className="flex-none flex items-center gap-2 px-3 py-2 bg-neutral-900 border-b border-neutral-800"
style={{ paddingTop: 'max(0.5rem, env(safe-area-inset-top))' }} style={{ paddingTop: 'max(0.5rem, env(safe-area-inset-top))' }}
@ -113,7 +102,6 @@ export default function SerialConsolePage({ onBack }: Props) {
<button <button
onClick={onBack} onClick={onBack}
className="p-1.5 rounded hover:bg-neutral-800 text-neutral-400 hover:text-white transition" className="p-1.5 rounded hover:bg-neutral-800 text-neutral-400 hover:text-white transition"
title="Back"
> >
<ChevronLeft className="w-5 h-5" /> <ChevronLeft className="w-5 h-5" />
</button> </button>
@ -129,13 +117,12 @@ export default function SerialConsolePage({ onBack }: Props) {
<button <button
onClick={() => termRef.current?.clear()} onClick={() => termRef.current?.clear()}
className="p-1.5 rounded hover:bg-neutral-800 text-neutral-400 hover:text-neutral-200 transition" className="p-1.5 rounded hover:bg-neutral-800 text-neutral-400 hover:text-neutral-200 transition"
title="Clear terminal" title="Clear"
> >
<RotateCcw className="w-4 h-4" /> <RotateCcw className="w-4 h-4" />
</button> </button>
</div> </div>
{/* Terminal */}
<div <div
className="flex-1 min-h-0 relative overflow-hidden" className="flex-1 min-h-0 relative overflow-hidden"
style={{ paddingBottom: 'env(safe-area-inset-bottom)' }} style={{ paddingBottom: 'env(safe-area-inset-bottom)' }}