Skip to main content

max / alloy

Hand the ANSI slot table to makeover, so shop can paint from it too
Author: Max Johnson <me@maxj.phd> · 2026-07-31 14:57 UTC
Signed with PGP, not checked
Commit: 3b08825cb411dbfea2999f4c1cb2d04364587947
Parent: 0659d87
3 files changed, +27 insertions, -91 deletions
M Cargo.lock +1 -3
@@ -896,9 +896,7 @@
896 896
897 897 [[package]]
898 898 name = "makeover"
899 - version = "2.3.0"
900 - source = "registry+https://github.com/rust-lang/crates.io-index"
901 - checksum = "e35cc903581eea6df09a3a102f101d85a6b941ea18431fcd0a70738762271fdf"
899 + version = "2.4.0"
902 900 dependencies = [
903 901 "include_dir",
904 902 "serde",
M Cargo.toml +8 -1
@@ -12,7 +12,14 @@
12 12
13 13 [workspace.dependencies]
14 14 ratatui = "0.30"
15 - makeover = "2.3.0"
15 + makeover = "2.4.0"
16 +
17 + # makeover 2.4.0 is not on crates.io yet: it carries `ansi_intent`, the
18 + # intent-to-ANSI-slot table this workspace handed over on 2026-07-31. Remove
19 + # this stanza the moment 2.4.0 publishes; a container build cannot see a local
20 + # path, so nothing but a local `cargo test` works while it stands.
21 + [patch.crates-io]
22 + makeover = { path = "../Libraries/makeover" }
16 23
17 24 [profile.release]
18 25 lto = "thin"
@@ -72,94 +72,25 @@
72 72 use anyhow::{Context, Result, anyhow, bail};
73 73 use makeover::{Rgb, ThemeColors};
74 74
75 - /// The twelve chromatic ANSI slots, as the intents they are painted with.
76 - ///
77 - /// One table, because there were two and they disagreed. `kargs.d/10-alloy.toml`
78 - /// carried this mapping for the Linux console and claimed in a comment that it
79 - /// "matches `etc/skel/.config/rio/config.toml`'s `[colors]` block, so a terminal
80 - /// and a bare VT agree on what red means". Three slots did not match: rio put
81 - /// `surface.raised` at 7 where the console had `surface.page`, `content.secondary`
82 - /// at 14 where the console had `category.six`, and `content.primary` at 15 where
83 - /// the console had `surface.raised`.
84 - ///
85 - /// Two of those three are achromatic and come from [`achromatic_slot`] instead;
86 - /// the third, slot 14, is settled here as `category.six`, which is what both
87 - /// the console and `vtrgb.py` had and only rio disagreed with. Rio renders from
88 - /// this table now, which makes the kargs comment's claim structural rather than
89 - /// aspirational.
90 - ///
91 - /// Indexed 1-6 and 9-14. The hues do not depend on whether the theme is light
92 - /// or dark — red is the theme's danger tone either way — which is exactly why
93 - /// the four achromatic slots are not in this table.
94 - const CHROMATIC: [(usize, &str); 12] = [
95 - (1, "status.danger"),
96 - (2, "status.success"),
97 - (3, "status.warning"),
98 - (4, "status.info"),
99 - (5, "category.five"),
100 - (6, "category.six"),
101 - (9, "action.primary"), // bright red — the theme's warm accent
102 - (10, "status.success"),
103 - (11, "status.warning"),
104 - (12, "status.info"),
105 - (13, "category.five"),
106 - (14, "category.six"),
107 - ];
108 -
109 - /// The four achromatic slots — 0, 7, 8, 15 — which invert with the theme.
110 - ///
111 - /// These are the slots a naive table gets wrong. ANSI 0 is "black" and 7 is
112 - /// "white", but what a terminal actually wants there is *the darkest tone* and
113 - /// *the lightest tone*, and which intent that is flips with the theme's
114 - /// polarity. A light theme's darkest tone is its ink; a dark theme's is its
115 - /// deepest surface. Pinning slot 0 to `content.primary` reads correctly on
116 - /// Akari Dawn and inverts on Akari Night, where it would hand the console a
117 - /// pale cream as "black".
118 - ///
119 - /// Not a hypothetical: the two hand-authored Helix themes already did it this
120 - /// way, dawn putting its ink at `black` and night putting its sunken surface
121 - /// there. That divergence was the evidence the rule exists.
122 - ///
123 - /// The arrangement below is `tools/vtrgb.py`'s, which had it right and is the
124 - /// reason that script no longer exists. There turned out to be *three* copies
125 - /// of this table, not the two the kargs comment admitted to, and all three
126 - /// disagreed:
127 - ///
128 - /// - `vtrgb.py` (light): 7 = `surface.raised`, 15 = `surface.overlay`
129 - /// - `rio`'s `[colors]`: 7 = `surface.raised`, 15 = `content.primary`
130 - /// - `kargs.d` (light): 7 = `surface.page`, 15 = `surface.raised`
131 - ///
132 - /// vtrgb.py wins on two grounds. It was the only one that varied by polarity at
133 - /// all, and it was the only one carrying a reason: slot 7 is *the login card*,
134 - /// so the greeter reads as a light card on a darker field, and slot 0 — which
135 - /// the kernel also uses as the console background — stays the darkest tone in
136 - /// either mode. It is also the copy that decided the running system, since
137 - /// `alloy-vtrgb.service` applies its table after the kernel has applied kargs'.
138 - fn achromatic_slot(index: usize, variant: &str) -> Option<&'static str> {
139 - // Anything that is not `dark` — including `high-contrast` — follows the
140 - // light anchors, as vtrgb.py did.
141 - let dark = variant == "dark";
142 - Some(match (index, dark) {
143 - (0, false) => "content.primary", // darkest text tone
144 - (0, true) => "surface.sunken", // darkest surface
145 - (7, false) => "surface.raised", // the login card
146 - (7, true) => "content.secondary", // a readable light tone
147 - (8, _) => "content.muted", // muted chrome, either way
148 - (15, false) => "surface.overlay", // lightest surface
149 - (15, true) => "content.primary", // lightest text tone
150 - _ => return None,
151 - })
152 - }
153 -
154 75 /// The intent painting ANSI slot `index` under a theme of `variant`.
155 - pub fn ansi_intent(index: usize, variant: &str) -> Option<&'static str> {
156 - achromatic_slot(index, variant).or_else(|| {
157 - CHROMATIC
158 - .iter()
159 - .find(|(slot, _)| *slot == index)
160 - .map(|(_, intent)| *intent)
161 - })
162 - }
76 + ///
77 + /// The table itself is `makeover::ansi_intent` as of makeover 2.4.0. It was
78 + /// written here, because this crate is where three disagreeing hand-maintained
79 + /// copies were folded into one: `kargs.d/10-alloy.toml` for the Linux console,
80 + /// rio's `[colors]` block, and `tools/vtrgb.py` for the greeter, which was the
81 + /// copy that had it right and no longer exists. Three slots differed between
82 + /// them, two achromatic and one chromatic (14, settled as `category.six`).
83 + ///
84 + /// It moved out on 2026-07-31 for the reason it was collected in the first
85 + /// place. `shop`, which replaces rio as Alloy's terminal, paints its palette at
86 + /// runtime from a theme rather than reading a generated config, so it needs the
87 + /// mapping as code and not as rendered hex. Copying it into shop would have
88 + /// made a fourth copy of the table this crate exists to have prevented.
89 + ///
90 + /// Kept as a re-export rather than deleted: templates reach it through
91 + /// `@{ansi.N}` and [`Palette::ansi`], and the callers here read better naming
92 + /// the crate that owns the rest of the skeleton.
93 + pub use makeover::ansi_intent;
163 94
164 95 /// Every color a template can name, resolved from one theme.
165 96 ///