- Rust 99.2%
- Shell 0.8%
Split the app into a frontend-neutral core and two frontends: - src/state.rs owns the scan, the tree, selection/zoom/expansion, and runs confirmed trash/delete actions. - src/ui/ holds what both frontends share: UiAction, ViewState, the flattened tree rows (moved out of the egui view, tests included) and node colours as plain RGB. - src/gui/ is the eframe frontend, unchanged in behaviour (feature `gui`, default). - src/tui/ is a new ratatui frontend (feature `tui`): same tree + treemap layout, treemap painted with half-block glyphs in truecolor or the 256-colour cube, keyboard and mouse navigation, confirm popup for delete/trash, ~10 fps redraw cap while scanning. `disk-manager --tui PATH` forces the terminal UI; without a DISPLAY or WAYLAND_DISPLAY it is picked automatically. A `--no-default-features --features tui` build has no windowing dependencies (2.4 MB release binary vs 25 MB) and gets its own CI job without the X11 packages. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|---|---|---|
| .forgejo/workflows | ||
| macos | ||
| src | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| README.md | ||
disk-manager
A WinDirStat-style disk usage analyzer: a squarified treemap plus a size-sorted folder tree, with a headless JSON mode for scripts and agents.
Two frontends share one scanner and one size tree: a native window, and a terminal UI for SSH sessions and servers. Both can delete or trash what they show. Built for macOS (Finder integration, app bundle), but everything works on any Unix.
Build & run
cargo run # GUI, no scan yet
cargo run -- ~/Downloads # GUI, starts scanning immediately
cargo run -- --tui ~/ # terminal UI
cargo test
Without a DISPLAY or WAYLAND_DISPLAY (an SSH session, say) the terminal UI
is picked automatically when it is compiled in. --tui forces it.
Frontends are cargo features, gui (default) and tui. On a server, skip
the windowing stack entirely:
cargo install --path . --no-default-features --features tui
disk-manager /var/lib # no path scans the current directory
Terminal UI
Same two panels as the window: folder tree on the left, treemap on the right,
breadcrumb above it. The treemap is painted with half-block glyphs, so each
cell carries two tiles vertically, and uses 24-bit colour when COLORTERM
says the terminal has it (falls back to the 256-colour cube otherwise, e.g.
inside an unconfigured tmux).
| key | effect |
|---|---|
Tab |
switch between tree and treemap |
↑ ↓ / j k |
move |
← → / h l |
tree: collapse / expand · treemap: previous / next tile |
Space |
fold / unfold a folder |
Enter |
zoom the treemap into the folder |
Backspace / u |
zoom out one level |
g / G |
first / last |
d |
delete permanently (asks first) |
t |
move to trash (asks first) |
r |
rescan |
? |
key help · q quits |
The mouse works too when the terminal forwards it: click selects, double-click opens, the wheel scrolls the tree, the breadcrumb is clickable.
Deleting is blocked while the scan runs, in both frontends: the walker would
keep streaming entries from under the folder and recreate it in the tree.
Note that t on a server puts things in ~/.local/share/Trash, which nobody
ever empties there; d is usually what you want remotely.
On macOS, build a real .app bundle so the OS shows the standard
Documents/Desktop/Downloads permission prompts:
./macos/bundle.sh
open "target/Disk Manager.app" --args "$HOME"
Headless mode
scan runs the same parallel scanner and prints a JSON size tree to stdout.
The shaping flags exist to bound the output so it fits in a fixed context:
disk-manager scan ~ --depth 3 --top 10 --min-size 10485760 --pretty
| flag | effect |
|---|---|
--depth N |
emit at most N levels below the root (0 = unlimited) |
--top N |
keep the N largest children per level (0 = all) |
--min-size BYTES |
drop entries below this size |
--pretty |
multi-line JSON instead of one line |
--cross-filesystems |
descend into other mounts (off by default, like du -x) |
How sizes are counted
Sizes are allocated bytes on disk — 512-byte blocks, exactly what du
reports, not the logical file length. Three consequences worth knowing:
- A small file still costs a full block; sparse files cost less than their size.
- Hard links are counted once, deduplicated by
(dev, ino). - Other filesystems are skipped by default. On macOS this matters even for a
single volume: scanning
/would otherwise follow the firmlink at/System/Volumes/Dataand count your data twice.
scan_total_matches_du in src/scan.rs pins this down against real du -sx
output, hard link included.
Layout
| file | role |
|---|---|
src/scan.rs |
parallel jwalk walker, streams batches over a channel |
src/tree.rs |
incremental aggregated size tree |
src/treemap.rs |
squarified treemap geometry, no UI dependency |
src/state.rs |
frontend-neutral app state: owns the scan, drains it, applies actions, runs confirmed deletes |
src/ui/ |
what both frontends share: the actions views emit, the flattened tree rows, node colours |
src/gui/ |
eframe frontend (feature gui) |
src/tui/ |
ratatui frontend (feature tui) |
src/cli.rs |
argument parsing and the headless JSON mode |
src/actions.rs |
reveal / trash / delete |
Path components are kept as OsString all the way through, because a component
path is what a delete action rebuilds an absolute path from — it has to
round-trip byte-for-byte even when a filename is not valid UTF-8. Names are
converted lossily only at display time, via Node::display_name.