Skip to main content

max / audiofiles

Name the columns in the sample browser and its siblings files.rs is the case this vocabulary was changed for: six of the eight cells in a sample row are conditional, and the column list carried the same six conditionals in the same order. Two lists that had to agree, in two functions, with nothing checking that they did. Named, columns() is the only place the shape is decided. The other four tables had fixed rows and move onto Table for the constructor alone.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01MptwXZ8k65v19rFmdGAyki
Author: Max Johnson <me@maxj.phd> · 2026-09-03 03:26 UTC
Signed with PGP, not checked
Commit: e3cbde18df76555a4c351e29b3fc833a390db929
Parent: 9138fb6
4 files changed, +143 insertions, -109 deletions
@@ -66,7 +66,7 @@
66 66 use quasi_router::layout::{FieldKind, Tone};
67 67 use quasi_router::{
68 68 Act, Action, Cell, Cells, Choice, Column, Consult, Field, Node, Outcome, RegionKind, Request,
69 - Response, Rest, RouteError, Router, Screen, Slot, Tag,
69 + Response, Rest, RouteError, Router, Screen, Slot, Table, Tag,
70 70 };
71 71
72 72 use super::{Chosen, Panels};
@@ -80,6 +80,12 @@
80 80 const MODE: &str = "mode";
81 81 /// What a move modal submits.
82 82 const FOLDER: &str = "folder";
83 + /// The move modal's one column heading, named by the table and by its rows.
84 + ///
85 + /// A const rather than the string twice, because [`row`] is written a screen
86 + /// away from the column list it answers and a heading that drifted in one of
87 + /// the two places would be an empty cell rather than a compile error.
88 + const DESTINATION: &str = "Folder";
83 89 /// What a rename modal submits.
84 90 const PATTERN: &str = "pattern";
85 91
@@ -301,20 +307,24 @@
301 307 let body = Slot::new(BODY, RegionKind::Pane)
302 308 .with(Node::page(format!("Move {} items", chosen.names.len())))
303 309 .with(Node::text("Choose where they go."))
304 - .with(Node::Table {
305 - columns: vec![Column::new("Folder")],
306 - rows,
307 - // Every folder in the vault, because a destination the picker does
308 - // not show is a destination you cannot choose.
309 - more: None,
310 - });
310 + // No `more`: every folder in the vault is here, because a destination
311 + // the picker does not show is a destination you cannot choose.
312 + .with(Node::from(
313 + Table::new(vec![Column::new(DESTINATION)]).rows(rows),
314 + ));
311 315
312 316 closing(subjects(body, &chosen.names))
313 317 }
314 318
315 319 /// One destination, as a row that submits itself.
320 + ///
321 + /// The cell names [`DESTINATION`] rather than counting to it: the column list is
322 + /// in [`moving`] and this is not, so position here would be an agreement between
323 + /// two functions that nothing enforces.
316 324 fn row(path: &str, value: &str) -> Cells {
317 - Cells::new(vec![Cell::new(path)]).activate(Action::post("/bulk/move").carrying(FOLDER, value))
325 + Cells::default()
326 + .at(DESTINATION, Cell::new(path))
327 + .activate(Action::post("/bulk/move").carrying(FOLDER, value))
318 328 }
319 329
320 330 /// `GET /bulk/rename`
@@ -457,33 +467,37 @@
457 467 }
458 468
459 469 let total = previews.len();
460 - Node::Table {
461 - columns: vec![Column::new("Old"), Column::new("New")],
462 - // Said rather than implied, as of quasi 0.15: a described table of the
463 - // first fifty of five hundred was indistinguishable from a table of
464 - // fifty until `Node::Table` grew `more`, which is the gap this port
465 - // noted at 65abb3c. No `forward`, because there is nowhere to ask --
466 - // the cap is a rendering budget and the rename acts on all of them.
467 - more: (total > SHOWN).then(|| Rest::showing(SHOWN).of(total)),
468 - rows: previews
469 - .iter()
470 - .take(SHOWN)
471 - .map(|(old, new)| {
472 - let collides = seen.get(new.as_str()).copied().unwrap_or(0) > 1;
473 - Cells::new(vec![
474 - Cell::new(old),
475 - // A collision is a tone on the value rather than a hover on
476 - // it, for the reason a suggestion's score is in its label in
477 - // `detail`: a reader with no pointer never sees a hover, and
478 - // this one is a warning about losing files.
479 - if collides {
480 - Cell::tag(Tag::badge(new.clone()).tone(Tone::Warning))
481 - } else {
482 - Cell::new(new)
483 - },
484 - ])
485 - })
486 - .collect(),
470 + // Two columns and two cells, written here in one expression, and every row
471 + // has both. That is the case position is still safe in, so the rows stay
472 + // positional: there is no second function for a heading to drift in, and no
473 + // branch that drops a cell -- the collision fork changes what the New cell
474 + // holds, never whether it is there.
475 + let table = Table::new(vec![Column::new("Old"), Column::new("New")]).rows(
476 + previews.iter().take(SHOWN).map(|(old, new)| {
477 + let collides = seen.get(new.as_str()).copied().unwrap_or(0) > 1;
478 + Cells::new(vec![
479 + Cell::new(old),
480 + // A collision is a tone on the value rather than a hover on it,
481 + // for the reason a suggestion's score is in its label in
482 + // `detail`: a reader with no pointer never sees a hover, and
483 + // this one is a warning about losing files.
484 + if collides {
485 + Cell::tag(Tag::badge(new.clone()).tone(Tone::Warning))
486 + } else {
487 + Cell::new(new)
488 + },
489 + ])
490 + }),
491 + );
492 + // Said rather than implied, as of quasi 0.15: a described table of the first
493 + // fifty of five hundred was indistinguishable from a table of fifty until a
494 + // table grew `more`, which is the gap this port noted at 65abb3c. No
495 + // `forward`, because there is nowhere to ask -- the cap is a rendering
496 + // budget and the rename acts on all of them.
497 + if total > SHOWN {
498 + table.more(Rest::showing(SHOWN).of(total)).into()
499 + } else {
500 + table.into()
487 501 }
488 502 }
489 503
@@ -273,7 +273,7 @@
273 273 /// channel count are properties of a file, not headline figures. The shipped
274 274 /// panel draws an `egui::Grid` of label/value pairs and that is what this is.
275 275 fn metadata(body: Slot, analysis: &Analysis) -> Slot {
276 - use quasi_router::{Cell, Cells, Column};
276 + use quasi_router::{Cell, Cells, Column, Table};
277 277
278 278 let mut rows = vec![
279 279 fact("Duration", seconds(analysis.duration)),
@@ -299,15 +299,18 @@
299 299 rows.push(fact("Loop", if is_loop { "Yes" } else { "No" }));
300 300 }
301 301
302 - body.with(Node::section("Metadata")).with(Node::Table {
303 - columns: vec![Column::new("Field"), Column::new("Value")],
304 - rows: rows
305 - .into_iter()
306 - .map(|(field, value)| Cells::new(vec![Cell::new(field), Cell::new(value)]))
307 - .collect(),
308 - // Nine fields at most, and every one that was found is here.
309 - more: None,
310 - })
302 + // Positional cells, and they stay that way: the two columns and the two
303 + // cells are written in the same expression, and it is the *rows* that come
304 + // and go here rather than the cells within one. Every row is a field and a
305 + // value, so there is no conditional cell to shift the ones behind it.
306 + //
307 + // No `more`: nine fields at most, and every one that was found is here.
308 + body.with(Node::section("Metadata")).with(Node::from(
309 + Table::new(vec![Column::new("Field"), Column::new("Value")]).rows(
310 + rows.into_iter()
311 + .map(|(field, value)| Cells::new(vec![Cell::new(field), Cell::new(value)])),
312 + ),
313 + ))
311 314 }
312 315
313 316 /// One label and one value.
@@ -487,20 +490,21 @@
487 490
488 491 /// What every chosen sample says, where they say the same thing.
489 492 fn agreed(body: Slot, spread: &Spread) -> Slot {
490 - use quasi_router::{Cell, Cells, Column};
493 + use quasi_router::{Cell, Cells, Column, Table};
491 494
492 - body.with(Node::section("In common")).with(Node::Table {
493 - columns: vec![Column::new("Field"), Column::new("Value")],
494 - rows: [
495 - ("BPM", &spread.bpm),
496 - ("Key", &spread.musical_key),
497 - ("Duration", &spread.duration),
498 - ]
499 - .into_iter()
500 - .map(|(field, value)| Cells::new(vec![Cell::new(field), Cell::new(reads(value))]))
501 - .collect(),
502 - more: None,
503 - })
495 + // Three fixed rows of two cells, with the columns beside them in the one
496 + // expression, so position is still safe to read here.
497 + body.with(Node::section("In common")).with(Node::from(
498 + Table::new(vec![Column::new("Field"), Column::new("Value")]).rows(
499 + [
500 + ("BPM", &spread.bpm),
501 + ("Key", &spread.musical_key),
502 + ("Duration", &spread.duration),
503 + ]
504 + .into_iter()
505 + .map(|(field, value)| Cells::new(vec![Cell::new(field), Cell::new(reads(value))])),
506 + ),
507 + ))
504 508 }
505 509
506 510 /// What a shared field reads as.
@@ -104,7 +104,7 @@
104 104 use quasi_router::layout::{Priority, Sort, Tone, Width};
105 105 use quasi_router::{
106 106 Act, Action, Cell, Cells, Choice, Choosing, Column, Field, Node, RegionKind, Request, Response,
107 - RouteError, Router, Screen, Slot, Tag,
107 + RouteError, Router, Screen, Slot, Table, Tag,
108 108 };
109 109
110 110 use super::{Collection, Panels, Sample};
@@ -579,14 +579,13 @@
579 579 .offering(Act::new("Import samples", Action::get("/import/open"))),
580 580 )
581 581 } else {
582 - Slot::new(BODY, RegionKind::Pane).with(Node::Table {
583 - columns: columns(state, shown),
584 - rows: samples.iter().map(|sample| row(sample, &listing)).collect(),
585 - // Everything the app has loaded and filtered is here. Windowing
586 - // rows it already holds is a renderer's job, which is the note in
587 - // this module's header.
588 - more: None,
589 - })
582 + // No `more`: everything the app has loaded and filtered is here.
583 + // Windowing rows it already holds is a renderer's job, which is the
584 + // note in this module's header.
585 + Slot::new(BODY, RegionKind::Pane).with(Node::from(
586 + Table::new(columns(state, shown))
587 + .rows(samples.iter().map(|sample| row(sample, &listing))),
588 + ))
590 589 }
591 590 }
592 591
@@ -677,10 +676,18 @@
677 676 }
678 677
679 678 /// One sample as a row.
679 + ///
680 + /// Every cell names its column, and six of the eight are conditional: the
681 + /// anchors cell appears with a basket answer and five more follow the reader's
682 + /// switched-on columns. Built by position, this function and [`columns`] were
683 + /// two lists of conditionals that had to agree in the same order, in two places,
684 + /// with nothing checking that they did. Naming them makes [`columns`] the only
685 + /// place the shape is decided, and a cell whose column is off is dropped rather
686 + /// than shifting everything after it one column left.
680 687 fn row(sample: &Sample, listing: &Listing<'_>) -> Cells {
681 688 let shown = listing.shown;
682 689 let basket_showing = listing.basket_showing;
683 - let mut values = vec![Cell::new(&sample.name)];
690 + let mut values = Cells::default().at(NAME, Cell::new(&sample.name));
684 691 if basket_showing {
685 692 // A badge per anchor, in basket order, the way the tags cell holds a run
686 693 // of tags: each anchor keeps its own edges rather than becoming a comma
@@ -694,27 +701,33 @@
694 701 for anchor in sample.matched.iter().skip(1) {
695 702 cell = cell.token(Tag::badge(anchor.clone()));
696 703 }
697 - values.push(cell);
704 + values = values.at(NEAR, cell);
698 705 }
699 706 if shown.duration {
700 - values.push(Cell::new(seconds(sample.duration)));
707 + values = values.at(DUR, Cell::new(seconds(sample.duration)));
701 708 }
702 709 if shown.bpm {
703 - values.push(Cell::new(
704 - sample
705 - .bpm
706 - .map_or_else(String::new, |bpm| format!("{bpm:.0}")),
707 - ));
710 + values = values.at(
711 + BPM,
712 + Cell::new(
713 + sample
714 + .bpm
715 + .map_or_else(String::new, |bpm| format!("{bpm:.0}")),
716 + ),
717 + );
708 718 }
709 719 if shown.key {
710 - values.push(Cell::new(sample.key.clone().unwrap_or_default()));
720 + values = values.at(KEY, Cell::new(sample.key.clone().unwrap_or_default()));
711 721 }
712 722 if shown.peak_db {
713 - values.push(Cell::new(
714 - sample
715 - .peak_db
716 - .map_or_else(String::new, |db| format!("{db:.1}")),
717 - ));
723 + values = values.at(
724 + PEAK,
725 + Cell::new(
726 + sample
727 + .peak_db
728 + .map_or_else(String::new, |db| format!("{db:.1}")),
729 + ),
730 + );
718 731 }
719 732 if shown.tags {
720 733 // Tags as tokens rather than as joined prose, which is what
@@ -731,20 +744,22 @@
731 744 for tag in sample.tags.iter().skip(1) {
732 745 cell = cell.token(Tag::badge(tag.clone()));
733 746 }
734 - values.push(cell);
747 + values = values.at(TAGS, cell);
735 748 }
736 - // A folder has nothing to play, and the cell stays because cells are
737 - // positional against the columns: dropping it would shift every value after
738 - // it one column left. Empty rather than absent is the same answer the
739 - // analysis cells already give for a folder.
740 - values.push(if sample.directory || sample.cloud_only {
741 - Cell::new("")
742 - } else {
743 - Cell::acts([Act::new(
744 - "Play",
745 - Action::post(format!("/files/{}/play", sample.id)),
746 - )])
747 - });
749 + // A folder has nothing to play. The cell is said anyway rather than left
750 + // out: the Play column is always on, and empty rather than absent is the
751 + // same answer the analysis cells already give for a folder.
752 + values = values.at(
753 + PLAY,
754 + if sample.directory || sample.cloud_only {
755 + Cell::new("")
756 + } else {
757 + Cell::acts([Act::new(
758 + "Play",
759 + Action::post(format!("/files/{}/play", sample.id)),
760 + )])
761 + },
762 + );
748 763
749 764 // Named and chosen, which is one call because they are one fact: the id the
750 765 // renderer answers a gesture with, and this row's standing in the selection
@@ -758,7 +773,7 @@
758 773 // by luck. What the chosen half closes is bigger -- until it landed nothing
759 774 // drew a multi-selection at all, so Cmd+A over five hundred samples looked
760 775 // exactly like a click on one.
761 - let mut row = Cells::new(values)
776 + let mut row = values
762 777 .choosing(sample.id.to_string(), sample.selected)
763 778 .activate(Action::post(format!("/files/{}/open", sample.id)));
764 779 row.current = listing.current == Some(sample.id);
@@ -78,7 +78,7 @@
78 78
79 79 use quasi_router::{
80 80 Action, Cell, Cells, Chrome, Column, Node, RegionKind, Request, Response, RouteError, Router,
81 - Screen, Slot,
81 + Screen, Slot, Table,
82 82 };
83 83
84 84 use super::{Panel, Panels};
@@ -281,20 +281,21 @@
281 281 if let Some(name) = group {
282 282 sections.push(Node::section(name));
283 283 }
284 - sections.push(Node::Table {
285 - columns: vec![Column::new("Key"), Column::new("Does")],
286 - rows: bindings
287 - .iter()
288 - .map(|binding| {
284 + // Positional cells: the two columns and the two cells are written a
285 + // line apart in the one expression, and a binding is always a key and
286 + // a label, so there is no cell here that appears only sometimes.
287 + //
288 + // No `more`: every key that is bound is listed, which is the whole
289 + // claim of this screen. A shortcuts table with something withheld
290 + // would be the drift it exists to end.
291 + sections.push(Node::from(
292 + Table::new(vec![Column::new("Key"), Column::new("Does")]).rows(bindings.iter().map(
293 + |binding| {
289 294 Cells::new(vec![Cell::new(&binding.key), Cell::new(&binding.label)])
290 295 .activate(binding.action.clone())
291 - })
292 - .collect(),
293 - // Every key that is bound is listed, which is the whole claim of
294 - // this screen. A shortcuts table with something withheld would be
295 - // the drift it exists to end.
296 - more: None,
297 - });
296 + },
297 + )),
298 + ));
298 299 }
299 300 sections
300 301 }