Skip to main content

max / quasi

Ask a row what it offers once in the terminal renderer, and close a gap Wave 1b. row_reachable and push_row each decided whether a row offers anything of its own, in two files, and they had drifted: push_row tested toggle and row_reachable did not, because a table row could not carry one. The collapse makes that a live gap rather than a harmless asymmetry. A row is a row now, so a table row can be a checklist item whose tick IS the write, and without this such a row would be unreachable in a terminal: nothing else about it is set, so no other clause fires and the focus walk never offers it. row_offers is the shared predicate and it tests toggle. What a row holds in its cells stays a separate question, because the two containers answer it differently on purpose: a table row steps into its cells, a list row's controls are stops of their own walked straight after it. That is the navigation model, not duplication, and it keeps its comment. Wave 0's placeholder is gone here too, on the webview's terms: a cell keyed to a declared column reaching a list row is a debug assertion and a benign release fallback, matching the two Table::row already carries. 230 tests green in quasi-tui, whole workspace green.
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 01:28 UTC
Signed with PGP, not checked
Commit: e45849120da7d1d0f80837f3b4a0da716d248fcd
Parent: 9a77315
2 files changed, +38 insertions, -13 deletions
@@ -704,13 +704,29 @@
704 704 /// One function because two would drift: [`node_spots`] decides what the caret
705 705 /// can land on and `node.rs` decides what the drawing counts, and a row counted
706 706 /// in one and not the other shifts every stop below the table by one.
707 + /// Whether a row offers anything of its own.
708 + ///
709 + /// The row's members only: what it holds in its cells is a separate question,
710 + /// and the two containers answer it differently on purpose. A table row steps
711 + /// into its cells ([`inside`]); a list row's controls are stops of their own,
712 + /// walked straight after it.
713 + ///
714 + /// One predicate since the 2026-09-05 collapse, and it closes a gap. This was
715 + /// written twice, and the copy here did not test `toggle` because a table row
716 + /// could not carry one. Now that a row is a row, a table row that is a
717 + /// checklist item can, and without this it would have been unreachable in a
718 + /// terminal: nothing else about such a row is set, so no other clause fires.
719 + pub(crate) fn row_offers(row: &Row) -> bool {
720 + row.open.is_some()
721 + || row.activate.is_some()
722 + || row.toggle.is_some()
723 + || row.selected.is_some()
724 + || row.chosen.is_some()
725 + || !row.menu.is_empty()
726 + }
727 +
707 728 pub(crate) fn row_reachable(cells: &Cells) -> bool {
708 - cells.open.is_some()
709 - || cells.activate.is_some()
710 - || cells.selected.is_some()
711 - || cells.chosen.is_some()
712 - || !cells.menu.is_empty()
713 - || !inside(cells).is_empty()
729 + row_offers(cells) || !inside(cells).is_empty()
714 730 }
715 731
716 732 /// The parts of a table row the caret can step into, as `(column, part)`
@@ -935,13 +951,7 @@
935 951 ///
936 952 /// The row comes first because it is the whole line and the controls sit on it.
937 953 fn push_row(row: &Row, region: &str, local: &Local<'_>, found: &mut Vec<Reach>) {
938 - if row.open.is_some()
939 - || row.activate.is_some()
940 - || row.toggle.is_some()
941 - || row.selected.is_some()
942 - || row.chosen.is_some()
943 - || !row.menu.is_empty()
944 - {
954 + if row_offers(row) {
945 955 found.push(Reach {
946 956 region: region.to_string(),
947 957 spot: Spot::Row {
@@ -876,6 +876,21 @@
876 876 spans.push(Span::raw(" "));
877 877 }
878 878 let focused = focus.get(index).copied().unwrap_or(false);
879 + // A list draws over the default column set, so every cell in one is
880 + // keyed by role. A cell keyed to a declared column has reached a list
881 + // row, which is a description error rather than something to style:
882 + // the table constructors produce those keys and a list row is built by
883 + // `Row::new` and its siblings.
884 + //
885 + // Loud in debug and benign in release, matching the webview and the two
886 + // assertions `Table::row` already carries (`d41d00a`). A panic in a
887 + // description is worse than a line drawn under the wrong style.
888 + debug_assert!(
889 + matches!(cell.key, quasi_router::CellKey::Role(_)),
890 + "a cell keyed to a declared column reached a list row, which draws over the \
891 + default column set and has no column to style it from. Key: {:?}",
892 + cell.key,
893 + );
879 894 let style = match &cell.key {
880 895 quasi_router::CellKey::Role(role) => part_style(tui, *role),
881 896 _ => part_style(tui, quasi_router::layout::RowPart::Primary),