Skip to content

sweep/2026-10-12: 9H + 22M fixed, 7 new tests, 42 commits - #4

Open
Chartres wants to merge 42 commits into
mainfrom
sweep/2026-10-12
Open

Chartres wants to merge 42 commits into
mainfrom
sweep/2026-10-12

Conversation

@Chartres

Copy link
Copy Markdown
Owner

Findings

Sev Found Fixed Skipped
H 9 9 0
M ~30 22 ~8
L ~40 8 ~32

Tests: 30 → 37 (+7 new)

Notable fixes

Crashes (H):

  • src/ui/treemap_view.rs + dir_tree.rs — byte-slice on non-ASCII names panicked the UI thread (e.g. "Příliš.pdf"); now char-safe truncation
  • src/app.rs — hovered_dir not reset on new scan → OOB index panic on first frame after scan completes
  • src/app.rs — pending_action (Confirm Trash dialog) kept stale NodeIds across a rescan → trashed wrong path
  • src/scanner/walk.rs — hard links counted once per link; <Hidden / Skipped> saturated to 0

Safety (H/M):

  • src/cleanup.rs — suffix rules (Library/Caches, .cargo, etc.) matched anywhere in the path string; /x/MyLibrary/Caches was a false candidate; now component-boundary checked
  • src/cleanup.rs — scan root itself was listed as a cleanup candidate; its Trash button bypassed the root guard
  • src/app.rs — Cmd+⌫ keyboard shortcut bypassed the scan-root/synthetic-node guard added to the context menu
  • src/cleanup.rs — .Trash offered as a candidate; its Trash button moved ~/.Trash into the Trash
  • src/cleanup.rs — .m2/.gradle descriptions didn't mention credentials (settings.xml, keys)
  • src/cleanup.rs — Docker description didn't warn about named volumes with non-regenerable data
  • src/cleanup.rs — target rule flagged any directory named target, not just Rust builds; guarded by Cargo.toml presence
  • src/app.rs — perform_delete had no is_alive check; stale ConfirmTrash trashed wrong path after partial refresh
  • src/scanner/tree.rs — remove_node didn't guard against double-remove; double-subtracted sizes
  • src/app.rs — zoom stack not sanitized on delete; trashing the zoomed-into dir left treemap blank

Correctness (M):

  • src/ui/treemap_view.rs — selecting "(none)" extension dimmed extension-less files instead of lighting them
  • src/ui/cleanup_window.rs — category headers repeated across size-sorted list; now sorted (category, size desc)
  • src/app.rs — collect_files(root).len() walked entire tree every frame; replaced with node.file_count
  • src/app.rs — batch/single trash failures only eprintln'd; now shown in status bar
  • src/app.rs — poll_scan ignored Disconnected; a crashed walker thread left UI stuck on "Scanning…"
  • src/app.rs — start_scan now resets pending_action, hovered_dir, partial_refresh_receiver, selected_extension
  • src/app.rs — Confirm Delete dialog shows full path, not just truncated filename
  • Context menu: Move to Trash suppressed for scan root and synthetic nodes (<Free Space>, <Hidden / Skipped>)

New tests:

  • tests/cleanup_tests.rs — classify, find_candidates, .Trash exclusion, .rustup, iCloud, node_modules
  • tests/walk_tests.rs — tempdir scan: file + subdir sizes
  • tests/tree_tests.rs — remove_node double-dead guard

Refactors (6 commits)

  • confirm_dialog helper for three trash confirmation modals (dedup ~100 lines)
  • AppState::remove_trashed shares post-delete cleanup across single/batch (dedup ~20 lines)
  • AppState::reset_dead_view_root deduped view/zoom cleanup across 3 call sites
  • aggregate_dir helper deduped size-field writes across compute_sizes, recompute_sizes_upward, graft_under
  • mark_descendants_dead — non-recursive, replaces recursive mark_dead_recursive + clear_descendants duplication
  • Table-driven Cmd+1/2/3 color-mode shortcuts

Skipped (architectural, out of scope)

  • Scan thread cancellation (M) — requires channel-based cooperative cancel plumbing
  • Arena byte reclamation (L) — requires arena compaction
  • st_blocks allocated-size (L) — OS-specific, needs careful sparse/special-file guards
  • /System/Volumes scan root empty (L) — volume prefix skip logic needs restructure

🤖 Generated with Claude Code

Chartres and others added 30 commits October 12, 2026 00:40
Byte-index slicing on multi-byte UTF-8 strings panics when the slice
boundary falls inside a character. Switch both treemap labels and the
dir-tree name column to chars().take(n).collect() so truncation always
lands on a char boundary.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A partial refresh completing after start_scan had been called would
graft its old NodeId into the new tree, corrupting the tree structure
and targeting the wrong path on Trash. Dropping the receiver in
start_scan prevents the stale graft from ever landing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Files with nlink > 1 can appear under multiple paths in the tree.
Previously every hard-link path added its full st_size to directory
totals, causing the root size to exceed the volume's used space and
making the "Hidden / Skipped" sentinel saturate to 0. Now the first
path we see for a (dev, ino) pair is counted normally; subsequent
paths for the same inode contribute 0 bytes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
perform_delete now returns early if the node is no longer alive
(e.g. killed by a concurrent partial refresh), preventing a trash
call to a path that no longer exists in the tree. remove_node gains
the same guard so a second removal doesn't double-subtract size and
counts from ancestors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Selecting the no-extension entry sets selected_extension to Some("").
Extension-less files return None from tree.extension(), so comparing
Some("") != None always dimmed them. Check for an empty string and
match against None instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Candidates were sorted by size, so the same category header would
re-emit whenever a different category's item appeared between two
items of the same category. Sort by (category, size desc) before the
display loop so each header appears once.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The bare-name rule matched any directory named \"target\", flagging
Maven targets, CMake build dirs, etc. with incorrect Rust messaging.
Guard the match with a Cargo.toml existence check in the parent dir.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
collect_files(root).len() allocated a Vec of every file ID on every
status-bar render frame. The root node already carries file_count.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Single and batch delete left view_root and zoom_stack pointing at dead
nodes after trashing a directory the user had zoomed into, leaving the
treemap blank. Reset view_root to the tree root and sanitize zoom_stack
whenever the current view_root is no longer alive after a delete.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… children_sorted, let _ = new_id)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Spawned curl processes were dropped without waiting, leaving zombies in
the process table for the app's lifetime. Reap each child on a
background thread.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rmation

A ConfirmTrash dialog opened before a rescan stored NodeIds from the old
tree. After the new tree loaded, confirming the dialog would act on
whichever path happened to share that id in the new tree.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… OOB panic

hovered_dir held a NodeId from the previous tree. When the scan finished
with a smaller tree the old id could be out of bounds, causing
tree.is_alive(id) to panic. Also clears hovered_node in the Done handler
for the same reason.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…iles

metadata.len() reports EOF size, not allocated blocks. Docker.raw, APFS
clones, and compressed files all appear larger than their disk footprint.
Notes the upgrade path to st_blocks * 512 with a zero-guard for special
files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… path integrity

Previously a directory whose stat() failed was skipped entirely, so its
children were reparented to find_closest_ancestor. Their tree-derived
full_path was then grandparent/child instead of parent/child, making
Trash and Reveal target the wrong path.

Now we register a zero-size stub dir in the tree and dir_map so children
are always attached to their real parent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Trash button in the cleanup window would have moved ~/.Trash into the
system Trash. Use the dedicated Empty Trash… button instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Failures were silently dropped after an eprintln. Now they set
batch_error_msg which is displayed in the toolbar status area for one
render frame so the user sees what couldn't be trashed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ext menu

Trashing the scan root is catastrophic; synthetic Free Space / Hidden /
Skipped nodes have no real disk path. Both cases are now excluded from the
context menu.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ename

ConfirmTrash now stores PathBuf instead of the bare filename String. The
dialog displays the complete path so the user knows exactly what will be
moved to Trash before confirming.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers Trash exclusion, .rustup/toolchains and iCloud path detection,
node_modules inclusion, and the find_candidates > 1 MB threshold.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Scans a temp dir with a regular file and a subdir, verifies sizes and
child counts aggregate correctly through the full walk pipeline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Verifies that calling remove_node twice on the same node is a no-op: the
second call neither underflows sizes nor panics.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ion modals

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…root

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd clear_descendants

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Raw ends_with("/Library/Caches") matches "/x/MyLibrary/Caches".
Check that the byte immediately before the matched suffix is '/'
(or the suffix spans the whole string) so only a real component
boundary counts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Chartres and others added 12 commits October 12, 2026 01:14
The old assertion checked category == "Trash" but no rule produces
that category string, so it could never fail. Assert instead that
no candidate's path ends with ".Trash".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Walking from the root itself could classify the scan root (e.g.
~/Library) as a cleanup candidate, which would be catastrophic.
Start the walk from root's children instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
.gradle may contain gradle.properties with credentials; .m2 may
contain settings.xml with credentials. Docker named volumes may
hold database data that cannot be regenerated. Update descriptions
so users know to review before deleting.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The context menu already refuses to offer Move to Trash for the
scan root and synthetic nodes (__free_space__, __skipped__). Apply
the same check to the Cmd+Backspace keyboard path so it cannot
bypass that guard.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the walker thread panics, the sender is dropped without
sending Done, causing try_recv to return Err(Disconnected). The
previous while-let-Ok loop silently exited, leaving scanning=true
forever. Now an Err(Disconnected) clears scanning and the receiver.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
take() consumed the error after one render cycle, so it appeared
for a single frame. Switch to as_ref() for display and clear it in
start_scan so the message persists until the user re-scans.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Failed perform_delete and failed Empty Trash were only eprintln!d.
Route both through batch_error_msg so the user sees the error in
the status bar.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A lingering extension filter from the previous scan dimmed the
treemap on the new one. Clear it in start_scan alongside the
other selection state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
treemap/mod.rs already sorts children by size descending before
building the LayoutItem slice. The second sort inside squarify.rs
was redundant.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iterals

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant