/** * HeadlessTerminal Component * * Display-only terminal that shows content from a headless terminal * running in the backend. Uses useAvocadoBackend() instead of window.desktopAPI. */ import { useEffect, useState, useCallback, useRef, type JSX } from 'react'; import { useAvocadoBackend } from '../../context/AvocadoProvider'; export interface HeadlessTerminalProps { terminalId: string; sessionId: string; cols?: number; rows?: number; isActive?: boolean; refreshInterval?: number; className?: string; fontSize?: number; fontFamily?: string; showLineNumbers?: boolean; showCursor?: boolean; } interface ScreenState { lines: string[]; content: string; cursorX: number; cursorY: number; lastUpdate: Date; } export function HeadlessTerminal({ terminalId, sessionId, cols = 80, rows = 24, isActive = false, refreshInterval = 100, className = '', fontSize = 14, fontFamily = 'Menlo, Monaco, "Courier New", monospace', showLineNumbers = false, showCursor = true, }: HeadlessTerminalProps): JSX.Element { const backend = useAvocadoBackend(); const [screenState, setScreenState] = useState({ lines: [], content: '', cursorX: 0, cursorY: 0, lastUpdate: new Date(), }); const [error, setError] = useState(null); const refreshTimerRef = useRef | null>(null); const fetchContent = useCallback(async () => { try { const [linesResult, cursorResult] = await Promise.all([ backend.terminal.getScreenLines?.(terminalId), backend.terminal.getCursorPosition?.(terminalId), ]); if (linesResult?.success && linesResult.lines) { setScreenState({ lines: linesResult.lines, content: linesResult.lines.join('\n'), cursorX: cursorResult?.success ? cursorResult.position?.x ?? 0 : 0, cursorY: cursorResult?.success ? cursorResult.position?.y ?? 0 : 0, lastUpdate: new Date(), }); setError(null); } else if (linesResult && !linesResult.success) { setError(linesResult.error || 'Failed to fetch content'); } } catch (err) { setError(err instanceof Error ? err.message : String(err)); } }, [terminalId, backend]); useEffect(() => { fetchContent(); if (refreshInterval > 0) { refreshTimerRef.current = setInterval(fetchContent, refreshInterval); } return () => { if (refreshTimerRef.current) { clearInterval(refreshTimerRef.current); refreshTimerRef.current = null; } }; }, [terminalId, refreshInterval, fetchContent]); const renderLine = (line: string, lineIndex: number): JSX.Element => { const isCursorLine = showCursor && lineIndex === screenState.cursorY; if (!isCursorLine) { return (
{showLineNumbers && ({lineIndex + 1})} {line || ' '}
); } const beforeCursor = line.slice(0, screenState.cursorX); const cursorChar = line[screenState.cursorX] || ' '; const afterCursor = line.slice(screenState.cursorX + 1); return (
{showLineNumbers && ({lineIndex + 1})} {beforeCursor} {cursorChar} {afterCursor}
); }; return (
Headless Terminal ({cols}x{rows}){isActive && (ACTIVE)} Cursor: ({screenState.cursorX}, {screenState.cursorY})
{error && (
Error: {error}
)}
{screenState.lines.length > 0 ? screenState.lines.map((line, index) => renderLine(line, index)) : (
No content yet...
)}
Last update: {screenState.lastUpdate.toLocaleTimeString()}
); } export default HeadlessTerminal;