Skip to main content

max / audiofiles

Group the described shortcuts table under the shipped tab's headings cf7872dc, and it takes quasi 0.54. The described table was four rows for five days because Runtime::pressed_binding read raw input with no focus guard, so a bare letter declared here was eaten out of the tag field and the search box. quasi-immediate 0.52.0 put the guard in the renderer and the table went to thirteen, which is the same wall ui::overlays::draw_shortcuts_tab breaks into seven hand-grouped arrays -- the shipped tab is the evidence that somebody had already decided a flat list that long is not a reference. Chrome::bind_in says the heading, Chrome::grouped gathers the runs in the order they were first bound, and shortcuts() draws a Node::section plus a Node::Table per group inside a new help-shortcuts region. Several tables rather than one with a group column: a group is a heading, and a column repeating "Toggles" six times says the same thing six times and is still one wall. Four of the seven headings have rows -- Bulk, Discovery, Toggles, System. The other three are the nine-of-sixteen shortcuts this module's header already lists as not the guard's business, not something the member cannot say. The group is a listing fact and nothing else: bound() answers exactly what it did, which the test asserts beside the headings. f and shift+f were declared adjacent while the table was flat, because that adjacency is what made the renderer's matching exact. The grouping separates them and the collision is unchanged, since matching reads the key rather than the neighbour. the_help_overlay_lists_exactly_the_keys_that_are_bound now reads across the groups: table_of answers with the first table it finds, which after this would have asserted about Bulk and called it the whole screen. Also takes quasi 0.54 for f35aafee (Act::fills), which this app does not use yet.
Author: Max Johnson <me@maxj.phd> · 2026-08-22 17:44 UTC
Signed with PGP, not checked
Commit: a0d24926b4d03dc7b17eb4f08f7ba64a3f33ba96
Parent: 43d7bba
4 files changed, +223 insertions, -64 deletions
M Cargo.lock +8 -16
@@ -4255,7 +4255,7 @@
4255 4255
4256 4256 [[package]]
4257 4257 name = "quasi-immediate"
4258 - version = "0.53.0"
4258 + version = "0.54.0"
4259 4259 dependencies = [
4260 4260 "docengine",
4261 4261 "egui",
@@ -4265,7 +4265,7 @@
4265 4265
4266 4266 [[package]]
4267 4267 name = "quasi-router"
4268 - version = "0.53.0"
4268 + version = "0.54.0"
4269 4269 dependencies = [
4270 4270 "makeover-layout",
4271 4271 ]
@@ -7568,19 +7568,19 @@
7568 7568
7569 7569 [[patch.unused]]
7570 7570 name = "quasi-axum"
7571 - version = "0.53.0"
7571 + version = "0.54.0"
7572 7572
7573 7573 [[patch.unused]]
7574 7574 name = "quasi-basics"
7575 - version = "0.53.0"
7575 + version = "0.54.0"
7576 7576
7577 7577 [[patch.unused]]
7578 7578 name = "quasi-http"
7579 - version = "0.53.0"
7579 + version = "0.54.0"
7580 7580
7581 7581 [[patch.unused]]
7582 7582 name = "quasi-notifs"
7583 - version = "0.53.0"
7583 + version = "0.54.0"
7584 7584
7585 7585 [[patch.unused]]
7586 7586 name = "quasi-store"
@@ -7588,19 +7588,11 @@
7588 7588
7589 7589 [[patch.unused]]
7590 7590 name = "quasi-tauri"
7591 - version = "0.53.0"
7591 + version = "0.54.0"
7592 7592
7593 7593 [[patch.unused]]
7594 7594 name = "quasi-webview"
7595 - version = "0.53.0"
7596 -
7597 - [[patch.unused]]
7598 - name = "makeover-build"
7599 - version = "0.50.0"
7600 -
7601 - [[patch.unused]]
7602 - name = "makeover-webview"
7603 - version = "0.58.0"
7595 + version = "0.54.0"
7604 7596
7605 7597 [[patch.unused]]
7606 7598 name = "quasi-type"
M Cargo.toml +2 -2
@@ -25,8 +25,8 @@
25 25 makeover-timing = "0.1"
26 26 # The described screens, behind audiofiles-browser's `quasi` feature. By git URL
27 27 # with a version requirement, per the tree's rule for cross-repo deps.
28 - quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.53" }
29 - quasi-immediate = { git = "https://makenot.work/git/max/quasi.git", version = "0.53" }
28 + quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.54" }
29 + quasi-immediate = { git = "https://makenot.work/git/max/quasi.git", version = "0.54" }
30 30 egui = { version = "0.35", default-features = false, features = ["default_fonts"] }
31 31 egui_extras = { version = "0.35", default-features = false }
32 32 eframe = { version = "0.35", default-features = false, features = ["default_fonts", "glow"] }
@@ -56,10 +56,15 @@
56 56 //!
57 57 //! # One more thing this screen could not say
58 58 //!
59 - //! - **A `Binding` has no group.** The shipped tab sorts twenty-six rows into
60 - //! Navigation, Selection, Bulk, Search, Discovery, Toggles and System, which
61 - //! for a list that long is the difference between a reference and a wall. The
62 - //! described one is flat, and at thirteen rows it has started to want them.
59 + //! - **A `Binding` had no group, and now has one.** The shipped tab sorts
60 + //! twenty-six rows into Navigation, Selection, Bulk, Search, Discovery,
61 + //! Toggles and System, which for a list that long is the difference between a
62 + //! reference and a wall. The described one was flat, and at thirteen rows it
63 + //! started to want them. `cf7872dc` answered it: `Chrome::bind_in` says the
64 + //! heading and `Chrome::grouped` gathers the runs, so the table below is four
65 + //! sections in the order this file bound them. Three of the shipped headings
66 + //! have no rows here, which is the nine-of-sixteen above rather than anything
67 + //! the member cannot say.
63 68 //! - **An action cannot sit inside a sentence.** The features tab writes "Use
64 69 //! `/` to focus the search bar" with `/` as a live link that closes the help
65 70 //! and focuses the field. `Node::Link` is a leaf and prose is a `Node::Text`,
@@ -88,6 +93,12 @@
88 93 const BODY: &str = "help-body";
89 94 /// The region a tab's contents land in.
90 95 const TAB: &str = "help-tab";
96 + /// The region the grouped key tables sit in.
97 + ///
98 + /// A region rather than a bare run because a tab's contents are one node, and
99 + /// grouping turned this tab from one table into a heading and a table per
100 + /// group.
101 + const SHORTCUTS_BODY: &str = "help-shortcuts";
91 102
92 103 /// Which tab is showing.
93 104 const SHORTCUTS: &str = "shortcuts";
@@ -106,44 +117,100 @@
106 117 #[must_use]
107 118 pub fn chrome() -> Chrome {
108 119 Chrome::new()
109 - .bind("f1", "Show this help", Action::get("/help"))
110 - .bind("f2", "Rename the selection", Action::get("/bulk/rename"))
111 - .bind("ctrl+t", "Tag the selection", Action::get("/bulk/tag"))
112 - .bind(
120 + // Four of the shipped tab's seven headings, in its order. `cf7872dc`
121 + // landed `Chrome::bind_in`, and the reason this table wanted it is the
122 + // reason the shipped one has arrays: thirteen rows read as a wall, and
123 + // a reader looking for the tagging key should not have to scan past the
124 + // panel toggles to find it.
125 + //
126 + // Navigation, Selection and Search have no rows here. See this module's
127 + // header for the seven shortcuts that are not the guard's business, and
128 + // for `/` -- the one absence that is a gap rather than a shape.
129 + .bind_in(
130 + BULK,
131 + "f2",
132 + "Rename the selection",
133 + Action::get("/bulk/rename"),
134 + )
135 + .bind_in(
136 + BULK,
137 + "ctrl+t",
138 + "Tag the selection",
139 + Action::get("/bulk/tag"),
140 + )
141 + .bind_in(
142 + BULK,
113 143 "ctrl+shift+m",
114 144 "Move the selection",
115 145 Action::get("/bulk/move"),
116 146 )
117 - .bind("ctrl+z", "Undo the last bulk action", Action::post("/undo"))
118 - // The five panels the toolbar toggles, by the name an address is built
119 - // from. `Panel::as_str` is that name, so a panel renamed here and there
120 - // is one edit rather than two.
121 - .bind("s", "Toggle the sidebar", toggling(Panel::Sidebar))
122 - .bind("d", "Toggle the detail panel", toggling(Panel::Detail))
123 - .bind("e", "Toggle the sample editor", toggling(Panel::Edit))
124 - .bind(
125 - "i",
126 - "Toggle the instrument panel",
127 - toggling(Panel::Instrument),
147 + .bind_in(
148 + BULK,
149 + "ctrl+z",
150 + "Undo the last bulk action",
151 + Action::post("/undo"),
128 152 )
129 - .bind("l", "Toggle loop", toggling(Panel::Loop))
130 - // The forge is a screen rather than a panel, which is why this one is
131 - // not built the same way as the five above it.
132 - .bind("f", "Open the sample forge", Action::get("/forge"))
133 - // Shift's two, and the pair that made the renderer's matching exact:
134 - // `shift+f` sits directly behind `f` in this table.
135 - .bind(
153 + // Shift's two. They were declared beside `f` while this table was flat,
154 + // because `shift+f` sitting directly behind it is what made the
155 + // renderer's matching exact (quasi-immediate 0.52.0); the grouping
156 + // separates them and the collision is unchanged, since matching reads
157 + // the key and not the neighbour.
158 + .bind_in(
159 + DISCOVERY,
136 160 "shift+f",
137 161 "Find similar samples",
138 162 Action::post("/detail/similar"),
139 163 )
140 - .bind(
164 + .bind_in(
165 + DISCOVERY,
141 166 "shift+d",
142 167 "Find duplicates",
143 168 Action::post("/detail/duplicates"),
144 169 )
170 + // The five panels the toolbar toggles, by the name an address is built
171 + // from. `Panel::as_str` is that name, so a panel renamed here and there
172 + // is one edit rather than two.
173 + .bind_in(TOGGLES, "s", "Toggle the sidebar", toggling(Panel::Sidebar))
174 + .bind_in(
175 + TOGGLES,
176 + "d",
177 + "Toggle the detail panel",
178 + toggling(Panel::Detail),
179 + )
180 + .bind_in(
181 + TOGGLES,
182 + "e",
183 + "Toggle the sample editor",
184 + toggling(Panel::Edit),
185 + )
186 + .bind_in(
187 + TOGGLES,
188 + "i",
189 + "Toggle the instrument panel",
190 + toggling(Panel::Instrument),
191 + )
192 + .bind_in(TOGGLES, "l", "Toggle loop", toggling(Panel::Loop))
193 + // The forge is a screen rather than a panel, which is why this one is
194 + // not built the same way as the five above it. It is still a Toggle to
195 + // a reader, which is the whole reason the group is said rather than
196 + // derived from the address: `/panels/sidebar` and `/forge` are siblings
197 + // in nothing.
198 + .bind_in(TOGGLES, "f", "Open the sample forge", Action::get("/forge"))
199 + .bind_in(SYSTEM, "f1", "Show this help", Action::get("/help"))
145 200 }
146 201
202 + /// The shipped tab's headings, for the four this table has rows under.
203 + ///
204 + /// Named rather than written at each call site: a heading spelt two ways is two
205 + /// groups, and the failure is a table that reads almost right.
206 + const BULK: &str = "Bulk";
207 + /// Finding samples by what they are like.
208 + const DISCOVERY: &str = "Discovery";
209 + /// Showing and hiding the app's own furniture.
210 + const TOGGLES: &str = "Toggles";
211 + /// The app itself.
212 + const SYSTEM: &str = "System";
213 +
147 214 /// Toggling one panel, addressed by the name the panel answers to.
148 215 ///
149 216 /// Named for what it does rather than for its argument, so it does not read as
@@ -213,21 +280,41 @@
213 280 /// what a host does, which is the rule the bulk port's tag completions and
214 281 /// folder filter both follow.
215 282 fn shortcuts() -> Node {
216 - Node::Table {
217 - columns: vec![Column::new("Key"), Column::new("Does")],
218 - rows: chrome()
219 - .bindings
220 - .iter()
221 - .map(|binding| {
222 - Cells::new(vec![Cell::new(&binding.key), Cell::new(&binding.label)])
223 - .activate(binding.action.clone())
224 - })
225 - .collect(),
226 - // Every key that is bound is listed, which is the whole claim of this
227 - // screen. A shortcuts table with something withheld would be the drift
228 - // it exists to end.
229 - more: None,
283 + // A heading and a table per group, in the order the app bound them, which
284 + // is what `Chrome::grouped` answers. Not sorted here: the reading order is
285 + // the shipped tab's and belongs to whoever wrote the table.
286 + //
287 + // Several tables rather than one with a group column, because a group is a
288 + // heading and not a value: a column repeating "Toggles" six times says the
289 + // same thing six times and is still one wall.
290 + let mut sections = Vec::new();
291 + for (group, bindings) in chrome().grouped() {
292 + // `None` is the ungrouped run, which this table has none of today and
293 + // would have again the moment somebody added a `bind`. Drawn without a
294 + // heading rather than under an invented one.
295 + if let Some(name) = group {
296 + sections.push(Node::section(name));
297 + }
298 + sections.push(Node::Table {
299 + columns: vec![Column::new("Key"), Column::new("Does")],
300 + rows: bindings
301 + .iter()
302 + .map(|binding| {
303 + Cells::new(vec![Cell::new(&binding.key), Cell::new(&binding.label)])
304 + .activate(binding.action.clone())
305 + })
306 + .collect(),
307 + // Every key that is bound is listed, which is the whole claim of
308 + // this screen. A shortcuts table with something withheld would be
309 + // the drift it exists to end.
310 + more: None,
311 + });
230 312 }
313 + let mut body = Slot::new(SHORTCUTS_BODY, RegionKind::Group);
314 + for node in sections {
315 + body = body.with(node);
316 + }
317 + Node::Region(body)
231 318 }
232 319
233 320 /// What the app does, in prose.
@@ -3494,18 +3494,54 @@
3494 3494 router().handle(&state, request)
3495 3495 }
3496 3496
3497 + /// The shortcuts tab, read back as the headings and rows it draws.
3498 + ///
3499 + /// One table per group since `cf7872dc`, so `table_of` -- which answers with
3500 + /// the first it finds -- would assert about the Bulk group and call it the
3501 + /// whole screen.
3502 + fn shortcut_sections(screen: &Screen) -> Vec<(Option<String>, Vec<(String, String)>)> {
3503 + fn walk(
3504 + body: &[quasi_router::Ranked],
3505 + found: &mut Vec<(Option<String>, Vec<(String, String)>)>,
3506 + heading: &mut Option<String>,
3507 + ) {
3508 + for placed in body {
3509 + match &placed.node {
3510 + Node::Heading { text, .. } => *heading = Some(text.clone()),
3511 + Node::Table { rows, .. } => found.push((
3512 + heading.take(),
3513 + rows.iter()
3514 + .map(|row| (cell_text(row, 0), cell_text(row, 1)))
3515 + .collect(),
3516 + )),
3517 + Node::Region(slot) => walk(&slot.body, found, heading),
3518 + _ => {}
3519 + }
3520 + }
3521 + }
3522 +
3523 + let mut found = Vec::new();
3524 + let mut heading = None;
3525 + for slot in &screen.slots {
3526 + walk(&slot.body, &mut found, &mut heading);
3527 + }
3528 + found
3529 + }
3530 +
3497 3531 #[test]
3498 3532 fn the_help_overlay_lists_exactly_the_keys_that_are_bound() {
3499 3533 // THE POINT OF THIS PORT. `Binding`'s own header says a help overlay is
3500 3534 // otherwise "a second, hand-written copy of them, free to drift from what
3501 3535 // the keys actually do", and the shipped tab is that copy in seven arrays.
3502 3536 // Here the two cannot disagree, and this is what says so.
3537 + //
3538 + // Read across the groups, because the grouping is a heading over the same
3539 + // one table: every key is still listed exactly once and in the order it was
3540 + // bound, which is what a listing withholding nothing means.
3503 3541 let response = helping(Request::get("/help")).unwrap();
3504 - let (_, rows) = table_of(overlay(&response));
3505 -
3506 - let listed: Vec<(String, String)> = rows
3507 - .iter()
3508 - .map(|row| (cell_text(row, 0), cell_text(row, 1)))
3542 + let listed: Vec<(String, String)> = shortcut_sections(overlay(&response))
3543 + .into_iter()
3544 + .flat_map(|(_, rows)| rows)
3509 3545 .collect();
3510 3546 let bound: Vec<(String, String)> = super::help::chrome()
3511 3547 .bindings
@@ -3517,6 +3553,50 @@
3517 3553 assert!(!bound.is_empty());
3518 3554 }
3519 3555
3556 + #[test]
3557 + fn the_shortcuts_tab_is_grouped_under_the_headings_the_shipped_tab_uses() {
3558 + // `cf7872dc`. At four rows a flat table was fine; at thirteen it is the
3559 + // same wall the shipped tab broke into seven hand-written arrays, and the
3560 + // arrays are the evidence that somebody already thought so.
3561 + let response = helping(Request::get("/help")).unwrap();
3562 + let sections = shortcut_sections(overlay(&response));
3563 +
3564 + let headings: Vec<Option<String>> = sections
3565 + .iter()
3566 + .map(|(heading, _)| heading.clone())
3567 + .collect();
3568 + assert_eq!(
3569 + headings,
3570 + [
3571 + Some("Bulk".to_owned()),
3572 + Some("Discovery".to_owned()),
3573 + Some("Toggles".to_owned()),
3574 + Some("System".to_owned()),
3575 + ],
3576 + "the reading order is the app's, not the alphabet's"
3577 + );
3578 +
3579 + // Every row sits under a heading: an ungrouped run would come back with
3580 + // `None`, and this table has none today.
3581 + assert!(
3582 + sections
3583 + .iter()
3584 + .all(|(heading, rows)| heading.is_some() && !rows.is_empty())
3585 + );
3586 +
3587 + // And the group is a listing fact and nothing more -- the keys still work
3588 + // the way they did, which is what `bound` answers.
3589 + let chrome = super::help::chrome();
3590 + assert_eq!(
3591 + chrome.bound("f1").and_then(|binding| binding.group.clone()),
3592 + Some("System".to_owned())
3593 + );
3594 + assert_eq!(
3595 + chrome.bound("f1").map(|binding| binding.action.clone()),
3596 + Some(quasi_router::Action::get("/help"))
3597 + );
3598 + }
3599 +
3520 3600 #[test]
3521 3601 fn every_bound_key_points_at_an_address_this_router_serves() {
3522 3602 // The other half of "cannot disagree": a binding naming a route that does