Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 46 additions & 4 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,47 @@ which stops being true the moment something new is under it. Only hand-folds
are stored; the default is derived, which is what makes "back to the default"
a single key.

**`Shift+F` roots the forest at one bead, and that is the other place a reader
asks by name.** The key draws the selected bead where a root is drawn, what the
forest draws beneath that bead anywhere else, and nothing else: no other root
and no other project's tree. It is for finishing one bead, so what it takes
away is everything a reader does not have to do in order to close the one they
are on. Everything beneath that bead is already in the tree it is drawn in, so
this is a change to what the layout walks rather than to what was collected,
and the bead keeps the place it has everywhere else — which is what lets a fold
set on it survive the key both ways. The default above rejects
collapsed-except-selected and the rejection stands: the selection has no
bearing on what is open, and it has none here either, because the mode stands
on the bead named at the keystroke and moving about under it moves nothing.
Pressing the key again puts the forest back, with the selection on the bead it
was rooted at, opening whatever has been shut over that bead in the meantime. The bead leaving the collection is the one thing that ends the
mode on its own; a bead that closes is still in the collection, so closing the
focused bead does not, and a bead the tracker has moved is followed to where it
moved to. Everything the mode stops drawing goes behind
one collapsed line per project rather than off the screen, which is *degrade,
never disappear* binding here as everywhere: every other root and every other
project's, and the focused bead's own root for the part of it that is left —
the beads above that bead and every branch off them, with the bead itself left
to the root of the forest rather than drawn a second time. That line stands over open work
with seats on it, which the filter's own line never does, so it says how many
seats and how many beads want looking at and never claims there are none. It
counts the beads it is standing over rather than the roots they came from,
because the beads on the screen are not behind it. Its
own project's line keeps it, so a reader opens the project where it already
was. It is a group like the others: it rests shut, the keys that open a group
open it, and the roots inside it rest shut as the filter's do. A bead in one of
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
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.

**A finished branch draws as one line and rests shut.** Its glyph, its
fraction and its fold marker already say *finished, and holds more*; opening it
is the ordinary fold. A closed bead standing over unfinished work is the
Expand Down Expand Up @@ -2287,10 +2328,10 @@ holds, so a group over live panes — unconfigured, conflicts, unattributed —
rests open; a group that reports on the reading rather than on work in flight —
failed projects, hidden trees — rests shut, and hidden trees in particular
holds trees hidden *because* nothing live is in them, so opening it would
contradict the rule it exists to serve. Hidden trees is the one group nothing
went wrong in — the filter put them there and a key takes them back out — so it
is the only one drawn without a warning, and the only line on the screen that
names a key: *a to show all*. A group that said only how many trees it hides
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
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
Expand Down Expand Up @@ -2364,6 +2405,7 @@ The bindings are vim-like, with the arrows as aliases:
| `Space` | fold or unfold the selected node |
| `a` | show every tree, not only those with a live agent |
| `?` | show these key bindings |
| `F` | draw the selected bead as the only root, or put the forest back |
| `/` | find part of a bead's id or title, wherever the forest draws it |
| `n` | go to the next bead matching the search |
| `N` | go to the one before it |
Expand Down
1 change: 1 addition & 0 deletions src/tui/drive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,7 @@ fn answered(
| Action::CollapseSubtree
| Action::RestoreDefault
| Action::ToggleFilter
| Action::FocusForest
| Action::Search
| Action::NextMatch
| Action::PreviousMatch,
Expand Down
8 changes: 8 additions & 0 deletions src/tui/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ pub(super) const BINDINGS: &[Binding] = &[
does: "show these key bindings",
hint: Some("keys"),
},
Binding {
keys: &[alone(KeyCode::Char('F'), "F")],
action: Action::FocusForest,
does: "draw the selected bead as the only root, or put the forest back",
hint: None,
},
Binding {
keys: &[alone(KeyCode::Char('/'), "/")],
action: Action::Search,
Expand Down Expand Up @@ -338,6 +344,7 @@ pub(super) mod tests {
Action::RestoreDefault,
Action::ToggleFilter,
Action::Focus,
Action::FocusForest,
Action::ShowBead,
Action::NextRelated,
Action::Back,
Expand Down Expand Up @@ -368,6 +375,7 @@ pub(super) mod tests {
| Action::RestoreDefault
| Action::ToggleFilter
| Action::Focus
| Action::FocusForest
| Action::ShowBead
| Action::NextRelated
| Action::Back
Expand Down
19 changes: 16 additions & 3 deletions src/tui/screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1195,7 +1195,7 @@ mod tests {
" Space fold or unfold the selected node",
" a show every tree, not only those with a live agent",
" ? show these key bindings",
" … 19 more bindings · no room on a screen this short",
" … 20 more bindings · no room on a screen this short",
]
);
}
Expand Down Expand Up @@ -1302,6 +1302,7 @@ mod tests {
" Space fold or unfold the selected node",
" a show every tree, not only those with a live agent",
" ? show these key bindings",
" F draw the selected bead as the only root, or put the forest back",
" / find part of a bead's id or title, wherever the forest draws it",
" n go to the next bead matching the search",
" N go to the one before it",
Expand Down Expand Up @@ -1344,15 +1345,27 @@ mod tests {

let drawn = window_inner(40, tall);

assert_eq!(drawn[8], " q, ^C quit");
assert_eq!(row_naming(&drawn, "q, ^C"), " q, ^C quit");
assert_eq!(
drawn[0], " Enter show the selected bead, o…",
"a line too long for forty columns, cut with the cut marked"
);
assert_eq!(drawn[18], " Right, l expand, or move to the fi…");
assert_eq!(
row_naming(&drawn, "Right, l"),
" Right, l expand, or move to the fi…"
);
assert_eq!(drawn.len(), BINDINGS.len(), "a narrow screen loses no rows");
}

/// The one drawn row whose keys are the ones named, whichever row that
/// turns out to be.
fn row_naming<'a>(drawn: &'a [String], keys: &str) -> &'a str {
drawn
.iter()
.find(|row| row.trim_start().starts_with(keys))
.unwrap_or_else(|| panic!("no row names {keys}: {drawn:#?}"))
}

/// The row is cut from its own end, so one that outgrew the narrowest
/// screen anyone uses would lose `q quit` — which is what a reader who
/// cannot get out is looking for. `bdi-2bb.12` cut `^R` from it to make
Expand Down
158 changes: 144 additions & 14 deletions src/view/draw/groups.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,26 @@ use crate::view::fitted::{Fitted, GAP};
use crate::view::lines::{Group, GroupKind, Item};
use crate::view::palette;
use crate::view::phrase;
use crate::view::row::WARNING;
use crate::view::row::{AGENT, WARNING};

use super::{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";

/// The line a group is drawn as. The hidden trees are the only group nothing
/// went wrong in — the filter put them there and a key takes them back out —
/// so they are the only one drawn without a warning.
/// What brings back the roots that rooting the forest at one bead put behind
/// a line.
const WHOLE_FOREST: &str = "F for the whole forest";

/// The line a group is drawn as. Two of them hold roots nothing went wrong in
/// — a key put them there and a key takes them back out — so those two are
/// the ones drawn without a warning.
pub(super) fn group_line(prefix: &str, group: &Group) -> Fitted {
let (said, hidden) = match group.kind {
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::Unattributed => (phrase::unattributed(group.count), false),
GroupKind::Unconfigured => (phrase::unconfigured(group.count), false),
};
Expand All @@ -31,17 +36,58 @@ pub(super) fn group_line(prefix: &str, group: &Group) -> Fitted {
} else {
(format!("{WARNING} {said}"), palette::ATTENTION)
};
let state = if hidden {
vec![Span::styled(SHOW_ALL, palette::QUIET)]
} else {
Vec::new()
let state = match group.kind {
GroupKind::HiddenTrees => vec![Span::styled(SHOW_ALL, palette::QUIET)],
GroupKind::HeldBack => held_back_state(group),
_ => Vec::new(),
};

Fitted::new(
let line = Fitted::new(
vec![Span::raw(prefix.to_string()), Span::styled(said, style)],
Vec::new(),
state,
)
);
match group.kind {
GroupKind::HeldBack => line.briefly(held_back_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.
///
/// The reader asked for one bead, so the roots 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<Span<'static>> {
let mut said = held_back_counts(group);
said.push(Span::styled(WHOLE_FOREST, palette::QUIET));
said
}

/// The same, where the row cannot afford the key as well.
///
/// 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<Span<'static>> {
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,
));
}
if counts.anomalies > 0 {
said.push(Span::styled(
format!("{WARNING} {}", phrase::anomalies_beneath(counts.anomalies)),
palette::ATTENTION,
));
}
said
}

/// The scope, where the directory chose it. Nothing went wrong, so it is
Expand Down Expand Up @@ -140,6 +186,7 @@ mod tests {
use crate::model::snapshot::{a_provider, build, Collected, LoosePane, UnconfiguredPane};
use crate::model::types::testing::{key as pane_key, A_SESSION};
use crate::model::types::PaneStatus;
use crate::view::fitted::CUT;
use chrono::{TimeZone, Utc};
use pretty_assertions::assert_eq;
use ratatui::style::Color;
Expand Down Expand Up @@ -170,6 +217,7 @@ mod tests {
project: Some("summit-works".into()),
count: 4,
with_findings: 0,
held: None,
};
let broken = Group {
with_findings: 2,
Expand Down Expand Up @@ -231,23 +279,104 @@ mod tests {
);
}

/// Every other group is something that went wrong, and is marked as such.
/// The hidden trees are not: the user asked for them to be hidden.
/// The reader rooted the forest at one bead and everything else went
/// 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.
#[test]
fn a_line_over_held_back_roots_says_its_seats_and_what_wants_looking_at() {
let group = Group {
kind: GroupKind::HeldBack,
project: Some("summit-works".into()),
count: 3,
with_findings: 0,
held: Some(Counts {
total: 12,
closed: 4,
live_agents: 2,
anomalies: 1,
}),
};

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:?}");
}

/// A row with no room for the way out says the counts without it. The
/// counts are what a reader can read nowhere else on the screen, and a
/// key named in part is a key they cannot press.
#[test]
fn a_row_too_narrow_for_the_way_out_still_says_what_is_back_there() {
let group = Group {
kind: GroupKind::HeldBack,
project: Some("summit-works".into()),
count: 3,
with_findings: 0,
held: Some(Counts {
total: 12,
closed: 4,
live_agents: 2,
anomalies: 1,
}),
};

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("F for"), "{drawn:?}");
assert!(!drawn[0].contains(CUT), "{drawn:?}");
}

/// 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() {
let group = Group {
kind: GroupKind::HeldBack,
project: Some("summit-works".into()),
count: 1,
with_findings: 0,
held: Some(Counts {
total: 9,
closed: 9,
live_agents: 0,
anomalies: 0,
}),
};

let drawn = Painted::of(group_line(SHUT, &group), 120, 1).rows();

assert!(drawn[0].contains("1 root held back"), "{drawn:?}");
assert!(!drawn[0].contains("beneath"), "{drawn:?}");
assert!(drawn[0].contains("F for the whole forest"), "{drawn:?}");
}

/// A group holding something that went wrong is marked as such. The two
/// that hold whole roots are not: the reader asked for those roots to be
/// out of the way, one with the filter and one with the key that roots the
/// forest at a bead.
#[test]
fn only_the_group_nothing_went_wrong_in_is_drawn_without_a_warning() {
fn only_a_group_holding_roots_the_reader_put_away_is_drawn_without_a_warning() {
for kind in every_kind() {
let group = Group {
kind,
project: None,
count: 2,
with_findings: 0,
held: None,
};
let drawn = Painted::of(group_line(SHUT, &group), 80, 1).rows();
let marked = drawn[0].contains(WARNING);

assert_eq!(
marked,
kind != GroupKind::HiddenTrees,
!matches!(kind, GroupKind::HiddenTrees | GroupKind::HeldBack),
"{kind:?}: {drawn:?}"
);
}
Expand Down Expand Up @@ -279,6 +408,7 @@ mod tests {
project: None,
count: 2,
with_findings: 0,
held: None,
};
let painted = Painted::of(group_line(SHUT, &group), 80, 1).row(0);

Expand Down
Loading