Skip to main content

max / makeover-tui

0.36.0: one line under the box, chosen by the precedence rule makeover-layout 0.36.0 adds `Field::note`, a third message channel, and a terminal field has room for exactly one line. `note_of` -- which answered `error.or(hint)` and now collides with the member's name -- becomes `message_of` and returns the text with the style to paint it: error, then note, then hint, which is the order `Field::note` names so that three renderers cannot arrive at three answers. A note carries its own tone, so `message_of` resolves it through `PieceStyle::tone` rather than assuming a note is a warning. An error is still always danger and a hint still always muted, because neither carries a tone to consult.
Author: Max Johnson <me@maxj.phd> · 2026-08-27 23:28 UTC
Signed with PGP, not checked
Commit: 1653d3b6088b16c3cb373b21b50445f40d00353e
Parent: b7375f4
2 files changed, +51 insertions, -18 deletions
M Cargo.toml +2 -2
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-tui"
3 - version = "0.35.0"
3 + version = "0.36.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.35.0"
22 + makeover-layout = "0.36.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/piece.rs +49 -16
@@ -574,7 +574,7 @@
574 574 kind if kind.offers_options() => u16::try_from(field.options.len()).unwrap_or(u16::MAX),
575 575 _ => 1,
576 576 };
577 - let note = note_of(field).map_or(0, |note| text::height(note, width));
577 + let note = message_of(style, field).map_or(0, |(text, _)| text::height(text, width));
578 578 label + body + note
579 579 }
580 580
@@ -682,18 +682,12 @@
682 682 _ => text::draw(&measured(field, held.text()), well, below(area, used), buf),
683 683 };
684 684
685 - // The error wins over the hint, the same order a webview uses: a hint is
686 - // what to type and an error is what went wrong, and once something has gone
687 - // wrong that is the sentence worth the row.
688 - match note_of(field) {
689 - Some(note) => {
690 - let painted = if field.error.is_some() {
691 - style.danger
692 - } else {
693 - style.muted
694 - };
695 - used + text::draw(note, painted, below(area, used), buf)
696 - }
685 + // Error, then note, then hint -- the order `Field::note` names, and the
686 + // order a webview draws them in. Once something has gone wrong that is the
687 + // sentence worth the row; failing that, what the chosen answer costs beats
688 + // standing help about how the field works.
689 + match message_of(style, field) {
690 + Some((text, painted)) => used + text::draw(text, painted, below(area, used), buf),
697 691 None => used,
698 692 }
699 693 }
@@ -819,9 +813,24 @@
819 813 }
820 814 }
821 815
822 - /// What goes under the box: what is wrong now, or the standing help.
823 - fn note_of<'a>(field: &Field<'a>) -> Option<&'a str> {
824 - field.error.or(field.hint)
816 + /// What goes under the box, and how it is painted.
817 + ///
818 + /// A terminal field has room for exactly one line, so the three message
819 + /// channels compete for it and the precedence is decided in
820 + /// [`makeover_layout::Field::note`]'s docs rather than three times here:
821 + /// **error, then note, then hint**. What is wrong outranks what the answer
822 + /// costs, which outranks how the field works.
823 + ///
824 + /// The tone comes with the note; an error is always danger and a hint is
825 + /// always muted, because neither carries one.
826 + fn message_of<'a>(style: &PieceStyle, field: &Field<'a>) -> Option<(&'a str, Style)> {
827 + if let Some(error) = field.error {
828 + return Some((error, style.danger));
829 + }
830 + if let Some((tone, note)) = field.note {
831 + return Some((note, style.tone(tone)));
832 + }
833 + field.hint.map(|hint| (hint, style.muted))
825 834 }
826 835
827 836 /// A box with nothing in it: the ghost text, and the caret when it has focus.
@@ -863,6 +872,30 @@
863 872
864 873 #[cfg(test)]
865 874 mod tests {
875 +
876 + #[test]
877 + fn one_line_takes_the_error_then_the_note_then_the_hint() {
878 + // A terminal field has room for exactly one message, so the three
879 + // channels compete and `Field::note` decides the order.
880 + let style = PieceStyle::default();
881 + let mut f = Field::new(FieldKind::Text, "title", "Title");
882 + f.hint = Some("how it works");
883 + assert_eq!(message_of(&style, &f).unwrap().0, "how it works");
884 +
885 + f.note = Some((Tone::Warning, "what it costs"));
886 + assert_eq!(message_of(&style, &f).unwrap().0, "what it costs");
887 + assert_eq!(message_of(&style, &f).unwrap().1, style.warning);
888 +
889 + f.error = Some("what is wrong");
890 + assert_eq!(message_of(&style, &f).unwrap().0, "what is wrong");
891 + assert_eq!(message_of(&style, &f).unwrap().1, style.danger);
892 +
893 + // A note carries its own tone, so a quiet one is not painted as a
894 + // warning just for being a note.
895 + f.error = None;
896 + f.note = Some((Tone::Neutral, "an ordinary fact"));
897 + assert_eq!(message_of(&style, &f).unwrap().1, style.content);
898 + }
866 899 use super::*;
867 900 use makeover_layout::{Choice, State};
868 901