| 🟩 Find it Squarified treemap of every byte |
🟨 Understand it Trends, diffs & duplicate hunting |
🟥 Reclaim it One-click cleanup → system Trash |
Tip
No Node. No setup. No telemetry. The desktop app is fully self-contained and scans the disk of the machine it runs on. Deletes always go to your system Trash — nothing is ever hard-deleted, so every action is recoverable.
TreeMap isn't just a treemap — it's a full disk-hygiene workbench. Ten views, one zero-dependency frontend.
|
Disk-usage ring, live scan progress, file-type donut chart, and the top-10 largest files and folders. Click a folder to leap straight into the treemap. A disk-full forecast projects from your scan history — "At current growth (+5.4 GB/day), this disk is full in ~58 days — top culprits: …" — and is honest when it can't know: too little history, erratic growth, or shrinking usage all say so instead of inventing a number. An All Storage strip unifies your local disk with any connected Google Drive / Dropbox / OneDrive — scan a cloud account into the very same treemap (metadata only, no file contents are ever downloaded; deletes go to the provider's own trash; duplicates/live/offload are disabled with clear notices). Opt-in and local-first: with no account connected, zero cloud code runs and nothing touches the network. |
A squarified treemap of every file, sized by bytes and colored teal → amber → red. Drill in, climb back with breadcrumbs + zoom-out, search with highlights ( |
|
A size-proportional icon grid with multi-select, sorting, and virtual scrolling — buttery even on huge folders. |
How much disk does each application own? Every app's total, split into app / caches / data / logs, with a "Clear caches safely" button (Trash-only, never touches your data) and click-through into the treemap. Files no app owns land in an honest "Everything else" bucket, so the totals always match the scan. |
|
Finds true duplicates (size + streamed SHA-256), grouped with reclaimable space per group. Auto-select keeps the newest copy of each. A Near-Duplicate Images tab catches resized, re-encoded and screenshot copies with a perceptual dHash. |
Every scan saves a lightweight snapshot, charted over time per folder — with a clear "what grew / what shrank since last scan" breakdown. |
|
Pick any two scans of the same folder for a file-level diff: added, removed, grew, shrank. Subtrees collapse to one row instead of thousands. |
Custom rules (old / huge / by extension / duplicated), Smart Suggestions — sorted into regenerable ( |
|
The third option next to keep and trash: Offload… copies files to another drive, verifies every byte (SHA-256, read back from the destination), and only then moves the originals to the Trash — never a bare move; any failure rolls back cleanly. This tab is the searchable index of everything offloaded, with per-destination totals, reveal-on-destination, and verified Restore. Unplugged drives show grayed out with a last-seen date. |
Re-scan folders on a schedule with growth-threshold alerts and disk-full forecast warnings (native desktop notifications; the forecast horizon is configurable in Settings, default 30 days). Tell it what to skip with paths, names, or globs like |
How it's built — Node.js + Express 5 + TypeScript on the backend. A single, zero-dependency
index.htmlon the frontend: hand-coded Canvas 2D, no React, no D3, no Chart.js. Ships as a web app and a downloadable Electron desktop app for macOS and Windows.
Grab the latest installer from the Releases page:
| Platform | File | How |
|---|---|---|
| 🍎 macOS | TreeMap-x.y.z-arm64.dmg |
Open it, drag TreeMap to Applications, launch. |
| 🪟 Windows | TreeMap Setup x.y.z.exe |
Run it and follow the installer. |
Important
First-launch security prompt. Because the app isn't signed with a paid Apple/Microsoft developer certificate, your OS shows a one-time warning.
- macOS: right-click the app → Open → Open
- Windows: click More info → Run anyway
After the first launch it opens normally.
🛠️ macOS says "TreeMap is damaged and can't be opened"?
That happens when the download's quarantine flag is still set. Clear it once, then launch normally — open Terminal and paste:
xattr -dr com.apple.quarantine /Applications/TreeMap.appNo Node.js or setup required — the desktop app is self-contained and scans the disk of the computer it runs on.
- 📌 Menu bar / tray icon with live free-disk stats and quick actions (open app, scan home folder, quit). Close the window and TreeMap stays in the tray so scheduled scans keep running — quit from the tray menu.
- 🖱️ Drag & drop a folder onto the window or dock icon to scan it instantly.
- 🔄 Auto-updates from GitHub Releases (Windows; asks before restarting). On macOS, auto-update needs a code-signed build, so unsigned builds skip it — grab new versions from Releases.
- 🔔 Growth alerts from scheduled scans arrive as native notifications.
npm install
npm run build
npm startThen open http://127.0.0.1:4280 in your browser.
💡 For development with auto-reload:
npm run dev
Requires Node.js 20+. Trash support uses gio on Linux (preinstalled on GNOME/KDE), Finder via osascript on macOS, and the Recycle Bin via PowerShell on Windows.
npm install
npm run app # build + launch the desktop app locally
npm run dist:mac # produce a macOS .dmg in release/
npm run dist:win # produce a Windows installer in release/
⚠️ You can only build the macOS app on a Mac and the Windows app on Windows. To get both without owning both machines, use the automated release below — GitHub builds them for you.
🤖 Publish a new version (automated GitHub Actions)
A workflow (.github/workflows/release.yml) builds the macOS and Windows installers on GitHub's servers and attaches them to a Release — including the latest*.yml metadata the in-app auto-updater checks.
To cut a release:
- Bump the
versioninpackage.json(e.g.1.2.1). - Create a matching tag prefixed with
v(e.g.v1.2.1) and push it. In GitHub Desktop: Repository → Push, then on github.com: Releases → Draft a new release → Choose a tag → typev1.2.1→ Publish. - The workflow runs automatically, builds both installers, and uploads them. After a few minutes the download links appear on the Releases page.
You can also trigger a test build anytime from Actions → Build & Release → Run workflow (installers are saved as downloadable artifacts instead of a Release).
Click to expand the full endpoint table
| Endpoint | Description |
|---|---|
POST /api/scan |
Start scanning a folder → { scanId } |
GET /api/scan/:id/progress |
Live scan progress (Server-Sent Events) |
GET /api/scan/:id/result |
Full file tree (202 while running) |
GET /api/scan/:id/treemap |
Pre-computed squarified treemap layout |
GET /api/scan/:id/stats |
Scan counters incl. engine, duration & fast-rescan cache usage |
GET /api/scan/:id/budgets |
Saved folder budgets cross-referenced against this scan |
GET /api/scan/:id/export?format=csv|pdf |
Download the scan as CSV (files / folders) or a PDF report |
GET /api/scans |
Completed scans currently in memory |
GET /api/large-files?scanId= |
Top N largest files |
GET /api/large-folders?scanId= |
Top N largest folders (recursive sizes) |
GET /api/file-types?scanId= |
Size breakdown by extension |
GET /api/apps?scanId= |
Per-app storage attribution: totals, app / cache / data / logs breakdown, safe-to-clear bytes |
GET /api/duplicates?scanId= |
Duplicate groups (starts hashing; poll until complete) |
GET /api/near-duplicates?scanId=&threshold= |
Perceptual (dHash) near-duplicate image clusters |
GET /api/empty-folders?scanId= |
Recursively empty folders (ignoreJunk configurable) |
GET /api/compare?scanIdA=&scanIdB= |
File-level diff of two scans of the same root |
GET /api/snapshots |
Scan history: roots, per-root snapshots (?path=), or all (?all=true) |
GET /api/snapshots/compare?a=&b= |
Top-level deltas between two snapshots |
GET /api/snapshots/tree?path=&at= |
Historical treemap closest to a timestamp (time slider), with grew/shrank data |
GET /api/forecast?path= |
Disk-full projection: days until full, confidence, top growers — honest when history is thin |
GET /api/watch/:scanId |
Live disk activity (Server-Sent Events): per-second batches of { path, delta, kind } |
POST /api/container/expand |
List a container's contents (zip/jar/tar/tgz/iso/docker) as virtual treemap children — never extracts |
POST /api/offload · GET /api/offload/:id/progress |
Copy → SHA-256 verify → trash originals, to another drive (SSE progress, cancellable with rollback) |
GET /api/offload/index · POST /api/offload/restore |
Searchable offload catalog (mount-aware) and verified restore |
GET /api/cloud/status · POST /api/cloud/connect · …/disconnect |
Cloud accounts: local-only status, PKCE OAuth (loopback + paste fallback), token wipe |
POST /api/cloud/scan · POST /api/cloud/trash |
Metadata-only cloud scan (registers like a disk scan) and provider-trash deletes — the documented pathGuard exemption |
GET /api/cleanup/suggestions?scanId= |
Smart cleanup suggestions (regenerable / cache / junk) |
GET /api/cleanup/browser-profiles?scanId= |
Per-browser-profile cache breakdown |
GET /api/git/repos?scanId= · POST /api/git/gc |
Git pack/loose/LFS breakdown, and git gc a scanned repo |
GET /api/system/snapshots · POST …/purge |
OS snapshot accounting (APFS / Btrfs / VSS) |
GET /api/settings · PUT /api/settings |
Ignore list, scheduled scans + folder budgets |
GET /api/notifications |
Growth alerts from scheduled scans |
GET /api/system · GET /api/trash/size |
Disk totals & platform; system Trash size |
GET /api/fs/list?path= |
Folder browser (powers the path picker) |
GET /api/files/preview?path= |
Quick-look preview (image / text / thumbnail) |
DELETE /api/files |
Move files to the system trash |
POST /api/files/open |
Open / reveal a path in Finder & co. |
Disk tools should never lose your data. TreeMap is built defensively:
- 🔒 Paths are sanitized and traversal-proofed; system dirs (
/proc,/sys,/dev,/run,C:\Windows\System32, …) are blocked outright. - 🎯 Trash/open endpoints only accept paths inside a folder you scanned — and never paths inside an archive (only the archive itself can be trashed).
- ♻️ Deletes always go through the OS Trash — undo from Finder/Explorer any time.
- 📤 Offload never bare-moves: copy first, verify every byte against a SHA-256 read back from the destination, and only then trash the originals — any failure rolls back with local data untouched.
- ☁️ Cloud scanning is strictly opt-in and metadata-only: no file contents are ever downloaded, OAuth tokens live only in the local app-data folder (Disconnect wipes them), cloud deletes go to the provider's own trash, and with no account connected no cloud code path executes at all.
- 🧬 The Duplicates view refuses to trash every copy in a group — at least one always stays.
- 🚦 Token-bucket rate limiting (10 req/s per IP), plus graceful SIGTERM shutdown that drains live SSE streams and stops background hashing, scheduled scans & live-activity watchers.
- ⏳ Scan results live in memory only and auto-expire after 30 minutes; history snapshots and settings are small JSON files in the platform app-data folder (
~/Library/Application Support/TreeMap,%APPDATA%\TreeMap, or~/.config/treemap).
src/
api/ Express routes (scan, files, system, insights, settings)
services/ DiskScanner (adaptive concurrent walker), Cleaner (trash/open),
DuplicateFinder (staged hashing), Snapshots (Trends history),
CleanupRules (smart suggestions), AppAttribution (per-app storage),
Forecast (disk-full projection), Watcher (live activity),
ContainerScanner (archive drill-down), Offload (copy-verify-trash),
cloud/ (Google Drive, Dropbox, OneDrive — OAuth + metadata scans),
Scheduler (recurring scans), Settings, Storage (app-data JSON), DiskUsage
models/ Shared TypeScript interfaces
utils/ formatBytes, squarified treemap, path sanitizer, glob matcher
middleware/ errorHandler, rateLimiter, pathGuard
index.ts App entrypoint + graceful shutdown
electron/
main.js Desktop shell: window, tray, drag-drop, notifications, auto-update
preload.js Context-isolated bridge for drag-drop paths & scan pushes
public/
index.html The entire frontend (inline CSS + JS, zero dependencies)
scripts/
gen-tray-icon.js One-time generator for the tray template icons
- Scan speed is a threadpool problem, not a walker problem. Every async
lstat/readdirruns on libuv's threadpool, which defaults to 4 threads — that, not the walker's concurrency, was the bottleneck. TreeMap sizes the pool to 2× cores (≤ 16) before it spins up; measured on APFS this scans ~1.6× faster, while 32 threads is slower than 4 (kernel metadata-lock contention). The dashboard shows which engine ran and how long the scan took. - Snapshots are automatic — one is saved after every successful scan, so Trends needs zero setup. Totals + top-level entries live in
snapshots.json(a few KB each, capped at 200 per folder); the time slider's shallow trees (≤ 3 levels, ~100 KB budget each) sit in separate per-root files so the main history file stays tiny. - The scheduler is a 60-second
setInterval, notnode-cron— hour-level granularity doesn't justify a dependency. Schedules fire while the app runs (the desktop app keeps running in the tray). - Duplicate detection is staged (size → first 64 KB hash → full SHA-256) so scans with hundreds of thousands of files finish hashing in seconds, and only true content matches are reported.
- Compare collapses subtrees — a deleted or added folder shows as one row, not thousands of file rows.
TreeMap · built with 🟩🟨🟥 by Prithvi-Web
If TreeMap freed up a few gigs for you, a ⭐ goes a long way.