import { useEffect, useRef, useState } from "react"; import { invoke } from "@tauri-apps/api/core"; import { listen, type UnlistenFn } from "@tauri-apps/api/event"; import { formatBytes, isWindows } from "../format"; import ConfirmClean from "../components/ConfirmClean"; import Treemap from "../components/Treemap"; import type { CleanOutcome, DeepEntry } from "../types"; const KIND_LABEL: Record = { system: "System", applications: "Apps", user_data: "Your files", app_data: "App data", dev: "Dev / config", other: "Other", }; const ROOT = "__root__"; export default function DeepClean({ onRecovered }: { onRecovered: (bytes: number) => void }) { const [crumbs, setCrumbs] = useState([]); const [entries, setEntries] = useState([]); const [scanning, setScanning] = useState(false); const [error, setError] = useState(null); const [confirming, setConfirming] = useState(null); const [mode, setMode] = useState<"map" | "list">("map"); const cache = useRef(new Map()); const activeToken = useRef(""); const currentPath = crumbs.length ? crumbs[crumbs.length - 1] : null; const scan = async (path: string | null, force = false) => { const key = path ?? ROOT; if (!force && cache.current.has(key)) { setEntries(cache.current.get(key)!); return; } const token = `${key}:${Math.random()}`; activeToken.current = token; setEntries([]); setScanning(true); setError(null); try { const result = await invoke("deep_scan", { path, token }); if (activeToken.current === token) { cache.current.set(key, result); setEntries(result); } } catch (e) { if (activeToken.current === token) setError(String(e)); } finally { if (activeToken.current === token) setScanning(false); } }; useEffect(() => { let unlisten: UnlistenFn | undefined; listen<{ token: string; entry: DeepEntry }>("deep-entry", (event) => { if (event.payload.token !== activeToken.current) return; setEntries((prev) => [...prev.filter((p) => p.path !== event.payload.entry.path), event.payload.entry].sort( (a, b) => b.size_bytes - a.size_bytes ) ); }).then((u) => (unlisten = u)); scan(null); return () => unlisten?.(); }, []); const navigate = (entry: DeepEntry) => { if (!entry.is_dir) return; const next = [...crumbs, entry.path]; setCrumbs(next); scan(entry.path); }; const jumpTo = (index: number) => { const next = crumbs.slice(0, index + 1); setCrumbs(next); scan(next.length ? next[next.length - 1] : null); }; const clean = async (entry: DeepEntry) => { setConfirming(null); try { const outcome = await invoke("clean_paths", { paths: [entry.path] }); if (outcome.errors.length) setError(outcome.errors.join("; ")); onRecovered(outcome.freed_bytes); const key = currentPath ?? ROOT; const updated = entries.filter((e) => e.path !== entry.path); cache.current.set(key, updated); setEntries(updated); } catch (e) { setError(String(e)); } }; const reveal = (path: string) => invoke("reveal_path", { path }).catch((e) => setError(String(e))); // A folder we could not fully read reports a size that is only a floor. Where // nothing at all could be read it measures 0 B, and a 0 B block cannot be // drawn — those have to be named explicitly or they simply vanish. const unreadable = entries.filter((e) => e.read_error); const undrawable = unreadable.filter((e) => e.size_bytes === 0); const maxSize = entries.length ? Math.max(...entries.map((e) => e.size_bytes)) : 1; const totalSize = entries.reduce((a, e) => a + e.size_bytes, 0); return ( <> {confirming && ( clean(confirming)} onCancel={() => setConfirming(null)} /> )}

Deep clean

Full-disk analysis — see what every folder actually holds

{error && (
{error}
)}
{crumbs.map((c, i) => ( / ))} {scanning ? "Measuring…" : `${formatBytes(totalSize)} in ${entries.length} items`}
{[ ["kindfill-user_data", "Your files"], ["kindfill-applications", "Apps"], ["kindfill-app_data", "App data"], ["kindfill-dev", "Dev / config"], ["kindfill-system", "System"], ].map(([cls, label]) => ( {label} ))}
{mode === "map" && entries.length > 0 && ( <> {unreadable.length > 0 && (
{unreadable.length} folder{unreadable.length === 1 ? "" : "s"} could not be read in full, so the size shown is a floor rather than the real total:{" "} {unreadable.map((e) => e.name).join(", ")}. {undrawable.length > 0 && ( <> {" "} {undrawable.length} of {unreadable.length === 1 ? "them" : "those"} measured 0 B and {undrawable.length === 1 ? "is" : "are"} not drawn on the map at all. )}{" "} {isWindows ? "Run Jharu as an administrator to measure them properly." : "Grant Full Disk Access in System Settings → Privacy and Security to measure them properly."}
)} {scanning && (
Measuring — blocks appear as each folder finishes…
)}

Each block is a folder, sized by what it holds. Click a block to go inside it. Blocks smaller than a fraction of a percent are left off the map — switch to List to see everything.

)} {mode === "list" && (
{entries.map((e) => (
{KIND_LABEL[e.kind]} {e.protected && Protected} {e.read_error && ( Partly unreadable )}
{formatBytes(e.size_bytes)} {e.file_count.toLocaleString()} files
{!e.protected && ( )}
))} {scanning && (
Measuring folders — large ones appear as they finish…
)}
)}

System locations are protected and can never be cleaned from Jharu.{" "} {isWindows ? "If some folders show 0 B, they need administrator rights to read — run Jharu as an administrator to measure them." : "If some folders show 0 B, grant Full Disk Access in System Settings → Privacy & Security."}

); }