import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { invoke } from '@tauri-apps/api/core'; import { listen } from '@tauri-apps/api/event'; import './UnpackLogsWindow.css'; interface UnpackLogEntry { archive: string; destination: string; status_code: number | null; stdout: string; stderr: string; started_at_ms: number; finished_at_ms: number; success: boolean; } interface FilteredEntry { entry: UnpackLogEntry; originalIndex: number; stdoutLines: string[]; stderrLines: string[]; matchCount: number; } export const isUnpackLogsView = (): boolean => new URLSearchParams(window.location.search).get('view') === 'unpack-logs'; const formatLogTime = (timestampMs: number): string => { if (timestampMs <= 0) { return 'unknown time'; } return new Date(timestampMs).toLocaleString(); }; const basename = (path: string): string => { const segments = path.split(/[\\/]/); return segments[segments.length - 1] || path; }; const nonEmptyLines = (text: string): string[] => text.split(/\r?\n/).filter(line => line.trim().length > 0); export const UnpackLogsWindow = () => { const [logs, setLogs] = useState([]); const [errorsOnly, setErrorsOnly] = useState(false); const [regexInput, setRegexInput] = useState(''); const [selectedOriginalIndex, setSelectedOriginalIndex] = useState(null); const refreshLogs = useCallback(async () => { const unpackLogs = await invoke('get_unpack_logs'); setLogs(unpackLogs); }, []); useEffect(() => { let unlisten: (() => void) | undefined; const setup = async () => { await refreshLogs(); unlisten = await listen('unpack-logs-updated', () => { void refreshLogs(); }); }; void setup(); return () => { unlisten?.(); }; }, [refreshLogs]); 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 filteredLogs = useMemo(() => { const out: FilteredEntry[] = []; logs.forEach((entry, originalIndex) => { if (errorsOnly && entry.success) return; const stdoutClean = nonEmptyLines(entry.stdout); const stderrClean = nonEmptyLines(entry.stderr); const stdoutLines = regex ? stdoutClean.filter(line => regex.test(line)) : stdoutClean; const stderrLines = regex ? stderrClean.filter(line => regex.test(line)) : stderrClean; const matchCount = stdoutLines.length + stderrLines.length; // With an active regex, hide entries that contribute no matching lines. if (regex && matchCount === 0) return; out.push({ entry, originalIndex, stdoutLines, stderrLines, matchCount }); }); return out; }, [logs, errorsOnly, regex]); const selectedListIndex = useMemo(() => { if (filteredLogs.length === 0) return -1; if (selectedOriginalIndex === null) return 0; const idx = filteredLogs.findIndex(item => item.originalIndex === selectedOriginalIndex); return idx >= 0 ? idx : 0; }, [filteredLogs, selectedOriginalIndex]); const current = selectedListIndex >= 0 ? filteredLogs[selectedListIndex] : null; const goTo = useCallback((listIndex: number) => { if (listIndex < 0 || listIndex >= filteredLogs.length) return; setSelectedOriginalIndex(filteredLogs[listIndex].originalIndex); }, [filteredLogs]); useEffect(() => { const handler = (e: KeyboardEvent) => { const target = e.target as HTMLElement | null; if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA')) return; if (e.key === 'ArrowUp' || e.key === 'k') { e.preventDefault(); goTo(selectedListIndex - 1); } else if (e.key === 'ArrowDown' || e.key === 'j') { e.preventDefault(); goTo(selectedListIndex + 1); } }; window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); }, [goTo, selectedListIndex]); const activeItemRef = useRef(null); useEffect(() => { activeItemRef.current?.scrollIntoView({ block: 'nearest' }); }, [selectedListIndex]); return (

Unpack Logs

setRegexInput(e.target.value)} title={regexError ?? ''} spellCheck={false} />
{regexError && (
regex error: {regexError}
)}
{current ? ( <>
{selectedListIndex + 1} / {filteredLogs.length} {regex && ` · ${current.matchCount} matching lines`}
[{formatLogTime(current.entry.finished_at_ms)}] {current.entry.success ? 'OK' : 'FAILED'} {' '}status={current.entry.status_code ?? 'none'}
archive: {current.entry.archive}
dest: {current.entry.destination}
{current.stdoutLines.length > 0 ? (
{current.stdoutLines.join('\n')}
) : (
{regex ? '(stdout: no matching lines)' : '(stdout empty)'}
)} {current.stderrLines.length > 0 && (
{current.stderrLines.join('\n')}
)}
) : (
Nothing to show.
)}
); };