Skip to main content

max / makeover-tui

Strip historical narrative from documentation Remove what a doc used to say, when it changed, the incidents that justified a rule, finished migration narration, and counts and versions that rot. State the rules in the present tense instead. Keep every instruction, prohibition and threshold, and keep the measurements that make a rule actionable. Public-facing docs keep their explanatory voice.
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-08-31 01:51 UTC
Signed with PGP, not checked
Commit: 74d56ab965c8f2a01f80b43d8f86421b0ca1c06b
Parent: 1a9946b
4 files changed, +94 insertions, -148 deletions
M README.md +11 -16
@@ -8,8 +8,8 @@
8 8
9 9 ## What a terminal actually costs you
10 10
11 - Not colour. That was this crate's original assumption and it is wrong on any
12 - terminal built this decade. Measured across the 31 shipped themes
11 + Not colour, on any terminal built this decade. Measured across the 31 shipped
12 + themes
13 13 (`makeover`'s `well_fidelity` example):
14 14
15 15 | | ANSI-16 | ANSI-256 | truecolor |
@@ -51,24 +51,19 @@
51 51 a fill will be visible against what is behind it, and at truecolor it fires on
52 52 exactly the two clamping themes, which is when it should.
53 53
54 - ## The correction it forced
54 + ## Substitution is renderer policy
55 55
56 - `makeover-layout::Fill` briefly carried a `fallback`, returning `Page` for
57 - `Well` so a consumer without `surface-well` had something to paint. That is an
58 - answer for a renderer that can always paint an exact colour. Here it is actively
59 - wrong: the page is usually the surface a well is cut *into*, so falling back to
60 - it produces precisely the invisibility the fallback existed to prevent.
61 -
62 - Substituting one intent for another turned out to be renderer policy rather than
63 - description. It moved out of `makeover-layout` and into `makeover-immediate`,
64 - which does want it. That is the first thing a second renderer was built to find,
65 - and it took a day rather than the API-window's usual 48 hours after adoption.
56 + Substituting one intent for another belongs to a renderer, never to the
57 + description. Falling back from `Fill::Well` to `Page` is an answer for a
58 + renderer that can always paint an exact colour; here it is wrong, because the
59 + page is usually the surface a well is cut *into*, so the substitution produces
60 + the invisibility it was meant to prevent. `makeover-immediate` wants that
61 + fallback and this renderer does not.
66 62
67 63 ## Status
68 64
69 - On crates.io at 0.1.0. No consumer yet: alloy_tui and shop are the intended
70 - first ones, both of which already take makeover and makeover-geometry from the
71 - registry.
65 + On crates.io. Intended first consumers are alloy_tui and shop, both of which
66 + already take makeover and makeover-geometry from the registry.
72 67
73 68 Design lives in the wiki note `makeover-tui`; the backlog is in GoingsOn under
74 69 the project of the same name.
M src/lib.rs +67 -96
@@ -56,90 +56,69 @@
56 56 //! will be visible against what is behind it, and at truecolor it fires on
57 57 //! exactly the two clamping themes, which is precisely when it should.
58 58 //!
59 - //! # 0.13.0: a modal, and two cues four ports were about to each invent
59 + //! # Overlays, selection and the focus ring
60 60 //!
61 - //! [`Depth::Overlay`](makeover_layout::Depth::Overlay) arrives in
62 - //! makeover-layout 0.14.0 and needed nothing here: [`Palette::fill`] has
63 - //! answered `Fill::Overlay` since this crate had a palette, so what was missing
64 - //! was the route from a description rather than the drawing. A test asserts it,
65 - //! because a route nothing exercises is one a refactor can quietly lose.
61 + //! [`Palette::fill`] answers `Fill::Overlay`, so
62 + //! [`Depth::Overlay`](makeover_layout::Depth::Overlay) needs nothing beyond the
63 + //! route from a description. A test asserts that route, because a route nothing
64 + //! exercises is one a refactor can quietly lose.
66 65 //!
67 - //! [`Theme::selection_on`] and [`Theme::focus_ring`] are the other half, and
68 - //! both are DERIVED rather than authored. Every consumer measured did selection
69 - //! with `REVERSED`, for want of an on-accent foreground; every one that wanted
70 - //! a focus ring either spent makeover's `border-strong` on it, which is a
71 - //! divider at 1.63:1 on Akari Dawn, or derived its own the way `alloy_tui`
72 - //! does. Four terminal ports were each about to answer that separately.
66 + //! [`Theme::selection_on`] and [`Theme::focus_ring`] are DERIVED rather than
67 + //! authored, and the direction is one-way: an authored key can fall back to a
68 + //! derivation and break no theme on disk, while a key this crate started
69 + //! requiring would break every theme that lacks it. So the theme format does not
70 + //! change, and the promotion stays available for a theme that ever needs to tune
71 + //! either. Without them a port does selection with `REVERSED` for want of an
72 + //! on-accent foreground, and spends makeover's `border-strong` on a focus ring,
73 + //! which is a divider at 1.63:1 on Akari Dawn.
73 74 //!
74 - //! Derived, not authored, because the direction is one-way. An authored key can
75 - //! fall back to a derivation and break no theme on disk; a key this crate
76 - //! started requiring would break every theme that lacks it. So the theme format
77 - //! does not change and nothing on disk grows, and the promotion stays available
78 - //! for a theme that ever needs to tune either.
75 + //! # The table
79 76 //!
80 - //! # 0.14.0: the first structural widget
81 - //!
82 - //! [`table`] is the first thing here that draws content rather than a surface,
83 - //! and it exists because 14 call sites across `mnw-cli` and `viewer` were
84 - //! already drawing one. `mnw-cli` had written the mapping layer by hand
85 - //! (`src/tui/widgets.rs`: a muted bold header, a selected row carried by the
86 - //! background alone) and `viewer` had written a smaller one, which is two
87 - //! answers to a question this crate is supposed to answer once.
88 - //!
89 - //! It is a mapping layer over [`ratatui::widgets::Table`] rather than a table
77 + //! [`table`] is the one thing here that draws content rather than a surface. It
78 + //! is a mapping layer over [`ratatui::widgets::Table`] rather than a table
90 79 //! implementation, because ratatui already lays tracks out, draws a header,
91 80 //! highlights a row and scrolls. What it has no answer for is content
92 81 //! measurement and narrowing, and those are what the module is.
93 82 //!
94 - //! # 0.19.0: `widget` is [`piece`], because the word went to the description
83 + //! # `piece`, not `widget`
95 84 //!
96 - //! `makeover-layout` 0.20.0 added `Region::Widget`, the third tier between a
97 - //! primitive and `Region::Bespoke`: a named assembly of primitives that each
98 - //! renderer draws its own way. That is host-agnostic and sits *above* every
99 - //! renderer.
85 + //! `makeover-layout` owns `Region::Widget`, the tier between a primitive and
86 + //! `Region::Bespoke`: a named assembly of primitives that each renderer draws
87 + //! its own way. That is host-agnostic and sits *above* every renderer.
100 88 //!
101 - //! This crate's `widget` module is the opposite end of the same stack —
89 + //! This crate's [`piece`] module is the opposite end of the same stack:
102 90 //! renderer-local, the answer to what a meter looks like in cells, taking a
103 - //! description plus what only a terminal knows. Two different things wearing
104 - //! one word, and the collision would have been worst exactly here, in a crate
105 - //! that has to implement the tier.
91 + //! description plus what only a terminal knows. Two different things must not
92 + //! wear one word, least of all in a crate that has to implement the tier. The
93 + //! style type is [`piece::PieceStyle`].
106 94 //!
107 - //! So this half moved and the description's half kept the word. That direction
108 - //! is not arbitrary: a second or third party naming a widget is naming the
109 - //! layout kind, and nothing outside this tree ever needed a word for a drawing
110 - //! routine. `WidgetStyle` is `PieceStyle`.
95 + //! # A bounded number, and an option that is not offered yet
111 96 //!
112 - //! # 0.27.0: a bar for a bounded number, and an option that is not offered yet
113 - //!
114 - //! `makeover-layout` 0.28.0's form findings, at the renderer that has the bar
115 - //! already. A [`makeover_layout::FieldKind::Range`] is drawn as [`piece::meter`]'s
116 - //! cells with its two ends read out either side, because the ends are what the
97 + //! A [`makeover_layout::FieldKind::Range`] is drawn as [`piece::meter`]'s cells
98 + //! with its two ends read out either side, because the ends are what the
117 99 //! question means and a terminal is where it would be easiest to quietly show a
118 - //! figure instead. An unbounded range falls back to the text path rather than
119 - //! to bounds this crate invented.
100 + //! figure instead. An unbounded range falls back to the text path rather than to
101 + //! bounds this crate invented.
120 102 //!
121 103 //! `Choice::unavailable` is the one place the three-tone convention's muted is
122 104 //! the truth rather than the lie it warns about: that option will not answer,
123 - //! and the reason it will not now sits on its row.
105 + //! and the reason it will not sits on its row.
124 106 //!
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.
107 + //! `Choice::detail` takes a row of its own under the option, inset by the width
108 + //! of the mark and muted for the same reason: the line is not a thing to press.
109 + //! This is the host with the most room of the three, so unlike a browser's
110 + //! `<select>` it does not run the line into the option's text.
130 111 //!
131 - //! `Field::placeholder` on a chooser, the third finding, is already answered
132 - //! here and needed nothing: this renderer draws every option of a select at
133 - //! once, so an unanswered one is a list with no mark against any row rather
134 - //! than an empty box with nothing in it.
112 + //! `Field::placeholder` on a chooser needs nothing here: this renderer draws
113 + //! every option of a select at once, so an unanswered one is a list with no mark
114 + //! against any row rather than an empty box.
135 115 //!
136 - //! # 0.33.0: an interval is one line
116 + //! # An interval is one line
137 117 //!
138 - //! `makeover-layout` 0.34.0's [`makeover_layout::FieldKind::Interval`], drawn
139 - //! as the low end, the word `to`, and the high end. One line because it is one
140 - //! question: two rows would read as two questions, which is exactly what the
141 - //! kind exists to stop the description saying, and a terminal has no
142 - //! side-by-side boxes to fall back on.
118 + //! [`makeover_layout::FieldKind::Interval`] is drawn as the low end, the word
119 + //! `to`, and the high end. One line because it is one question: two rows would
120 + //! read as two questions, which is what the kind exists to stop the description
121 + //! saying, and a terminal has no side-by-side boxes to fall back on.
143 122 //!
144 123 //! - **An open end draws the bound it falls back to**, muted, because that is
145 124 //! where the axis ends rather than a value anybody set. With no bound to fall
@@ -155,39 +134,32 @@
155 134 //! states both names: a separator inside one string would make this crate own a
156 135 //! delimiter either end could contain.
157 136 //!
158 - //! # 0.32.0: a number reads with its unit
137 + //! # A number reads with its unit
159 138 //!
160 - //! `makeover-layout` 0.33.0's `Field::unit`, drawn on the value rather than in
161 - //! the label. A terminal has one line the eye is on -- the number -- and the
162 - //! label is a line above it, so `0.05 s` is the reading and `Attack (s)` two
163 - //! rows up is not. A range shows it after the readout beside the bar; a typed
164 - //! number after the value. Every other kind ignores it, and which those are is
165 - //! `FieldKind::measurable`'s answer rather than a `matches!` kept here.
139 + //! `Field::unit` is drawn on the value rather than in the label. A terminal has
140 + //! one line the eye is on, the number, and the label is a line above it, so
141 + //! `0.05 s` is the reading and `Attack (s)` two rows up is not. A range shows it
142 + //! after the readout beside the bar; a typed number after the value. Every other
143 + //! kind ignores it, and which those are is `FieldKind::measurable`'s answer
144 + //! rather than a `matches!` kept here.
166 145 //!
167 - //! # 0.31.0: the bar fills along the curve
146 + //! # The bar fills along the curve
168 147 //!
169 - //! `makeover-layout` 0.32.0's [`Curve`](makeover_layout::Curve). Where a value
170 - //! sits on the bar is the curve's answer now, not its proportion of the extent.
171 - //! Under `Curve::Linear` those are the same number, so every range drawn before
172 - //! this is unchanged; under a constant ratio they are not, and a bar filled
173 - //! linearly would put an envelope's whole useful half inside its first cell.
148 + //! Where a value sits on the bar is [`Curve`](makeover_layout::Curve)'s answer,
149 + //! not its proportion of the extent. Under `Curve::Linear` those are the same
150 + //! number; under a constant ratio they are not, and a bar filled linearly would
151 + //! put an envelope's whole useful half inside its first cell.
174 152 //!
175 - //! The two ends beside the bar do not move: they are `f(0)` and `f(1)`, which
176 - //! is what they always were and is now what the description calls them.
153 + //! The two ends beside the bar do not move: they are `f(0)` and `f(1)`.
177 154 //!
178 - //! # The correction this renderer forced
155 + //! # Substitution is renderer policy
179 156 //!
180 - //! [`makeover_layout::Fill`] briefly carried a `fallback` method, returning
181 - //! `Page` for `Well` so a consumer without `surface-well` had something to
182 - //! use. That is an answer for a renderer that can always paint a colour. Here
183 - //! it is actively wrong: page *is* the surface a well is usually cut into, so
184 - //! falling back to it produces the exact invisibility the fallback was meant
185 - //! to avoid.
186 - //!
187 - //! Substituting one intent for another is renderer policy, not description.
188 - //! The fallback moved out of the description and into
189 - //! `makeover-immediate`, where it belongs, which is the first thing a second
190 - //! renderer was built to find.
157 + //! Substituting one intent for another belongs to a renderer, never to the
158 + //! description. Falling back from `Fill::Well` to `Page` is an answer for a
159 + //! renderer that can always paint a colour; here it is wrong, because the page
160 + //! *is* the surface a well is usually cut into, so the substitution produces the
161 + //! exact invisibility it was meant to avoid. `makeover-immediate` wants that
162 + //! fallback and this renderer does not.
191 163
192 164 #![forbid(unsafe_code)]
193 165
@@ -317,7 +289,7 @@
317 289 pub raised: Color,
318 290 /// `surface-overlay`.
319 291 pub overlay: Color,
320 - /// `surface-well`, absent on makeover before 2.3.0.
292 + /// `surface-well`, when the theme has one.
321 293 pub well: Option<Color>,
322 294 /// `bevel-light`.
323 295 pub bevel_light: Color,
@@ -334,9 +306,9 @@
334 306 /// No substitution happens here. A missing intent stays missing, and
335 307 /// [`frame`] answers it with structure instead of with a different colour.
336 308 /// That rule is what lets the wildcard below be a real answer rather than
337 - /// a hole: [`Fill`] is `#[non_exhaustive]` from `makeover-layout` 0.4.0
338 - /// onward, so the description can name a surface this renderer has not
339 - /// learned to paint, and saying so is better than failing to build.
309 + /// a hole: [`Fill`] is `#[non_exhaustive]`, so the description can name a
310 + /// surface this renderer has not learned to paint, and saying so is better
311 + /// than failing to build.
340 312 #[must_use]
341 313 pub const fn fill(&self, fill: Fill) -> Option<Color> {
342 314 match fill {
@@ -439,8 +411,7 @@
439 411 /// sides, which draws a *line* rather than a lit edge and gives up the light
440 412 /// model that makes a bevel legible.
441 413 ///
442 - /// Adopted from `alloy_tui`, which reached this independently and got there
443 - /// first (2026-07-26, two days before this crate existed).
414 + /// Shared with `alloy_tui`, which reached the same glyph set independently.
444 415 pub(crate) const BEVEL: GlyphSet = GlyphSet {
445 416 top: "▀",
446 417 bottom: "▄",
M src/piece.rs +15 -34
@@ -1,32 +1,17 @@
1 1 //! The pieces every terminal app draws, drawn once.
2 2 //!
3 - //! # Called `widget` until 0.19.0
3 + //! # Not `widget`
4 4 //!
5 - //! Renamed because `makeover-layout` 0.20.0 took the word for something else,
6 - //! and the two meanings do not sit together. A `Region::Widget` there is
7 - //! host-agnostic: a named assembly of primitives that every renderer draws its
8 - //! own way. What is in this module is the opposite end — renderer-local, the
9 - //! answer to *what a meter looks like in cells*, taking a description plus what
10 - //! only a terminal knows.
5 + //! `makeover-layout` owns that word for something else, and the two meanings do
6 + //! not sit together. A `Region::Widget` there is host-agnostic: a named
7 + //! assembly of primitives that every renderer draws its own way. What is in
8 + //! this module is the opposite end, renderer-local, the answer to *what a meter
9 + //! looks like in cells*, taking a description plus what only a terminal knows.
10 + //! The style type is `PieceStyle`.
11 11 //!
12 - //! One word for both would have made the tier unreadable in the crate that
13 - //! implements it. This half moved because the other half is the ecosystem-facing
14 - //! one: a second or third party naming a widget is naming the layout kind, and
15 - //! nothing outside this tree ever needed a word for a drawing routine.
16 - //!
17 - //! `WidgetStyle` went with it and is `PieceStyle`.
18 - //!
19 - //! Arrived in 0.16.0 out of `quasi-tui`, which had written all of them and was
20 - //! the second consumer to do so. A meter, a badge, a control, a figure and a
21 - //! form field are what a screen is made of below the level [`table`](crate::table)
22 - //! works at, and every one of them had been hand-rolled at least twice in this
23 - //! tree before it was lifted.
24 - //!
25 - //! [`activity`] and [`awaiting`] joined them in 0.35.0, out of wiki
26 - //! `loading-and-progress-standard`. They are the one pair here that arrived
27 - //! before their second consumer rather than after it: nothing in the tree drew
28 - //! a wait at all, on any surface, which is why the crate that had the vocabulary
29 - //! for one had never been asked for the drawing.
12 + //! A meter, a badge, a control, a figure and a form field are what a screen is
13 + //! made of below the level [`table`](crate::table) works at. [`activity`] and
14 + //! [`awaiting`] draw a wait, out of wiki `loading-and-progress-standard`.
30 15 //!
31 16 //! # What these take, and what they leave alone
32 17 //!
@@ -263,8 +248,6 @@
263 248 ///
264 249 /// Either end may be empty while the other stands. An open end is an
265 250 /// answer -- "over 120 BPM" -- rather than a half-filled box.
266 - ///
267 - /// Added 0.33.0 with makeover-layout 0.34.0.
268 251 Between {
269 252 /// What the lower box holds now.
270 253 lower: &'a str,
@@ -524,8 +507,7 @@
524 507 ///
525 508 /// Its own function rather than extra lines out of [`act`], because a control
526 509 /// is one [`Line`] everywhere it is drawn and a caller laying out a run needs
527 - /// to know it is placing two things. Added 0.40.0 with `Act::hint`; quasi-tui
528 - /// built this line itself before that.
510 + /// to know it is placing two things.
529 511 #[must_use]
530 512 pub fn act_note(style: &PieceStyle, act: &Act<'_>) -> Option<Line<'static>> {
531 513 act.hint
@@ -911,9 +893,9 @@
911 893
912 894 /// A value with what it is measured in, as one string.
913 895 ///
914 - /// The unit rides on the value rather than on the label, which is
915 - /// `makeover-layout` 0.33.0's rule and is what a terminal wants anyway: the
916 - /// label is a line above and the number is the line the eye is on.
896 + /// The unit rides on the value rather than on the label, which is what a
897 + /// terminal wants: the label is a line above and the number is the line the eye
898 + /// is on.
917 899 fn measured(field: &Field<'_>, value: &str) -> String {
918 900 match unit_of(field) {
919 901 Some(unit) => format!("{value} {unit}"),
@@ -979,8 +961,7 @@
979 961 /// What is left of `area` after `used` rows from the top.
980 962 /// The rows under what has been drawn, inset by the width of an option's mark.
981 963 ///
982 - /// makeover-layout 0.39.0. An option's second line has to read as belonging to
983 - /// the option above it rather than as another option, and the only thing that
964 + /// An option's second line has to read as belonging to the option above it rather than as another option, and the only thing that
984 965 /// says so on a terminal is where it starts. The inset is `text::draw`'s to
985 966 /// honour as an area rather than as spaces in the string: the drawing wraps on
986 967 /// words, so leading spaces would survive the first line and vanish from every
M src/text.rs +1 -2
@@ -5,8 +5,7 @@
5 5 //! both [`height`] and [`draw`] read it, rather than each having its own idea
6 6 //! of how many rows a paragraph takes.
7 7 //!
8 - //! Arrived here from `quasi-tui` in 0.16.0, which is where it was written and
9 - //! where it stopped being quasi's: nothing below is about a described screen.
8 + //! Nothing below is about a described screen.
10 9 //! Flow layout is the shape every terminal consumer in the tree ends up with —
11 10 //! ask for a height at a width, then draw into the rect you were given — and it
12 11 //! needs a wrap that answers both questions the same way. ratatui's own