diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cba8d0..5e3ddeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to CarWash will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Changed +- In the TUI, marking a directory marks everything beneath it, including recently used and + needs-review artifacts; before, those had to be expanded and marked one by one. Protected + artifacts are still never marked, and the review screen still lists what is recent or needs + review before anything is deleted. `a` still marks only what is ready. + ## [0.4.1] - 2026-09-27 ### Fixed diff --git a/crates/carwash/src/tui/app.rs b/crates/carwash/src/tui/app.rs index 1ce896e..a758000 100644 --- a/crates/carwash/src/tui/app.rs +++ b/crates/carwash/src/tui/app.rs @@ -1001,33 +1001,32 @@ impl App { return; } } else { - let ready: Vec = ids + // A directory marks everything beneath it except protected artifacts, which can + // never be marked; the review screen still calls out recent and needs-review ones. + let markable: Vec = ids .iter() .copied() - .filter(|&id| self.markable(id) && self.hold(id).is_none()) + .filter(|&id| self.markable(id)) .collect(); - let held = ids - .iter() - .filter(|&&id| self.markable(id) && self.hold(id).is_some()) - .count(); - if !ready.is_empty() && ready.iter().all(|id| self.marked.contains(id)) { + let protected = ids.len() - markable.len(); + if markable.is_empty() { + self.toast( + format!("Nothing to mark here: {protected} protected"), + Level::Warn, + ); + return; + } + if markable.iter().all(|id| self.marked.contains(id)) { for id in &ids { self.marked.remove(id); } } else { - self.marked.extend(ready.iter().copied()); - if ready.is_empty() && held > 0 { + self.marked.extend(markable.iter().copied()); + if protected > 0 { self.toast( format!( - "Nothing ready here: {held} held back (expand to mark individually)" - ), - Level::Warn, - ); - } else if held > 0 { - self.toast( - format!( - "Marked {} · {held} held back (recent or needs review)", - ready.len() + "Marked {} · {protected} protected left alone", + markable.len() ), Level::Info, ); @@ -1313,14 +1312,14 @@ pub(crate) mod tests { } #[test] - fn marking_a_project_marks_only_ready_artifacts() { + fn marking_a_project_marks_everything_beneath_it() { let mut app = app(); assert_eq!(app.rows.rows[0].label, "a"); press(&mut app, KeyCode::Char(' ')); assert!(app.marked.contains(&ArtifactId(0))); assert!( - !app.marked.contains(&ArtifactId(1)), - "recently used is held back" + app.marked.contains(&ArtifactId(1)), + "recently used is marked too" ); // Marking again unmarks. press(&mut app, KeyCode::Char('k')); @@ -1328,6 +1327,21 @@ pub(crate) mod tests { assert!(app.marked.is_empty()); } + #[test] + fn marking_a_project_skips_protected_artifacts() { + let mut app = app(); + let b = app + .rows + .rows + .iter() + .position(|row| row.label == "b") + .expect("project b row"); + app.select(b); + press(&mut app, KeyCode::Char(' ')); + assert!(app.marked.is_empty()); + assert!(app.toast.as_ref().unwrap().text.contains("protected")); + } + #[test] fn protected_artifacts_cannot_be_marked() { let mut app = app(); diff --git a/crates/carwash/src/tui/keymap.rs b/crates/carwash/src/tui/keymap.rs index 273041c..e477fb1 100644 --- a/crates/carwash/src/tui/keymap.rs +++ b/crates/carwash/src/tui/keymap.rs @@ -319,7 +319,7 @@ pub const RECLAIM: &[Binding] = &[ "Space", A::Mark, S::Select, - "mark / unmark (directories mark their ready artifacts)", + "mark / unmark (directories mark everything beneath them)", ) .hint("mark"), bind( diff --git a/crates/carwash/src/tui/view.rs b/crates/carwash/src/tui/view.rs index 8b8fe5c..6df0a5e 100644 --- a/crates/carwash/src/tui/view.rs +++ b/crates/carwash/src/tui/view.rs @@ -329,7 +329,7 @@ fn mark_cell(app: &App, row: &Row) -> Span<'static> { Span::styled(g.locked, t.muted()) } else if totals.marked == 0 { Span::styled(g.unmarked, t.muted()) - } else if totals.marked >= totals.ready.max(1) { + } else if totals.marked >= totals.markable { Span::styled(g.marked, t.bold(t.accent2)) } else { Span::styled(g.partial, t.fg(t.accent2))