Skip to main content

max / makeover-tui

0.33.0: an interval is one line `makeover-layout` 0.34.0's `FieldKind::Interval`, drawn as the low end, the word `to`, and the high end. `Held::Between` carries the two values. One line because it is one question. Two rows would read as two questions, which is exactly what the kind exists to stop the description saying, and a terminal has no side-by-side boxes to fall back on. An open end draws the bound it falls back to, muted, because that is where the axis ends rather than a value anybody set. With no bound to fall back on the end stays blank: a terminal inventing a number there would report a filter nobody applied, which is `range_line`'s position on an unreadable value. The word rather than a dash. A dash between two numbers is a minus sign to anyone reading a signed axis, and half the measured axes are signed -- audiofiles filters loudness in dBFS. `Held::Between` rather than one string with a separator, for the reason the description states both names: a delimiter this crate owned could appear inside either end.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-22 01:46 UTC
Signed with PGP, not checked
Commit: 4c7057982ff19fe3aa1c769e179a875d3b6719a7
Parent: 0a16857
3 files changed, +171 insertions, -3 deletions
M Cargo.toml +2 -2
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-tui"
3 - version = "0.32.0"
3 + version = "0.33.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.33.0"
22 + makeover-layout = "0.34.0"
23 23 makeover = { version = "3.0", optional = true }
24 24
25 25 [lints.rust]
M src/lib.rs +22
@@ -127,6 +127,28 @@
127 127 //! once, so an unanswered one is a list with no mark against any row rather
128 128 //! than an empty box with nothing in it.
129 129 //!
130 + //! # 0.33.0: an interval is one line
131 + //!
132 + //! `makeover-layout` 0.34.0's [`makeover_layout::FieldKind::Interval`], drawn
133 + //! as the low end, the word `to`, and the high end. One line because it is one
134 + //! question: two rows would read as two questions, which is exactly what the
135 + //! kind exists to stop the description saying, and a terminal has no
136 + //! side-by-side boxes to fall back on.
137 + //!
138 + //! - **An open end draws the bound it falls back to**, muted, because that is
139 + //! where the axis ends rather than a value anybody set. With no bound to fall
140 + //! back on the end stays blank, which is [`piece::field`]'s standing position
141 + //! on a value this crate would have to invent.
142 + //! - **The word rather than a dash.** A dash between two numbers is a minus
143 + //! sign to anyone reading a signed axis, and half the measured axes are
144 + //! signed: audiofiles filters loudness in dBFS.
145 + //! - The unit rides on each end, through the same `measured` the typed path
146 + //! uses, so `90 BPM to 130 BPM` reads without the label being consulted.
147 + //!
148 + //! [`piece::Held::Between`] is the second value, for the reason the description
149 + //! states both names: a separator inside one string would make this crate own a
150 + //! delimiter either end could contain.
151 + //!
130 152 //! # 0.32.0: a number reads with its unit
131 153 //!
132 154 //! `makeover-layout` 0.33.0's `Field::unit`, drawn on the value rather than in
M src/piece.rs +147 -1
@@ -245,6 +245,23 @@
245 245 Text(&'a str),
246 246 /// A checkbox, on or off.
247 247 On(bool),
248 + /// Both ends of a [`FieldKind::Interval`], lower first.
249 + ///
250 + /// Two values rather than one string with a separator, which is
251 + /// [`makeover_layout::Field::upper_name`]'s reason one level down: an
252 + /// interval is submitted under two names, so it is held as two values, and
253 + /// a delimiter this crate owned could appear inside either of them.
254 + ///
255 + /// Either end may be empty while the other stands. An open end is an
256 + /// answer -- "over 120 BPM" -- rather than a half-filled box.
257 + ///
258 + /// Added 0.33.0 with makeover-layout 0.34.0.
259 + Between {
260 + /// What the lower box holds now.
261 + lower: &'a str,
262 + /// What the upper box holds now.
263 + upper: &'a str,
264 + },
248 265 }
249 266
250 267 impl<'a> Held<'a> {
@@ -252,11 +269,20 @@
252 269 #[must_use]
253 270 pub const fn text(self) -> &'a str {
254 271 match self {
255 - Self::Text(text) => text,
272 + Self::Text(text) | Self::Between { lower: text, .. } => text,
256 273 Self::Absent | Self::On(_) => "",
257 274 }
258 275 }
259 276
277 + /// The upper end, for the one variant that has one.
278 + #[must_use]
279 + pub const fn upper(self) -> &'a str {
280 + match self {
281 + Self::Between { upper, .. } => upper,
282 + Self::Absent | Self::Text(_) | Self::On(_) => "",
283 + }
284 + }
285 +
260 286 /// Whether a checkbox is ticked.
261 287 #[must_use]
262 288 pub const fn on(self) -> bool {
@@ -469,6 +495,14 @@
469 495 let line = range_line(style, field, held.text(), well);
470 496 text::draw_line(&line, below(area, used), buf)
471 497 }
498 + // One question, so one line. The two ends read left to right with the
499 + // word between them, which is what a terminal has instead of two boxes
500 + // side by side: a second row would read as a second question, and that
501 + // is the reading the kind exists to prevent.
502 + FieldKind::Interval => {
503 + let line = interval_line(style, field, held, well);
504 + text::draw_line(&line, below(area, used), buf)
505 + }
472 506 kind if kind.offers_options() => {
473 507 let mut rows = 0;
474 508 for choice in field.options {
@@ -589,6 +623,43 @@
589 623 ])
590 624 }
591 625
626 + /// An interval as one line: the low end, the word, the high end.
627 + ///
628 + /// One line because it is one question. Two rows would read as two questions,
629 + /// which is exactly what [`FieldKind::Interval`] exists to stop the description
630 + /// saying, and a terminal has no side-by-side boxes to fall back on.
631 + ///
632 + /// # An open end draws the bound it falls back to
633 + ///
634 + /// Muted, because it is where the axis ends rather than a value anybody set.
635 + /// With no bound to fall back on there is nothing honest to draw and the end
636 + /// stays blank: a terminal inventing a number here would report a filter the
637 + /// user never applied, which is [`range_line`]'s position on an unreadable
638 + /// value.
639 + ///
640 + /// # The word, not a dash
641 + ///
642 + /// A dash between two numbers is a minus sign to anyone reading a signed axis,
643 + /// and half the measured axes are signed -- audiofiles filters loudness in
644 + /// dBFS. `to` costs two cells and cannot be misread.
645 + fn interval_line(
646 + style: &PieceStyle,
647 + field: &Field<'_>,
648 + held: Held<'_>,
649 + well: Style,
650 + ) -> Line<'static> {
651 + let end = |value: &str, fallback: Option<&str>| match (value.is_empty(), fallback) {
652 + (false, _) => Span::styled(measured(field, value), well),
653 + (true, Some(bound)) => Span::styled(measured(field, bound), style.muted),
654 + (true, None) => Span::styled(String::new(), style.muted),
655 + };
656 + Line::from(vec![
657 + end(held.text(), field.min),
658 + Span::styled(" to ", style.secondary),
659 + end(held.upper(), field.max),
660 + ])
661 + }
662 +
592 663 /// The unit to draw beside this field's value, if there is one to draw.
593 664 ///
594 665 /// Two conditions rather than one: the field has to carry a unit and its kind
@@ -1008,6 +1079,81 @@
1008 1079 assert_eq!(rows(&buf)[1].trim_end(), "kick");
1009 1080 }
1010 1081
1082 + #[test]
1083 + fn an_interval_is_one_line_with_both_ends_on_it() {
1084 + // One question, one line. Two rows would read as two questions, which
1085 + // is the reading the kind exists to prevent.
1086 + let style = style();
1087 + let field_ = Field {
1088 + min: Some("0"),
1089 + max: Some("300"),
1090 + unit: Some("BPM"),
1091 + ..Field::interval("bpm_min", "bpm_max", "BPM range")
1092 + };
1093 + let mut buf = buffer(40, 3);
1094 + field(
1095 + &style,
1096 + &field_,
1097 + Held::Between {
1098 + lower: "90",
1099 + upper: "130",
1100 + },
1101 + false,
1102 + buf.area,
1103 + &mut buf,
1104 + );
1105 + assert_eq!(rows(&buf)[0].trim_end(), "BPM range");
1106 + assert_eq!(rows(&buf)[1].trim_end(), "90 BPM to 130 BPM");
1107 + assert_eq!(rows(&buf)[2].trim_end(), "");
1108 + }
1109 +
1110 + #[test]
1111 + fn an_open_end_falls_back_to_the_bound_it_means() {
1112 + // "Over 120" is an answer rather than a half-filled box, and where the
1113 + // axis ends is what the empty end stands for.
1114 + let style = style();
1115 + let field_ = Field {
1116 + min: Some("0"),
1117 + max: Some("300"),
1118 + ..Field::interval("bpm_min", "bpm_max", "BPM range")
1119 + };
1120 + let mut buf = buffer(40, 3);
1121 + field(
1122 + &style,
1123 + &field_,
1124 + Held::Between {
1125 + lower: "120",
1126 + upper: "",
1127 + },
1128 + false,
1129 + buf.area,
1130 + &mut buf,
1131 + );
1132 + assert_eq!(rows(&buf)[1].trim_end(), "120 to 300");
1133 + }
1134 +
1135 + #[test]
1136 + fn an_unbounded_open_end_draws_nothing_rather_than_a_number() {
1137 + // A terminal inventing a bound here would report a filter nobody
1138 + // applied, which is `range_line`'s position on an unreadable value.
1139 + // What is left reads as the sentence it is: up to 130.
1140 + let style = style();
1141 + let field_ = Field::interval("bpm_min", "bpm_max", "BPM range");
1142 + let mut buf = buffer(40, 3);
1143 + field(
1144 + &style,
1145 + &field_,
1146 + Held::Between {
1147 + lower: "",
1148 + upper: "130",
1149 + },
1150 + false,
1151 + buf.area,
1152 + &mut buf,
1153 + );
1154 + assert_eq!(rows(&buf)[1].trim_end(), "to 130");
1155 + }
1156 +
1011 1157 #[test]
1012 1158 fn a_range_holding_something_unreadable_still_shows_it() {
1013 1159 // The app put the value there. A terminal that quietly rounded it to a