diff --git a/docs/design.md b/docs/design.md index 431b39b..c8b9f21 100644 --- a/docs/design.md +++ b/docs/design.md @@ -967,10 +967,7 @@ them is reached by the keys that reach any bead, the line opening to let the selection in, so looking a bead up in another root is not paid for with the put-back key. A search offers what it can take the reader to, which under this mode is every bead the forest holds: the ones beneath the focused bead, and the -ones behind the line, which it opens on the way. The bead the forest is rooted -at reads its whole id where a root would, and the beads under it read as its -children: a column of shortened ids is read by putting the drawn root in front -of each one, and the root this branch came out of is behind the line. It names the key +ones behind the line, which it opens on the way. It names the key that brings them back, because it is the line a reader is looking at when they want them back, and a row with no room for that hint gives it up whole rather than cutting it to a key nobody can press. @@ -1985,13 +1982,16 @@ glyph, literal because `bd`'s are and do not move with the terminal's theme escape for it and inheriting is what lets a row's own brightness reach its glyph. -**An id is shown as what it adds to its root's**, `.20` for `smt-4kd3p.20`, -and kept whole where it does not carry that prefix followed by a dot — the -dangling and re-parented nodes, and the root itself — because a bare suffix -would place it under a root it does not belong to. **It is drawn in the colour -of the glyph beside it**, so a status arrives as a block a reader finds rather -than as the one column a glyph occupies; an open bead's id goes without a -colour for the same reason its glyph does. +**An id is shown as what it adds to the id above it**, `.20` for +`smt-4kd3p.20` drawn under `smt-4kd3p`, and kept whole where the row above it +does not carry its id followed by a dot — the dangling and re-parented nodes, +and a row with nothing above it — because a bare suffix would place it under a +parent it does not belong to. The rule holds at every depth, so walking up the +rows and joining what each says gives the whole id back, which is what makes a +column of them readable. **It is drawn in the colour of the glyph beside it**, +so a status arrives as a block a reader finds rather than as the one column a +glyph occupies; an open bead's id goes without a colour for the same reason its +glyph does. **A ground and two tiers say how live a row is**, which is the one thing about a bead `bd list` has no way to know and so the one thing this scale is diff --git a/src/view/draw/mod.rs b/src/view/draw/mod.rs index a6b0e6a..86a51a3 100644 --- a/src/view/draw/mod.rs +++ b/src/view/draw/mod.rs @@ -373,7 +373,7 @@ mod tests { } pub(super) fn row(node: &Node) -> Row { - row::cells(node, "smt-4kd3p", None, None) + row::cells(node, Some("smt-4kd3p"), None, None) } /// A project whose roots all read, so its line is its name and its counts. diff --git a/src/view/forest/layout.rs b/src/view/forest/layout.rs index 9e46114..dd40fd7 100644 --- a/src/view/forest/layout.rs +++ b/src/view/forest/layout.rs @@ -682,7 +682,7 @@ impl TreeLayout<'_> { place: Some(root.clone()), content: Content::Bead(row::cells( node, - self.shortened_against(), + None, bead.progress, shut_over(bead.beneath, first_copy(self.tree, at, above), folded), )), @@ -769,7 +769,7 @@ impl TreeLayout<'_> { place: Some(place.clone()), content: Content::Bead(row::cells( node, - self.shortened_against(), + Some(&parent.key().id), bead.progress, shut_over(bead.beneath, first, folded), )), @@ -805,18 +805,6 @@ impl TreeLayout<'_> { Some(link.bead) != self.without } - /// The id every row here is shortened against: the bead this drawing - /// starts at. A reader reads a column of suffixes by putting the drawn - /// root in front of each one, so the bead the forest is rooted at reads - /// whole and what hangs under it reads against that. The root it came out - /// of is behind the line, shortening what is drawn there. - fn shortened_against(&self) -> &str { - match self.rooted { - Some(rooted) => &rooted.place.steps.last().unwrap_or(&rooted.place.tree).id, - None => &self.tree.root, - } - } - /// What a run stands for: its members and everything beneath them. Walked /// again where a bead is being left out, because the tree's own answer was /// worked out over a run this drawing is not making and reaches beads it diff --git a/src/view/forest/mod.rs b/src/view/forest/mod.rs index 2fb1945..b230f41 100644 --- a/src/view/forest/mod.rs +++ b/src/view/forest/mod.rs @@ -2581,8 +2581,8 @@ credential_command = "secret harbour" "▾ orbital", " └── ○ tow-1 raise the tower", " ├── ○ .1 stand the mast", - " │ └── ○ .1.1 bolt the sections", - " │ └── ○ .1.1.1 dress the cables", + " │ └── ○ .1 bolt the sections", + " │ └── ○ .1 dress the cables", " └─▸ ○ .2 pour the base", ]; @@ -2625,8 +2625,8 @@ credential_command = "secret harbour" "▾ orbital", " └── ○ tow-1 raise the tower", " ├── ○ .1 stand the mast", - " │ └── ○ .1.1 bolt the sections", - " │ └── ◐ .1.1.1 dress the cables", + " │ └── ○ .1 bolt the sections", + " │ └── ◐ .1 dress the cables", " └─▸ ○ .2 pour the base", ] ); @@ -2696,7 +2696,7 @@ credential_command = "secret harbour" assert!( sketch(&forest) .iter() - .any(|line| line.contains(".1.1.1 dress the cables")), + .any(|line| line.contains(".1 dress the cables")), "{:#?}", sketch(&forest) ); @@ -3204,10 +3204,10 @@ credential_command = "secret harbour" " └── ◐ rly-2 re-site the relay", " ├── ○ .1 trench the run", " ├── ✓ .2 strike the old mast", - " │ └── ✓ .2.1 drop the guys", - " │ └── ◐ .2.1.1 cut the stays", + " │ └── ✓ .1 drop the guys", + " │ └── ◐ .1 cut the stays", " ├── ✓ .4 lift the feeder", - " │ └── ✓ .4.1 coil the heliax", + " │ └── ✓ .1 coil the heliax", " └─▸ … 4 more", ] ); @@ -3608,9 +3608,9 @@ credential_command = "secret harbour" " └── ◐ sdg-4 re-point the crossover", " ├─▸ ◐ .3 re-signal the box", " ├── ✓ .1 slew the up line", - " │ ├── ○ .1.2 weld the closure rail", - " │ ├─▸ ✓ .1.1 key the switch", - " │ └── ✓ .1.3 lift the old chairs", + " │ ├── ○ .2 weld the closure rail", + " │ ├─▸ ✓ .1 key the switch", + " │ └── ✓ .3 lift the old chairs", " └─▸ ✓ .2 clip the down line", ] ); @@ -5447,7 +5447,7 @@ credential_command = "secret harbour" " └── ◐ slu-1 rehang the sluice", " ├─▸ ◐ .1 forge the new pintles", " └── ○ .2 hang the gate", - " └┄▸ ◐ .1 forge the new pintles", + " └┄▸ ◐ slu-1.1 forge the new pintles", ] ); @@ -5459,14 +5459,53 @@ credential_command = "secret harbour" "▾ orbital", " └── ◐ slu-1 rehang the sluice", " ├── ◐ .1 forge the new pintles", - " │ └── ○ .1.1 cast the pintle blanks", + " │ └── ○ .1 cast the pintle blanks", " └── ○ .2 hang the gate", - " └┄┄ ◐ .1 forge the new pintles", - " └── ○ .1.1 cast the pintle blanks", + " └┄┄ ◐ slu-1.1 forge the new pintles", + " └── ○ .1 cast the pintle blanks", ] ); } + /// A column of short ids is only readable if a reader can rebuild the + /// whole one from it: they put what the row above says in front of what + /// this row says, and stop at a row that reads whole because there is + /// nothing left to put in front of it. Over every bead these fixtures + /// draw, that walk gives back the id `bd` holds it under. + #[test] + fn walking_up_the_rows_and_joining_the_ids_gives_a_beads_whole_id_back() { + let fixtures = [ + flatten(snapshot()), + flatten(tower_staffed(&["tow-1.1.1.1"])), + flatten(alone("orbital", SLUICE, &panes_on(&["slu-1.1"]))), + flatten(alone("orbital", RELAY, &two_panes())), + flatten(alone("orbital", SIDING, &panes_on(&["sdg-4.3"]))), + ]; + + let mut shortened = 0; + for mut forest in fixtures { + forest.apply(Action::ExpandSubtree); + let mut above: Vec = Vec::new(); + for line in forest.lines() { + let (Some(place), Content::Bead(row)) = (&line.place, &line.content) else { + continue; + }; + above.truncate(place.steps.len()); + let rebuilt = match above.last() { + Some(parent) if row.id.starts_with('.') => format!("{parent}{}", row.id), + _ => row.id.clone(), + }; + + assert_eq!(rebuilt, place.key().id, "{:#?}", sketch(&forest)); + + shortened += usize::from(row.id.starts_with('.')); + above.push(rebuilt); + } + } + + assert!(shortened > 0, "no row was drawn short to walk up from"); + } + /// Four columns a level, every line in a forest with trees in it, /// whatever that line is doing. /// @@ -6556,13 +6595,18 @@ credential_command = "secret harbour" /// The forest row is the one place `bdi` ever prints a shortened id — /// `row::abbreviate` has no other caller — so on a long screen the short - /// id is the only spelling the reader has been shown. A substring is what - /// reaches it: `.1.1` is a prefix of nothing. + /// id is the only spelling the reader has been shown, and what they type + /// has to reach the bead they read it off. The query is taken from the row + /// rather than written out here, because a search matches on the whole id + /// and a query spelled by hand would pass on that alone. + /// + /// A substring is what reaches it: the drawn form is a prefix of nothing. #[test] fn a_search_matches_the_shortened_id_the_row_draws() { let mut forest = flatten(snapshot()); + let drawn = row_of(&forest, "orb-7.1").id.clone(); - assert_eq!(forest.seek(".1.1"), went_to("orbital", "orb-7.1.1", 1, 1)); + assert_eq!(forest.seek(&drawn), went_to("orbital", "orb-7.1", 1, 4)); } #[test] diff --git a/src/view/row.rs b/src/view/row.rs index 1cc6cd0..dcfd321 100644 --- a/src/view/row.rs +++ b/src/view/row.rs @@ -90,7 +90,7 @@ pub fn says_the_same_about_its_link(badge: &Badged, said: &str) -> bool { pub fn cells( node: &Node, - root: &str, + above: Option<&str>, progress: Option, shut_over: Option, ) -> Row { @@ -137,7 +137,7 @@ pub fn cells( Row { status: node.status.clone(), glyph: status_glyph(&node.status), - id: abbreviate(&node.id, root).to_string(), + id: abbreviate(&node.id, above).to_string(), title: node.title.clone(), badges: node.badges.clone(), progress, @@ -176,12 +176,17 @@ pub fn status_glyph(status: &Status) -> char { } } -/// A node's id with its root's prefix dropped, which is what makes a column of -/// ids readable. A node that does not carry the root's prefix keeps its whole -/// id: a bare suffix would place it under a root it does not belong to, and -/// the dangling and re-parented nodes are exactly the ones that would lie. -pub fn abbreviate<'a>(id: &'a str, root: &str) -> &'a str { - id.strip_prefix(root) +/// A node's id with the part the id above it already spells dropped, which is +/// what makes a column of ids readable: walking up the rows and joining what +/// they say gives the whole id back. A node whose parent's id is not the front +/// of its own keeps its whole id, because a bare suffix would place it under a +/// parent it does not belong to, and the dangling and re-parented nodes are +/// exactly the ones that would lie. A node with nothing above it — a tree's +/// root, or the bead a rooted forest starts at — has nothing to measure +/// against and is drawn whole. +pub fn abbreviate<'a>(id: &'a str, above: Option<&str>) -> &'a str { + above + .and_then(|above| id.strip_prefix(above)) .filter(|rest| rest.starts_with('.')) .unwrap_or(id) } @@ -322,28 +327,34 @@ mod tests { staffed.agent = Some(agent(JoinSource::AgentPane)); assert_eq!( - cells(&staffed, ROOT, None, None).glyph, - cells(&node("smt-4kd3p.20", Status::InProgress), ROOT, None, None).glyph + cells(&staffed, Some(ROOT), None, None).glyph, + cells( + &node("smt-4kd3p.20", Status::InProgress), + Some(ROOT), + None, + None + ) + .glyph ); } #[test] - fn a_node_under_the_root_shows_only_what_it_adds_to_it() { - assert_eq!(abbreviate("smt-4kd3p.20", ROOT), ".20"); - assert_eq!(abbreviate("smt-4kd3p.1.4", ROOT), ".1.4"); + fn a_node_shows_only_what_it_adds_to_the_id_above_it() { + assert_eq!(abbreviate("smt-4kd3p.20", Some(ROOT)), ".20"); + assert_eq!(abbreviate("smt-4kd3p.1.4", Some("smt-4kd3p.1")), ".4"); } - /// A dangling or re-parented node is drawn under a root it does not - /// descend from, and a bare suffix there would say it does. + /// A dangling or re-parented node is drawn under a bead it does not descend + /// from, and a bare suffix there would say it does. #[test] - fn a_node_that_does_not_descend_from_the_root_keeps_its_whole_id() { - assert_eq!(abbreviate("mdw-6qzt4.3", ROOT), "mdw-6qzt4.3"); - assert_eq!(abbreviate("smt-4kd3pX.3", ROOT), "smt-4kd3pX.3"); + fn a_node_that_does_not_descend_from_the_id_above_it_keeps_its_whole_id() { + assert_eq!(abbreviate("mdw-6qzt4.3", Some(ROOT)), "mdw-6qzt4.3"); + assert_eq!(abbreviate("smt-4kd3pX.3", Some(ROOT)), "smt-4kd3pX.3"); } #[test] - fn the_root_keeps_its_whole_id() { - assert_eq!(abbreviate(ROOT, ROOT), ROOT); + fn a_node_with_nothing_above_it_keeps_its_whole_id() { + assert_eq!(abbreviate(ROOT, None), ROOT); } #[test] @@ -418,7 +429,7 @@ mod tests { #[test] fn a_bead_with_nothing_wrong_carries_no_marker_at_all() { - let row = cells(&node("smt-4kd3p.20", Status::Open), ROOT, None, None); + let row = cells(&node("smt-4kd3p.20", Status::Open), Some(ROOT), None, None); assert_eq!(row.anomalies, None); assert_eq!(row.agent, None); @@ -454,7 +465,7 @@ mod tests { key: "delivery_pr".into(), }]; - let row = cells(&unfilled, ROOT, None, None); + let row = cells(&unfilled, Some(ROOT), None, None); assert!( row.notes.iter().any(|note| note.contains("delivery_pr")), @@ -471,7 +482,7 @@ mod tests { Some(&format!("https://forge.invalid/orbital{HOSTILE}/pull/12")), ); - let row = cells(&hostile, ROOT, None, None); + let row = cells(&hostile, Some(ROOT), None, None); assert!( row.notes.iter().any(|note| note.contains("delivery_pr")), @@ -488,7 +499,7 @@ mod tests { Some("https://forge.invalid/orbital/atlas/pull/12"), ); - let row = cells(&hostile, ROOT, None, None); + let row = cells(&hostile, Some(ROOT), None, None); assert!( row.notes.iter().any(|note| note.contains("delivery_pr")), @@ -505,7 +516,7 @@ mod tests { let mut hostile = badged("⇢ atlas #12", Some(SOMEWHERE)); hostile.badges[0].short = Some(format!("⇢ #12{HOSTILE}")); - let row = cells(&hostile, ROOT, None, None); + let row = cells(&hostile, Some(ROOT), None, None); assert!( row.notes @@ -522,7 +533,7 @@ mod tests { let mut unlinked = badged("⏸ waiting", None); unlinked.badges[0].short = Some(format!("⏸{HOSTILE}")); - let row = cells(&unlinked, ROOT, None, None); + let row = cells(&unlinked, Some(ROOT), None, None); assert_eq!(row.notes, Vec::::new()); } @@ -531,7 +542,7 @@ mod tests { fn a_badge_that_draws_its_link_leaves_nothing_on_the_row() { let linked = badged("⇢ #12", Some(SOMEWHERE)); - let row = cells(&linked, ROOT, None, None); + let row = cells(&linked, Some(ROOT), None, None); assert_eq!(row.notes, Vec::::new()); } @@ -544,7 +555,7 @@ mod tests { let mut both = badged("⇢ atlas #12", Some(SOMEWHERE)); both.badges[0].short = Some("⇢ #12".into()); - let row = cells(&both, ROOT, None, None); + let row = cells(&both, Some(ROOT), None, None); assert_eq!(row.notes, Vec::::new()); } @@ -553,7 +564,7 @@ mod tests { /// lose, so it leaves no note however it renders. #[test] fn a_badge_with_no_link_leaves_nothing_on_the_row() { - let row = cells(&badged("⏸ waiting", None), ROOT, None, None); + let row = cells(&badged("⏸ waiting", None), Some(ROOT), None, None); assert_eq!(row.notes, Vec::::new()); } @@ -561,7 +572,7 @@ mod tests { #[test] fn a_status_outside_bds_own_set_leaves_the_word_bd_used_on_the_row() { let odd = node("smt-4kd3p.20", Status::Other("triage".into())); - let row = cells(&odd, ROOT, None, None); + let row = cells(&odd, Some(ROOT), None, None); assert_eq!(row.glyph, '?'); assert!( @@ -590,7 +601,7 @@ mod tests { }, ]; - let row = cells(&badged, ROOT, None, None); + let row = cells(&badged, Some(ROOT), None, None); let drawn: Vec<&str> = row.badges.iter().map(|b| b.text.as_str()).collect(); assert_eq!(drawn, vec!["⇢ #12", "⏸ waiting"]); @@ -620,7 +631,7 @@ mod tests { }, ]; - let row = cells(&badged, ROOT, None, None); + let row = cells(&badged, Some(ROOT), None, None); assert_eq!( row.badges[0].link.as_deref(), @@ -636,7 +647,12 @@ mod tests { #[test] fn a_row_says_what_the_bead_says() { - let row = cells(&node("smt-4kd3p.20", Status::Blocked), ROOT, None, None); + let row = cells( + &node("smt-4kd3p.20", Status::Blocked), + Some(ROOT), + None, + None, + ); assert_eq!(row.status, Status::Blocked); assert_eq!(row.glyph, '●');