From 21fdc66264dfe58887cdf18f5de38c4c4931abbe Mon Sep 17 00:00:00 2001 From: Graeme Foster <80714+GraemeF@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:53:07 +0100 Subject: [PATCH] fix(view): say N other beads over what rooting the forest moved The collapsed line said `N roots held back`. It said what had been done to those roots rather than where they now are, it guessed at why the reader pressed the key, and it spelled them with a word readers do not use for them. Graeme settled the words: they are beads, the line names how many, and nothing on it says why they are behind the line, because why the reader asked for one bead is theirs and this program cannot know it. `GroupKind` and the layout walk take the group's own name rather than the screen's, so nothing in the code is named for a sentence that can be rewritten again. Nothing is coined, so the terminology table gains no entry: the gloss of tree the argument would have leaned on is inherited from beads, and this line is the shape `phrase::elided` already draws. The same row's counts and its key hint abutted, because this was the one state block on a row not built through `draw::beside`, whose doc comment claims every one of them is. Both existing tests assert on each cell alone, which passes whether the cells abut or not, so the assertion that holds the gap reads the joined row. --- docs/design.md | 2 +- src/view/draw/groups.rs | 81 +++++++++++++++++++++++---------------- src/view/forest/layout.rs | 34 ++++++++-------- src/view/forest/mod.rs | 24 ++++++------ src/view/lines.rs | 12 +++--- src/view/phrase.rs | 22 ++++++----- 6 files changed, 95 insertions(+), 80 deletions(-) diff --git a/docs/design.md b/docs/design.md index 431b39b..15c2cad 100644 --- a/docs/design.md +++ b/docs/design.md @@ -2331,7 +2331,7 @@ holds trees hidden *because* nothing live is in them, so opening it would contradict the rule it exists to serve. Hidden trees is a group nothing went wrong in — the filter put them there and a key takes them back out — so it is drawn without a warning and it names that key: *a to show all*. The roots -`Shift+F` holds back are the other group read that way. A group that said only how many trees it hides +`Shift+F` puts out of the way are the other group read that way. A group that said only how many trees it hides would read as "nothing to see here" while hiding broken ones, so it also says how many of them carry findings: *4 trees with no live agent · 1 with findings*. The findings stay hidden — the reader asked for that — but the group diff --git a/src/view/draw/groups.rs b/src/view/draw/groups.rs index eedcd0c..6ba085c 100644 --- a/src/view/draw/groups.rs +++ b/src/view/draw/groups.rs @@ -9,7 +9,7 @@ use crate::view::palette; use crate::view::phrase; use crate::view::row::{AGENT, WARNING}; -use super::{pane_marker, sentence}; +use super::{beside, pane_marker, sentence}; /// What lifts the live-agent filter, said beside the trees it is holding back. const SHOW_ALL: &str = "a to show all"; @@ -26,7 +26,7 @@ pub(super) fn group_line(prefix: &str, group: &Group) -> Fitted { GroupKind::FailedProjects => (phrase::failed_projects(group.count), false), GroupKind::Conflicts => (phrase::conflicts(group.count), false), GroupKind::HiddenTrees => (phrase::hidden_trees(group.count, group.with_findings), true), - GroupKind::HeldBack => (phrase::held_back(group.count), true), + GroupKind::OutOfTheWay => (phrase::other_beads(group.count), true), GroupKind::Unattributed => (phrase::unattributed(group.count), false), GroupKind::Unconfigured => (phrase::unconfigured(group.count), false), }; @@ -38,7 +38,7 @@ pub(super) fn group_line(prefix: &str, group: &Group) -> Fitted { }; let state = match group.kind { GroupKind::HiddenTrees => vec![Span::styled(SHOW_ALL, palette::QUIET)], - GroupKind::HeldBack => held_back_state(group), + GroupKind::OutOfTheWay => out_of_the_way_state(group), _ => Vec::new(), }; @@ -48,21 +48,21 @@ pub(super) fn group_line(prefix: &str, group: &Group) -> Fitted { state, ); match group.kind { - GroupKind::HeldBack => line.briefly(held_back_counts(group)), + GroupKind::OutOfTheWay => line.briefly(out_of_the_way_counts(group)), _ => line, } } -/// What a line standing over the roots the mode is holding back says beside -/// itself: the seats in there, the beads wanting looking at, and the key that -/// brings them back. +/// What a line standing over the beads the mode put out of the way says +/// beside itself: the seats in there, the beads wanting looking at, and the +/// key that brings them back. /// -/// The reader asked for one bead, so the roots go; what they did not ask for +/// The reader asked for one bead, so the rest go; what they did not ask for /// was to be told there is nobody on them. Said in the words a line resting /// shut over the same things already uses. -fn held_back_state(group: &Group) -> Vec> { - let mut said = held_back_counts(group); - said.push(Span::styled(WHOLE_FOREST, palette::QUIET)); +fn out_of_the_way_state(group: &Group) -> Vec> { + let mut said = out_of_the_way_counts(group); + beside(&mut said, Span::styled(WHOLE_FOREST, palette::QUIET)); said } @@ -70,22 +70,28 @@ fn held_back_state(group: &Group) -> Vec> { /// /// The hint goes first and goes whole, because `F for th…` names no key while /// the counts are the part of this line a reader can read nowhere else. -fn held_back_counts(group: &Group) -> Vec> { +fn out_of_the_way_counts(group: &Group) -> Vec> { let mut said = Vec::new(); let Some(counts) = &group.held else { return said; }; if counts.live_agents > 0 { - said.push(Span::styled( - format!("{AGENT} {}", phrase::agents_beneath(counts.live_agents)), - palette::AGENT, - )); + beside( + &mut said, + Span::styled( + format!("{AGENT} {}", phrase::agents_beneath(counts.live_agents)), + palette::AGENT, + ), + ); } if counts.anomalies > 0 { - said.push(Span::styled( - format!("{WARNING} {}", phrase::anomalies_beneath(counts.anomalies)), - palette::ATTENTION, - )); + beside( + &mut said, + Span::styled( + format!("{WARNING} {}", phrase::anomalies_beneath(counts.anomalies)), + palette::ATTENTION, + ), + ); } said } @@ -283,10 +289,14 @@ mod tests { /// behind this line, open work and the seats on it included. So the line /// says how many, rather than leaving a reader to read one number as the /// whole truth about what is back there. + /// + /// Asserted as one string rather than cell by cell, because two cells + /// that abut read as one that names neither, and a `contains` on each of + /// them alone passes either way. #[test] - fn a_line_over_held_back_roots_says_its_seats_and_what_wants_looking_at() { + fn a_line_over_the_beads_put_out_of_the_way_says_its_seats_and_what_wants_looking_at() { let group = Group { - kind: GroupKind::HeldBack, + kind: GroupKind::OutOfTheWay, project: Some("summit-works".into()), count: 3, with_findings: 0, @@ -300,10 +310,11 @@ mod tests { let drawn = Painted::of(group_line(SHUT, &group), 120, 1).rows(); - assert!(drawn[0].contains("3 roots held back"), "{drawn:?}"); - assert!(drawn[0].contains("2 agents beneath"), "{drawn:?}"); - assert!(drawn[0].contains("1 bead beneath"), "{drawn:?}"); - assert!(drawn[0].contains("F for the whole forest"), "{drawn:?}"); + assert!(drawn[0].contains("3 other beads"), "{drawn:?}"); + assert!( + drawn[0].contains("2 agents beneath ⚠ 1 bead beneath F for the whole forest"), + "{drawn:?}" + ); } /// A row with no room for the way out says the counts without it. The @@ -312,7 +323,7 @@ mod tests { #[test] fn a_row_too_narrow_for_the_way_out_still_says_what_is_back_there() { let group = Group { - kind: GroupKind::HeldBack, + kind: GroupKind::OutOfTheWay, project: Some("summit-works".into()), count: 3, with_findings: 0, @@ -326,9 +337,11 @@ mod tests { let drawn = Painted::of(group_line(SHUT, &group), 64, 1).rows(); - assert!(drawn[0].contains("3 roots held back"), "{drawn:?}"); - assert!(drawn[0].contains("2 agents beneath"), "{drawn:?}"); - assert!(drawn[0].contains("1 bead beneath"), "{drawn:?}"); + assert!(drawn[0].contains("3 other beads"), "{drawn:?}"); + assert!( + drawn[0].contains("2 agents beneath ⚠ 1 bead beneath"), + "{drawn:?}" + ); assert!(!drawn[0].contains("F for"), "{drawn:?}"); assert!(!drawn[0].contains(CUT), "{drawn:?}"); } @@ -336,9 +349,9 @@ mod tests { /// And says neither where there is neither. A nought said is a column /// spent telling a reader about nothing. #[test] - fn a_line_over_quiet_held_back_roots_says_nothing_of_seats_at_all() { + fn a_line_over_one_quiet_bead_put_out_of_the_way_says_nothing_of_seats_at_all() { let group = Group { - kind: GroupKind::HeldBack, + kind: GroupKind::OutOfTheWay, project: Some("summit-works".into()), count: 1, with_findings: 0, @@ -352,7 +365,7 @@ mod tests { let drawn = Painted::of(group_line(SHUT, &group), 120, 1).rows(); - assert!(drawn[0].contains("1 root held back"), "{drawn:?}"); + assert!(drawn[0].contains("1 other bead"), "{drawn:?}"); assert!(!drawn[0].contains("beneath"), "{drawn:?}"); assert!(drawn[0].contains("F for the whole forest"), "{drawn:?}"); } @@ -376,7 +389,7 @@ mod tests { assert_eq!( marked, - !matches!(kind, GroupKind::HiddenTrees | GroupKind::HeldBack), + !matches!(kind, GroupKind::HiddenTrees | GroupKind::OutOfTheWay), "{kind:?}: {drawn:?}" ); } diff --git a/src/view/forest/layout.rs b/src/view/forest/layout.rs index 9e46114..87fd02d 100644 --- a/src/view/forest/layout.rs +++ b/src/view/forest/layout.rs @@ -45,8 +45,8 @@ enum Child<'a> { /// question a fold raises — what did that just take off the screen — has one /// answer wherever it is asked. /// -/// The same reading answers a root drawn behind the line the mode holding -/// roots back puts them behind: the bead the forest is rooted at is beneath +/// The same reading answers a root drawn behind the line the mode puts the +/// rest of the forest behind: the bead the forest is rooted at is beneath /// that root and drawn at the top of the screen, and this count holds it, for /// the same reason two copies of a bead do not add up. fn shut_over(beneath: Counts, first: bool, folded: Option) -> Option { @@ -159,7 +159,7 @@ pub(super) fn first_bead_of( ) -> Option { let roots = match kind { GroupKind::HiddenTrees => hidden_trees(snapshot, project), - GroupKind::HeldBack => held_back(snapshot, project, rooted) + GroupKind::OutOfTheWay => out_of_the_way(snapshot, project, rooted) .into_iter() .map(|root| root.tree) .collect(), @@ -228,7 +228,7 @@ pub(super) fn walked<'a>( ); } drawn.extend( - held_back(snapshot, Some(project), Some(rooted)) + out_of_the_way(snapshot, Some(project), Some(rooted)) .into_iter() .map(|root| (root.tree, vec![0])), ); @@ -257,8 +257,8 @@ fn group_of( // Counted off the beads behind the line rather than off the roots they // came from: this line is all a reader gets of what is behind it, and // the bead the forest is rooted at is on the screen already. - GroupKind::HeldBack => { - let held = held_back(snapshot, project, rooted); + GroupKind::OutOfTheWay => { + let held = out_of_the_way(snapshot, project, rooted); let counts = Counts::over(held.iter().flat_map(Behind::beads)); (held.len(), 0, Some(counts)) } @@ -325,15 +325,15 @@ fn reached( walked } -/// What one project is holding back because the forest is rooted at one bead: -/// every root it collected but the one that bead stands in, and that one for -/// the part of it the mode stopped drawing. None at all where the forest is -/// rooted at no bead. +/// What one project put out of the way because the forest is rooted at one +/// bead: every root it collected but the one that bead stands in, and that +/// one for the part of it the mode stopped drawing. None at all where the +/// forest is rooted at no bead. /// -/// Shown and hidden alike. The mode holds back what the filter was showing as -/// well as what it was not, and one line standing for both sets is the only -/// line that adds up. -fn held_back<'a>( +/// Shown and hidden alike. The mode moves what the filter was showing as well +/// as what it was not, and one line standing for both sets is the only line +/// that adds up. +fn out_of_the_way<'a>( snapshot: &'a Snapshot, project: Option<&str>, rooted: Option<&Rooted>, @@ -548,7 +548,7 @@ impl<'a> Layout<'a> { trunk.push(!last); } match kind { - GroupKind::HiddenTrees | GroupKind::HeldBack => { + GroupKind::HiddenTrees | GroupKind::OutOfTheWay => { let roots = self.roots_in(kind, project.as_deref()); let count = roots.len(); for (n, root) in roots.into_iter().enumerate() { @@ -578,7 +578,7 @@ impl<'a> Layout<'a> { /// for the project it is one of. fn roots_in(&self, kind: GroupKind, project: Option<&str>) -> Vec> { match kind { - GroupKind::HeldBack => held_back(self.snapshot, project, self.rooted), + GroupKind::OutOfTheWay => out_of_the_way(self.snapshot, project, self.rooted), _ => hidden_trees(self.snapshot, project) .into_iter() .map(|tree| Behind { @@ -873,7 +873,7 @@ fn group_items(snapshot: &Snapshot, kind: GroupKind, project: Option<&str>) -> V .collect(), // These two hold whole roots, drawn as trees rather than as things in // a group. - GroupKind::HiddenTrees | GroupKind::HeldBack => Vec::new(), + GroupKind::HiddenTrees | GroupKind::OutOfTheWay => Vec::new(), GroupKind::Unattributed => snapshot .unattributed .iter() diff --git a/src/view/forest/mod.rs b/src/view/forest/mod.rs index 2fb1945..fea018f 100644 --- a/src/view/forest/mod.rs +++ b/src/view/forest/mod.rs @@ -360,7 +360,7 @@ impl Forest { // drawn at all while the forest is rooted at one bead. if self.held_back(place) { chain.push(Handle::Group( - GroupKind::HeldBack, + GroupKind::OutOfTheWay, Some(place.tree.project.clone()), )); } else if self.hidden(&place.tree) { @@ -5351,7 +5351,7 @@ credential_command = "secret harbour" // Neither holds a finding of its own: each stands over // whole roots, and what is wrong inside one of those is // the root's to report when the group is opened. - GroupKind::HiddenTrees | GroupKind::HeldBack => {} + GroupKind::HiddenTrees | GroupKind::OutOfTheWay => {} }, _ => {} } @@ -7080,10 +7080,10 @@ credential_command = "secret harbour" " │ └─▸ … 3 more", " └── [Unattributed orbital] 2", "▾ ferry", - " ├─▸ [HeldBack ferry] 1", + " ├─▸ [OutOfTheWay ferry] 1", " └── [Unattributed ferry] 1", "▾ harbour", - " └─▸ [HeldBack harbour] 1", + " └─▸ [OutOfTheWay harbour] 1", "▸ [FailedProjects] 1", "▾ [Unconfigured] 1", "▾ [Conflicts] 1", @@ -7108,13 +7108,13 @@ credential_command = "secret harbour" "▾ orbital", " ├─▸ ○ orb-7.1 re-point the dish", " │ └── ! Dangling(1)", - " ├─▸ [HeldBack orbital] 1", + " ├─▸ [OutOfTheWay orbital] 1", " └── [Unattributed orbital] 2", "▾ ferry", - " ├─▸ [HeldBack ferry] 1", + " ├─▸ [OutOfTheWay ferry] 1", " └── [Unattributed ferry] 1", "▾ harbour", - " └─▸ [HeldBack harbour] 1", + " └─▸ [OutOfTheWay harbour] 1", "▸ [FailedProjects] 1", "▾ [Unconfigured] 1", "▾ [Conflicts] 1", @@ -7149,7 +7149,7 @@ credential_command = "secret harbour" .iter() .position(|line| { matches!(&line.content, Content::Group(group) - if group.kind == GroupKind::HeldBack + if group.kind == GroupKind::OutOfTheWay && group.project.as_deref() == Some(project)) }) .unwrap_or_else(|| panic!("no line holds {project} back: {:#?}", sketch(forest))) @@ -7169,7 +7169,7 @@ credential_command = "secret harbour" .iter() .find_map(|line| match &line.content { Content::Group(group) - if group.kind == GroupKind::HeldBack + if group.kind == GroupKind::OutOfTheWay && group.project.as_deref() == Some(project) => { group.held.clone() @@ -7534,7 +7534,7 @@ credential_command = "secret harbour" " │ ├── ! Dangling(1)", " │ ├── ○ .1 true the mount", " │ └── ○ .2 seal the feed horn", - " ├─▸ [HeldBack orbital] 1", + " ├─▸ [OutOfTheWay orbital] 1", ] ); } @@ -7559,7 +7559,7 @@ credential_command = "secret harbour" "▾ orbital", " ├─▸ ○ orb-7.1 re-point the dish", " │ └── ! Dangling(1)", - " ├── [HeldBack orbital] 1", + " ├── [OutOfTheWay orbital] 1", " │ └─▸ ◐ orb-7 lift the ground station", " │ └── ! Dangling(1)", ] @@ -7753,7 +7753,7 @@ credential_command = "secret harbour" sketch(&forest) ); assert!( - drawn_here(&forest, "[HeldBack orbital]"), + drawn_here(&forest, "[OutOfTheWay orbital]"), "still rooted at one bead: {:#?}", sketch(&forest) ); diff --git a/src/view/lines.rs b/src/view/lines.rs index fe6f6ac..cc732f4 100644 --- a/src/view/lines.rs +++ b/src/view/lines.rs @@ -216,8 +216,8 @@ pub struct Group { /// known by, so a fold on one project's group is not a fold on another's. pub project: Option, pub count: usize, - /// What the roots this group is holding back add up to, for the group that - /// holds whole roots back because the forest is rooted at one bead. + /// What the roots behind this group's line add up to, for the group that + /// holds whole roots because the forest is rooted at one bead. /// /// Nothing for the groups that hold things rather than roots: a pane, a /// failed project and a conflict are each drawn whole when the group opens, @@ -242,8 +242,8 @@ pub enum GroupKind { Unconfigured, Conflicts, /// The roots the forest is not drawing because the reader rooted it at one - /// bead. *coined* - HeldBack, + /// bead. + OutOfTheWay, HiddenTrees, Unattributed, } @@ -264,7 +264,7 @@ impl GroupKind { /// its paths that no bead claims. Everything beneath a project is under /// its one line, so a reader has one place to look. pub const UNDER_A_PROJECT: [GroupKind; 3] = [ - GroupKind::HeldBack, + GroupKind::OutOfTheWay, GroupKind::HiddenTrees, GroupKind::Unattributed, ]; @@ -277,7 +277,7 @@ impl GroupKind { pub(crate) fn live(self) -> bool { match self { GroupKind::Unconfigured | GroupKind::Conflicts | GroupKind::Unattributed => true, - GroupKind::FailedProjects | GroupKind::HiddenTrees | GroupKind::HeldBack => false, + GroupKind::FailedProjects | GroupKind::HiddenTrees | GroupKind::OutOfTheWay => false, } } } diff --git a/src/view/phrase.rs b/src/view/phrase.rs index 6e068d1..6060663 100644 --- a/src/view/phrase.rs +++ b/src/view/phrase.rs @@ -469,19 +469,21 @@ pub fn conflicts(count: usize) -> String { format!("{count} {conflict} nothing could settle") } -/// The roots the forest is not drawing because the reader rooted it at one +/// The beads the forest is not drawing because the reader rooted it at one /// bead. /// -/// *Held back* rather than hidden: a key put them behind this line and the -/// same key brings them back, so nothing here has gone wrong and nothing here -/// is the tracker's doing. +/// *Beads* rather than roots, which is the word a reader has for them. Each +/// one counted here is a root of the forest, and the bead the mode stands on +/// is not among them — the root it stands in is, for the part of it the mode +/// stopped drawing, and that root is a bead other than the focused one like +/// any of the rest. /// -/// Not *other*, either. The root the focused bead stands in is behind this -/// line as well, for the part of it the mode stopped drawing, and every root -/// counted here has its own row nowhere but behind this line. -pub fn held_back(count: usize) -> String { - let root = if count == 1 { "root" } else { "roots" }; - format!("{count} {root} held back") +/// Nothing here says why they are behind the line. Why the reader asked for +/// one bead is theirs, and a line guessing at it would be a claim this +/// program cannot make. +pub fn other_beads(count: usize) -> String { + let bead = if count == 1 { "bead" } else { "beads" }; + format!("{count} other {bead}") } /// Trees the live-agent filter is holding back, and how many of those carry