No description
  • Rust 99.2%
  • Shell 0.8%
Find a file
Vincent S. b2a2830b91
Some checks failed
CI / tui (push) Successful in 1m27s
CI / test (push) Failing after 1m44s
[ADD] Terminal UI for SSH sessions, behind a tui cargo feature
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>
2026-09-17 00:22:24 +02:00
.forgejo/workflows [ADD] Terminal UI for SSH sessions, behind a tui cargo feature 2026-09-17 00:22:24 +02:00
macos [ADD] Initial commit 2026-06-16 10:10:31 +02:00
src [ADD] Terminal UI for SSH sessions, behind a tui cargo feature 2026-09-17 00:22:24 +02:00
.gitignore [ADD] Initial commit 2026-06-16 10:10:31 +02:00
Cargo.lock [ADD] Terminal UI for SSH sessions, behind a tui cargo feature 2026-09-17 00:22:24 +02:00
Cargo.toml [ADD] Terminal UI for SSH sessions, behind a tui cargo feature 2026-09-17 00:22:24 +02:00
README.md [ADD] Terminal UI for SSH sessions, behind a tui cargo feature 2026-09-17 00:22:24 +02:00

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/Data and 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.