import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { invoke } from '@tauri-apps/api/core'; import { listen } from '@tauri-apps/api/event'; import { SegmentedRadio } from './components/SegmentedRadio'; import { capLogRows, consumeLoadedHistoryRow, dedupeBufferedRows, formatCount, LEVEL_FILTER_MIN, LEVEL_FILTER_OPTIONS, LEVEL_ORDER, lineCountsFromRows, type LevelFilter, type MainLogHistoryPayload, type MainLogLinePayload, type MainLogRow, rowFromPayload, rowsFromHistory, } from './lib/mainLogs'; import { AsyncOwner, windowAsyncScope } from './lib/asyncOwnership'; import './MainLogsWindow.css'; export const isMainLogsView = (): boolean => new URLSearchParams(window.location.search).get('view') === 'main-logs'; export const MainLogsWindow = () => { const [logs, setLogs] = useState([]); const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(null); const [regexInput, setRegexInput] = useState(''); const [levelFilter, setLevelFilter] = useState('all'); const [autoScroll, setAutoScroll] = useState(true); const [paused, setPaused] = useState(false); const [pausedBufferCount, setPausedBufferCount] = useState(0); const [copyStatus, setCopyStatus] = useState(null); const viewportRef = useRef(null); const historyLoadedRef = useRef(false); const initialBufferRef = useRef([]); const pausedBufferRef = useRef([]); const pausedRef = useRef(false); const lastHistorySequenceRef = useRef(0); const historyLineCountsRef = useRef>(new Map()); const viewOwnerRef = useRef(null); const copyStatusTimerRef = useRef(undefined); const appendVisibleRows = useCallback((rows: MainLogRow[]) => { setLogs(current => capLogRows([...current, ...rows])); }, []); const bufferPausedRows = useCallback((rows: MainLogRow[]) => { pausedBufferRef.current = capLogRows([...pausedBufferRef.current, ...rows]); setPausedBufferCount(pausedBufferRef.current.length); }, []); useEffect(() => { const owner = new AsyncOwner(error => { console.error('Failed to unregister main log listener:', error); }); viewOwnerRef.current = owner; owner.own(() => { if (copyStatusTimerRef.current !== undefined) { window.clearTimeout(copyStatusTimerRef.current); copyStatusTimerRef.current = undefined; } }); const handleIncomingRow = owner.guard((row: MainLogRow) => { if (!historyLoadedRef.current) { initialBufferRef.current = capLogRows([...initialBufferRef.current, row]); return; } if ( consumeLoadedHistoryRow( historyLineCountsRef.current, row, lastHistorySequenceRef.current, ) ) { return; } if (pausedRef.current) { bufferPausedRows([row]); return; } appendVisibleRows([row]); }); const setup = async () => { try { const registered = await owner.register(() => listen('main-log-line', event => { handleIncomingRow(rowFromPayload(event.payload)); }) ); if (!registered) return; } catch (err) { if (owner.isActive()) { historyLoadedRef.current = true; setLoadError(err instanceof Error ? err.message : String(err)); setLoading(false); } return; } try { await owner.applyIfActive( () => invoke('get_main_logs'), history => { lastHistorySequenceRef.current = history.lastSequence; const historyRows = rowsFromHistory(history.contents); const historyLineCounts = lineCountsFromRows(historyRows); const liveRows = dedupeBufferedRows( historyLineCounts, initialBufferRef.current, lastHistorySequenceRef.current, ); initialBufferRef.current = []; historyLineCountsRef.current = historyLineCounts; historyLoadedRef.current = true; if (pausedRef.current) { setLogs(capLogRows(historyRows)); bufferPausedRows(liveRows); } else { setLogs(capLogRows([...historyRows, ...liveRows])); } setLoadError(null); setLoading(false); }, ); } catch (err) { if (owner.isActive()) { historyLoadedRef.current = true; setLoadError(err instanceof Error ? err.message : String(err)); setLoading(false); } } }; void setup(); return () => { windowAsyncScope.adopt(owner.dispose()); if (viewOwnerRef.current === owner) viewOwnerRef.current = null; historyLoadedRef.current = false; initialBufferRef.current = []; lastHistorySequenceRef.current = 0; historyLineCountsRef.current = new Map(); }; }, [appendVisibleRows, bufferPausedRows]); const { regex, regexError } = useMemo(() => { if (!regexInput) { return { regex: null as RegExp | null, regexError: null as string | null }; } try { return { regex: new RegExp(regexInput, 'i'), regexError: null }; } catch (e) { return { regex: null, regexError: e instanceof Error ? e.message : String(e) }; } }, [regexInput]); const filteredRows = useMemo(() => { const minLevel = LEVEL_FILTER_MIN[levelFilter]; return logs.filter(row => { if (LEVEL_ORDER[row.level] < minLevel) return false; return regex ? regex.test(row.line) : true; }); }, [levelFilter, logs, regex]); const lastVisibleRow = filteredRows.length > 0 ? filteredRows[filteredRows.length - 1] : null; useEffect(() => { if (!autoScroll) return; const viewport = viewportRef.current; if (!viewport) return; const frame = requestAnimationFrame(() => { viewport.scrollTop = viewport.scrollHeight; }); return () => cancelAnimationFrame(frame); }, [autoScroll, filteredRows.length, lastVisibleRow?.id]); const flushPausedRows = useCallback(() => { const buffered = pausedBufferRef.current; if (buffered.length === 0) return; pausedBufferRef.current = []; setPausedBufferCount(0); appendVisibleRows(buffered); }, [appendVisibleRows]); const togglePaused = useCallback(() => { if (paused) { pausedRef.current = false; setPaused(false); flushPausedRows(); return; } pausedRef.current = true; setPaused(true); }, [flushPausedRows, paused]); const clearLogs = useCallback(() => { setLogs([]); initialBufferRef.current = []; pausedBufferRef.current = []; setPausedBufferCount(0); }, []); const copyFilteredLogs = useCallback(async () => { const owner = viewOwnerRef.current; if (!owner) return; try { if (!await owner.applyIfActive( () => navigator.clipboard.writeText(filteredRows.map(row => row.line).join('\n')), () => setCopyStatus('Copied'), )) return; } catch { if (!owner.isActive()) return; setCopyStatus('Copy failed'); } if (copyStatusTimerRef.current !== undefined) { window.clearTimeout(copyStatusTimerRef.current); } copyStatusTimerRef.current = window.setTimeout(owner.guard(() => { copyStatusTimerRef.current = undefined; setCopyStatus(null); }), 1600); }, [filteredRows]); return (

Application Logs

{copyStatus && {copyStatus}}
setRegexInput(e.target.value)} title={regexError ?? ''} spellCheck={false} />
{regexError && (
regex error: {regexError}
)} {loadError && (
load error: {loadError}
)}
{loading ? 'loading' : `showing ${formatCount(filteredRows.length, 'line')} of ${formatCount(logs.length, 'line')}`} {paused && pausedBufferCount > 0 && ` - ${formatCount(pausedBufferCount, 'paused line')}`}
{filteredRows.length === 0 ? (
{logs.length === 0 ? 'No application logs recorded yet.' : 'No log lines match the current filters.'}
) : filteredRows.map(row => (
{row.line}
))}
); };