Skip to main content

max / audiofiles

Ask an audiofiles row what it holds through the merged vocabulary Wave 2c. The screens themselves needed nothing: the grammar's surface is unchanged, so all eleven shape files parse as they did. What moved is the test and parity code, which reached past the constructors into the fields. `Row::parts` and `Cells::values` are `Row::cells`, and `Cell::parts` is `Cell::content`. Five test helpers hand-rolled `Row::role`, filtering the run for one `RowPart` and pulling the node out; they call it now. The parity harness had two arms for what is one type, and they had drifted. The table arm claimed a row only when it opened, while the shipped table hangs a menu off every cell's response, so a table row offering nothing but a menu was an offer the described side could not see. One `row_offers`, matching `quasi_immediate::node::row`'s own condition. `-> Cells` stays as the shape return type in bulk.rs and files.rs. It is not a type alias leak: `declare!` reads that token to pick the body form, so `-> Row` there is a compile error rather than the same thing said again. 1735 tests green, clippy clean with -D warnings.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01P8ostB2UmZJGj5WjSHRSot
Author: Max Johnson <me@maxj.phd> · 2026-09-06 16:52 UTC
Signed with PGP, not checked
Commit: a819a2df10f3bfdf48ffb67a282e83bb57c68576
Parent: 07ef2b4
6 files changed, +65 insertions, -68 deletions
@@ -554,9 +554,12 @@
554 554 declare! {
555 555 /// The move modal.
556 556 ///
557 - /// A table of one column rather than a list, because picking a row is what
558 - /// this screen is for and `Cells::activate` is what says a row is
559 - /// pressable. The shipped modal draws `selectable_label` per directory with
557 + /// A table of one column rather than a list, which since the 2026-09-05
558 + /// collapse is a presentation choice and nothing more: both draw the same
559 + /// [`Row`](quasi_router::Row), and `Row::activate` is what says a row is
560 + /// pressable either way. A table because a destination is a record the
561 + /// reader is picking from a set, not a feed.
562 + /// The shipped modal draws `selectable_label` per directory with
560 563 /// a substring filter above it; the filter is not described, on the rule the
561 564 /// tag completions follow -- narrowing a list while someone types is what a
562 565 /// host does with a list it was handed.
@@ -51,9 +51,9 @@
51 51 //! has to become a context menu, an action sheet and a key-driven menu, and no
52 52 //! single renderer can be where that is said. It is right, and the file list is
53 53 //! a [`Node::Table`], whose
54 - //! [`Cells`](quasi_router::Cells) had no `menu`.
54 + //! [`Row`](quasi_router::Row) had no `menu`.
55 55 //!
56 - //! **[`Cells::menu`](quasi_router::Cells::menu) exists as of quasi-router 0.20.0, and this screen describes
56 + //! **[`Row::menu`](quasi_router::Row::menu) exists as of quasi-router 0.20.0, and this screen describes
57 57 //! its row menu through it.** See `menu` below. A member is added when a consumer
58 58 //! asks for it, not because its sibling has one.
59 59 //!
@@ -62,7 +62,7 @@
62 62 //! every row gets the sample columns and a Play control, folders included.
63 63 //!
64 64 //! The one entry still not described is **Add to Collection**, the only nested
65 - //! one. `Cells::menu` is flat, matching `Row::menu`, and a submenu wants a second
65 + //! one. `Row::menu` is flat, and a submenu wants a second
66 66 //! consumer before [`Act`] grows a child list. Flattening it reads "Add to
67 67 //! Kicks", "Add to Breaks" for as many collections as exist, which is honest and
68 68 //! gets long.
@@ -90,7 +90,7 @@
90 90 //! on one. Picking eleven kicks out of four hundred by hand is close to what this
91 91 //! app is for, so the selection has a described shape.
92 92 //!
93 - //! It is two members. `Cells::chosen` is the
93 + //! It is two members. `Row::chosen` is the
94 94 //! third state beside `current` and `selected`, three-valued for the reason the
95 95 //! tick is: a ctrl-click lands on an **unchosen** row, so a renderer has to know
96 96 //! that row can join a set before anything is in one.
@@ -584,7 +584,7 @@
584 584 /// The file list holds both, and a described row has to tell them apart or
585 585 /// every row gets the sample columns and a Play control, folders included.
586 586 /// `draw_context_menu` branches on exactly this and offers two different
587 - /// menus, so `Cells::menu` cannot be described without it.
587 + /// menus, so `Row::menu` cannot be described without it.
588 588 pub directory: bool,
589 589 /// Whether the bytes are only in the cloud.
590 590 ///
@@ -601,11 +601,11 @@
601 601 /// unchosen one. What reads it is `files::menu`, which offers a row inside a
602 602 /// multi-selection what can be done to the *set* rather than to the row.
603 603 ///
604 - /// It is also what the list *draws*. Until quasi 0.78 nothing could: `Cells`
604 + /// It is also what the list *draws*. Until quasi 0.78 nothing could: a row
605 605 /// marked the current row and held a tick, a tick is a staged set awaiting a
606 606 /// commit control rather than a selection already in force, and a
607 607 /// five-hundred-row Cmd+A therefore looked exactly like a one-row click.
608 - /// `Cells::chosen` is the third state that closes it, and `Cells::choosing`
608 + /// `Row::chosen` is the third state that closes it, and `Row::choosing`
609 609 /// is what `files::row` writes this into.
610 610 pub selected: bool,
611 611 /// Whether this sample is in the basket -- one of the samples being looked
@@ -197,7 +197,7 @@
197 197 /// Raise the empty-space menu when a press lands on no row.
198 198 ///
199 199 /// The second host gesture, and the smaller half of audiofiles `41996627`. A
200 - /// row answers a menu because a row is a described thing and `Cells::menu` says
200 + /// row answers a menu because a row is a described thing and `Row::menu` says
201 201 /// what it offers; **the absence of a row is not a described thing**, so nothing
202 202 /// in a description can answer a press on the part of the list that is not one.
203 203 /// The renderer is the only side that knows -- it drew the rows and it knows
@@ -2589,7 +2589,7 @@
2589 2589 ///
2590 2590 /// - **What the press chose.** Pressing an unchosen row and dragging drags
2591 2591 /// *that* row: the selection collapses onto the row under the cursor first.
2592 - /// The rows carry a `Cells::value`, so `row_at` answers with the sample itself
2592 + /// The rows carry a `Row::value`, so `row_at` answers with the sample itself
2593 2593 /// and the app looks it up in its own contents, rather than matching a position
2594 2594 /// back against the ordering the description happened to be built from, which
2595 2595 /// is the same row identified twice and agreeing by luck.
@@ -303,36 +303,13 @@
303 303 out.addresses.push(reorder.destination.as_str().to_owned());
304 304 }
305 305 }
306 - for cells in rows {
307 - if let Some(activate) = &cells.activate {
308 - out.push(Role::Button, first_words(&cells.values), false);
309 - out.addresses.push(activate.destination.as_str().to_owned());
310 - }
311 - for cell in &cells.values {
312 - for part in &cell.parts {
313 - walk(part, out);
314 - }
315 - }
306 + for row in rows {
307 + row_offers(row, out);
316 308 }
317 309 }
318 310 Node::List { rows, .. } => {
319 311 for row in rows {
320 - // A row is claimed when it opens OR when it offers a menu,
321 - // which is `quasi_immediate::node::row`'s own condition: a row
322 - // that only offers a menu still needs somewhere to right-click,
323 - // and it is announced by its first text either way. The menu's
324 - // own acts are not offers here -- they are not on screen until
325 - // the gesture -- but the row that carries them is.
326 - if row.activate.is_some() || !row.menu.is_empty() {
327 - let named: Vec<_> = row.parts.iter().map(|part| part.node.clone()).collect();
328 - out.push(Role::Button, first_text(&named), false);
329 - }
330 - if let Some(activate) = &row.activate {
331 - out.addresses.push(activate.destination.as_str().to_owned());
332 - }
333 - for part in &row.parts {
334 - walk(&part.node, out);
335 - }
312 + row_offers(row, out);
336 313 }
337 314 }
338 315 Node::Region(slot) => {
@@ -396,12 +373,39 @@
396 373 cells
397 374 .iter()
398 375 .find_map(|cell| {
399 - let said = first_text(&cell.parts);
376 + let said = first_text(&cell.content);
400 377 (!said.is_empty()).then_some(said)
401 378 })
402 379 .unwrap_or_default()
403 380 }
404 381
382 + /// What one row offers, whichever container it sits in.
383 + ///
384 + /// A list row and a table row are one type since the 2026-09-05 collapse, and
385 + /// this is written once for the reason `quasi_immediate::node::row_name` is:
386 + /// the two copies had already drifted. The table copy claimed a row only when
387 + /// it opened, while the shipped table hangs a menu off every cell's response,
388 + /// so a table row that offered nothing but a menu was an offer this side could
389 + /// not see.
390 + fn row_offers(row: &quasi_router::Row, out: &mut Offering) {
391 + // A row is claimed when it opens OR when it offers a menu, which is
392 + // `quasi_immediate::node::row`'s own condition: a row that only offers a
393 + // menu still needs somewhere to right-click, and it is announced by its
394 + // first text either way. The menu's own acts are not offers here -- they
395 + // are not on screen until the gesture -- but the row that carries them is.
396 + if row.activate.is_some() || !row.menu.is_empty() {
397 + out.push(Role::Button, first_words(&row.cells), false);
398 + }
399 + if let Some(activate) = &row.activate {
400 + out.addresses.push(activate.destination.as_str().to_owned());
401 + }
402 + for cell in &row.cells {
403 + for node in &cell.content {
404 + walk(node, out);
405 + }
406 + }
407 + }
408 +
405 409 /// The first thing a run of leaves says.
406 410 fn first_text(parts: &[Node]) -> String {
407 411 parts
@@ -1949,7 +1949,7 @@
1949 1949 // Peak is off in the fixture, so it is not described at all.
1950 1950 assert_eq!(names, ["Name", "Duration", "BPM", "Key", "Tags", "Play"]);
1951 1951 assert_eq!(rows.len(), 1);
1952 - assert_eq!(rows[0].values.len(), columns.len(), "a cell per column");
1952 + assert_eq!(rows[0].cells.len(), columns.len(), "a cell per column");
1953 1953 }
1954 1954
1955 1955 #[test]
@@ -2067,7 +2067,7 @@
2067 2067
2068 2068 #[test]
2069 2069 fn a_sample_row_offers_the_menu_the_shipped_one_offers() {
2070 - // `Cells::menu`, the member this screen asked quasi-router for. The
2070 + // `Row::menu`, the member this screen asked quasi-router for. The
2071 2071 // assertion is against `draw_context_menu`'s sample branch: same acts, same
2072 2072 // order, same words.
2073 2073 let files = FakeFiles::with(vec![sample(7, "kick.wav")]);
@@ -2130,13 +2130,9 @@
2130 2130
2131 2131 // And a folder still fills every column, because cells are positional: the
2132 2132 // Play cell is empty rather than missing.
2133 - assert_eq!(rows[0].values.len(), columns.len());
2133 + assert_eq!(rows[0].cells.len(), columns.len());
2134 2134 assert!(
2135 - !rows[0]
2136 - .values
2137 - .last()
2138 - .expect("a Play cell")
2139 - .carries_control(),
2135 + !rows[0].cells.last().expect("a Play cell").carries_control(),
2140 2136 "a folder offered something to play"
2141 2137 );
2142 2138 }
@@ -2195,9 +2191,9 @@
2195 2191 "a basket answer is already ranked, so the column does not offer a sort"
2196 2192 );
2197 2193
2198 - let tokens = |row: &quasi_router::Cells| -> Vec<String> {
2199 - row.values[1]
2200 - .parts
2194 + let tokens = |row: &quasi_router::Row| -> Vec<String> {
2195 + row.cells[1]
2196 + .content
2201 2197 .iter()
2202 2198 .filter_map(|part| match part {
2203 2199 Node::Token(tag) => Some(tag.label.clone()),
@@ -2219,7 +2215,7 @@
2219 2215
2220 2216 assert!(columns.iter().all(|column| column.name != "Near"));
2221 2217 assert_eq!(
2222 - rows[0].values.len(),
2218 + rows[0].cells.len(),
2223 2219 columns.len(),
2224 2220 "cells are positional against the columns"
2225 2221 );
@@ -3581,9 +3577,9 @@
3581 3577 }
3582 3578
3583 3579 /// The text of one cell in a table row.
3584 - fn cell_text(row: &quasi_router::Cells, at: usize) -> String {
3585 - row.values[at]
3586 - .parts
3580 + fn cell_text(row: &quasi_router::Row, at: usize) -> String {
3581 + row.cells[at]
3582 + .content
3587 3583 .iter()
3588 3584 .filter_map(|node| match node {
3589 3585 Node::Text { text, .. } => Some(text.clone()),
@@ -3594,10 +3590,8 @@
3594 3590
3595 3591 /// A row's trailing fact, if it has one.
3596 3592 fn meta_of(row: &quasi_router::Row) -> Option<String> {
3597 - row.parts
3598 - .iter()
3599 - .find(|part| part.role == quasi_router::layout::RowPart::Meta)
3600 - .and_then(|part| match &part.node {
3593 + row.role(quasi_router::layout::RowPart::Meta)
3594 + .find_map(|node| match node {
3601 3595 Node::Text { text, .. } => Some(text.clone()),
3602 3596 _ => None,
3603 3597 })
@@ -4048,7 +4042,7 @@
4048 4042 panic!("expected the preview table");
4049 4043 };
4050 4044 for row in rows {
4051 - let marked = row.values[1].parts.iter().any(|part| {
4045 + let marked = row.cells[1].content.iter().any(|part| {
4052 4046 matches!(part, Node::Token(tag) if tag.tone == quasi_router::layout::Tone::Warning)
4053 4047 });
4054 4048 assert!(marked, "a colliding name is not marked");
@@ -4433,10 +4427,8 @@
4433 4427
4434 4428 /// A row's primary part.
4435 4429 fn primary_of(row: &quasi_router::Row) -> Option<String> {
4436 - row.parts
4437 - .iter()
4438 - .find(|part| part.role == quasi_router::layout::RowPart::Primary)
4439 - .and_then(|part| match &part.node {
4430 + row.role(quasi_router::layout::RowPart::Primary)
4431 + .find_map(|node| match node {
4440 4432 Node::Text { text, .. } => Some(text.clone()),
4441 4433 _ => None,
4442 4434 })
@@ -5228,10 +5220,8 @@
5228 5220 let marks: Vec<String> = rows
5229 5221 .iter()
5230 5222 .flat_map(|row| {
5231 - row.parts
5232 - .iter()
5233 - .filter(|part| part.role == quasi_router::layout::RowPart::Tokens)
5234 - .filter_map(|part| match &part.node {
5223 + row.role(quasi_router::layout::RowPart::Tokens)
5224 + .filter_map(|node| match node {
5235 5225 Node::Token(tag) => Some(tag.label.clone()),
5236 5226 _ => None,
5237 5227 })
@@ -8347,7 +8337,7 @@
8347 8337
8348 8338 let tagged: Vec<String> = deep_rows(&screen)
8349 8339 .into_iter()
8350 - .filter_map(|row| row.parts.first().map(|_| row.primary()))
8340 + .filter_map(|row| row.cells.first().map(|_| row.primary()))
8351 8341 .collect();
8352 8342 assert!(
8353 8343 tagged.iter().any(|said| said.contains("drums/kick")),