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 './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 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(() => { let cancelled = false; let unlisten: (() => void) | undefined; const handleIncomingRow = (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 { unlisten = await listen('main-log-line', event => { handleIncomingRow(rowFromPayload(event.payload)); }); const history = await invoke('get_main_logs'); if (cancelled) return; 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); } catch (err) { if (!cancelled) { historyLoadedRef.current = true; setLoadError(err instanceof Error ? err.message : String(err)); } } finally { if (!cancelled) { setLoading(false); } } }; void setup(); return () => { cancelled = true; historyLoadedRef.current = false; initialBufferRef.current = []; lastHistorySequenceRef.current = 0; historyLineCountsRef.current = new Map(); unlisten?.(); }; }, [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; requestAnimationFrame(() => { viewport.scrollTop = viewport.scrollHeight; }); }, [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 () => { try { await navigator.clipboard.writeText(filteredRows.map(row => row.line).join('\n')); setCopyStatus('Copied'); } catch { setCopyStatus('Copy failed'); } window.setTimeout(() => 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}
))}
); };