Skip to main content

max / makeover-tui

0.39.0: an option's second line takes a row of its own `Choice::detail` from makeover-layout 0.39.0. Inset by the width of the mark so it reads as belonging to the option above it, and muted because it is not a thing to press. The inset is an area rather than spaces in the string: `text::draw` wraps on words, so leading spaces survive the first line and vanish from every one after it.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_0136sbU8F6i9WrcvA3wn4Lgk
Author: Max Johnson <me@maxj.phd> · 2026-08-29 15:42 UTC
Signed with PGP, not checked
Commit: 8cf68a1aaeb867fd94c719ccce96c39afde447f9
Parent: 1c06983
3 files changed, +70 insertions, -2 deletions
M Cargo.toml +2 -2
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-tui"
3 - version = "0.38.0"
3 + version = "0.39.0"
4 4 edition = "2024"
5 5 description = "The terminal renderer for makeover-layout, on ratatui. Colour stops being the constraint above 256 entries; geometry never does, because an edge occupies a whole cell on every side."
6 6 license = "MIT"
@@ -19,7 +19,7 @@
19 19 # compile against an API added in a later one -- which is what `Depth::Overlay`
20 20 # would do here. The rest of the suite has pinned this way since
21 21 # makeover-webview found it the hard way.
22 - makeover-layout = "0.38.0"
22 + makeover-layout = "0.39.0"
23 23 # The cadence the activity mark blinks at, and the motion-off seam beside it.
24 24 # Taken rather than chosen here: `activity_lit` is this crate's only use of it,
25 25 # and the whole point is that the number is not this crate's to pick.
M src/lib.rs +6
@@ -122,6 +122,12 @@
122 122 //! the truth rather than the lie it warns about: that option will not answer,
123 123 //! and the reason it will not now sits on its row.
124 124 //!
125 + //! `Choice::detail` (makeover-layout 0.39.0) takes a row of its own under the
126 + //! option, inset by the width of the mark and muted for the same reason: the
127 + //! line is not a thing to press. This is the host with the most room of the
128 + //! three -- a browser's `<select>` has to run the line into its option's text
129 + //! and a terminal does not, so it does not.
130 + //!
125 131 //! `Field::placeholder` on a chooser, the third finding, is already answered
126 132 //! here and needed nothing: this renderer draws every option of a select at
127 133 //! once, so an unanswered one is a list with no mark against any row rather
M src/piece.rs +62
@@ -740,6 +740,19 @@
740 740 below(area, used + rows),
741 741 buf,
742 742 );
743 + // What picking it means, on a row of its own under the option.
744 + // makeover-layout 0.39.0, and this is the host with the most
745 + // room of the three: a browser's `<select>` has to run the line
746 + // into the option's text and a terminal does not, so it does
747 + // not.
748 + //
749 + // Indented past the mark, so the line reads as belonging to the
750 + // option above it rather than as another option. Muted, which
751 + // is the truth here rather than the lie the arms above are
752 + // careful about: the row is not a thing to press.
753 + if let Some(detail) = choice.detail {
754 + rows += text::draw(detail, style.muted, indented(area, used + rows), buf);
755 + }
743 756 }
744 757 rows
745 758 }
@@ -945,6 +958,27 @@
945 958 }
946 959
947 960 /// What is left of `area` after `used` rows from the top.
961 + /// The rows under what has been drawn, inset by the width of an option's mark.
962 + ///
963 + /// makeover-layout 0.39.0. An option's second line has to read as belonging to
964 + /// the option above it rather than as another option, and the only thing that
965 + /// says so on a terminal is where it starts. The inset is `text::draw`'s to
966 + /// honour as an area rather than as spaces in the string: the drawing wraps on
967 + /// words, so leading spaces would survive the first line and vanish from every
968 + /// one after it.
969 + ///
970 + /// Four columns, which is `"( ) "`. Named against the mark rather than picked,
971 + /// so a mark that changes width takes this with it.
972 + fn indented(area: Rect, used: u16) -> Rect {
973 + const MARK: u16 = 4;
974 + let area = below(area, used);
975 + Rect {
976 + x: area.x + MARK.min(area.width),
977 + width: area.width.saturating_sub(MARK),
978 + ..area
979 + }
980 + }
981 +
948 982 fn below(area: Rect, used: u16) -> Rect {
949 983 let used = used.min(area.height);
950 984 Rect {
@@ -1470,6 +1504,34 @@
1470 1504 assert!(muted.add_modifier.contains(Modifier::DIM));
1471 1505 }
1472 1506
1507 + #[test]
1508 + fn an_option_can_carry_the_line_that_says_what_it_means() {
1509 + // makeover-layout 0.39.0. A terminal has rows, so the line gets one of
1510 + // its own under the option, indented past the mark and muted: it is not
1511 + // a thing to press, which is the one reading muted is honest about.
1512 + let style = style();
1513 + let options = [
1514 + Choice::new("16", "Basic").detailing("$16/mo. Fits text, blogs, newsletters."),
1515 + Choice::new("24", "Small Files"),
1516 + ];
1517 + let mut field_ = Field::new(FieldKind::Radio, "tier", "Tier");
1518 + field_.options = &options;
1519 + let mut buf = buffer(46, 5);
1520 + field(&style, &field_, Held::Text("16"), false, buf.area, &mut buf);
1521 +
1522 + let drawn = rows(&buf);
1523 + assert_eq!(drawn[1].trim_end(), "(*) Basic");
1524 + assert_eq!(
1525 + drawn[2].trim_end(),
1526 + " $16/mo. Fits text, blogs, newsletters."
1527 + );
1528 + // The next option follows the line rather than being pushed off: the
1529 + // row count the drawing returns is what the caller lays out with.
1530 + assert_eq!(drawn[3].trim_end(), "( ) Small Files");
1531 + let muted = buf.cell((4, 2)).expect("the detail row").style();
1532 + assert!(muted.add_modifier.contains(Modifier::DIM));
1533 + }
1534 +
1473 1535 #[test]
1474 1536 fn an_unchosen_option_does_not_read_as_disabled() {
1475 1537 // The three-tone convention: muted is inert, and every option in this