use abstracttui::base::palette::XTERM_256;
use abstracttui::base::{Rgba, Size};
use abstracttui::render::color::{
nearest_ansi16, nearest_xterm256, quantize_pair_256, quantize_set_256,
quantize_set_256_into_with, GroundIntent, PairIntent,
};
use abstracttui::render::{Cell, ColorDepth, FrameDiff, PresentCaps, Presenter, Style, Surface};
use abstracttui::testing::{xterm_256, VtScreen};
use abstracttui::theme::{themes, TokenSet};
fn grounds(t: &TokenSet) -> [(&'static str, Rgba); 5] {
t.grounds().map(|(id, c)| (id.name(), c))
}
fn ground_pairs(t: &TokenSet) -> Vec<(String, Rgba, Rgba)> {
let g = grounds(t);
let mut out = Vec::new();
for i in 0..g.len() {
for j in (i + 1)..g.len() {
out.push((format!("{} vs {}", g[i].0, g[j].0), g[i].1, g[j].1));
}
}
out
}
const KNOWN_256_COLLAPSES: &[(&str, &str)] = &[
("abstract-dark", "bg vs surface"),
("abstract-light", "bg vs surface"),
("observer-night", "bg vs surface"),
("catppuccin-mocha", "bg vs surface"),
("catppuccin-macchiato", "surface vs shadow_ground"),
("catppuccin-frappe", "bg vs surface"),
("rose-pine", "bg vs surface"),
("rose-pine-moon", "bg vs surface"),
("tokyo-night", "surface_raised vs selection_bg"),
("solarized-dark", "surface_raised vs selection_bg"),
("catppuccin-latte", "surface_raised vs shadow_ground"),
("rose-pine-dawn", "bg vs surface_raised"),
("one-light", "bg vs surface"),
("everforest-light", "surface_raised vs shadow_ground"),
("abstract-midnight", "bg vs shadow_ground"),
];
#[test]
fn ground_collapse_at_256_colours_is_exactly_the_known_set() {
let mut found: Vec<(String, String)> = Vec::new();
for th in themes() {
for (name, a, b) in ground_pairs(&th.tokens) {
if nearest_xterm256(a) == nearest_xterm256(b) {
found.push((th.id.to_string(), name));
}
}
}
let actual: Vec<(&str, &str)> = found
.iter()
.map(|(t, n)| (t.as_str(), n.as_str()))
.collect();
assert_eq!(
actual, KNOWN_256_COLLAPSES,
"the set of grounds that collapse at 256 colours changed.\n\
GREW: a theme lost a surface distinction — fix it or the theme \
ships with two grounds that render as one on a 256-colour \
terminal.\n\
SHRANK: something improved; update KNOWN_256_COLLAPSES and say \
so on claim:tui-audit-does-not-survive-quantisation."
);
}
#[test]
fn no_theme_needs_a_three_way_ground_solve() {
for th in themes() {
let g = grounds(&th.tokens);
let collisions = ground_pairs(&th.tokens)
.iter()
.filter(|(_, a, b)| nearest_xterm256(*a) == nearest_xterm256(*b))
.count();
assert!(
collisions <= 1,
"{}: {collisions} colliding ground pairs. More than one means \
the pairwise re-pick may not converge on its own and the fix \
needs a joint solve — which needs adjacency, which only the \
compositor knows.",
th.id
);
let mut idx: Vec<u8> = g.iter().map(|(_, c)| nearest_xterm256(*c)).collect();
idx.sort_unstable();
idx.dedup();
assert!(
idx.len() >= g.len() - 1,
"{}: five grounds occupy only {} palette entries — more than \
one pair has merged",
th.id,
idx.len()
);
}
}
#[test]
fn borders_survive_256_quantisation_even_where_the_fill_does_not() {
for th in themes() {
let t = &th.tokens;
assert_ne!(
nearest_xterm256(t.border),
nearest_xterm256(t.bg),
"{}: border collapses into bg at 256 colours — the last cue \
that a panel exists would be gone",
th.id
);
}
}
#[test]
fn selection_survives_against_bg_but_not_against_every_ground() {
for th in themes() {
assert_ne!(
nearest_xterm256(th.tokens.selection_bg),
nearest_xterm256(th.tokens.bg),
"{}: selection band is invisible on the app ground at 256 \
colours — this is the one selection property that has always \
held, and it just stopped",
th.id
);
}
let on_raised: Vec<&str> = themes()
.iter()
.filter(|th| {
nearest_xterm256(th.tokens.selection_bg) == nearest_xterm256(th.tokens.surface_raised)
})
.map(|th| th.id)
.collect();
assert_eq!(
on_raised,
["tokyo-night", "solarized-dark"],
"the set of themes whose selection vanishes on `surface_raised` \
changed — a selected row inside a popover is the case this \
covers, and it is in KNOWN_256_COLLAPSES too"
);
}
#[test]
fn ansi16_ground_collapse_is_measured_and_unknowable() {
let mut collapsed = 0;
let mut total = 0;
for th in themes() {
for (_, a, b) in ground_pairs(&th.tokens) {
total += 1;
if nearest_ansi16(a) == nearest_ansi16(b) {
collapsed += 1;
}
}
}
assert_eq!(total, 260, "26 themes x 10 ground pairs");
assert!(
collapsed > 0 && collapsed < total,
"expected SOME 16-colour collapse and not total collapse, got \
{collapsed}/{total} — a 0 or a {total} means the measurement \
stopped measuring"
);
}
const JND: f32 = 2.3;
fn srgb_to_linear(u: u8) -> f32 {
let c = u as f32 / 255.0;
if c <= 0.04045 {
c / 12.92
} else {
((c + 0.055) / 1.055).powf(2.4)
}
}
fn lab(c: Rgba) -> (f32, f32, f32) {
let (r, g, b) = (
srgb_to_linear(c.r),
srgb_to_linear(c.g),
srgb_to_linear(c.b),
);
let x = (0.4124 * r + 0.3576 * g + 0.1805 * b) / 0.95047;
let y = 0.2126 * r + 0.7152 * g + 0.0722 * b;
let z = (0.0193 * r + 0.1192 * g + 0.9505 * b) / 1.08883;
let f = |t: f32| {
if t > 0.008856 {
t.cbrt()
} else {
7.787 * t + 16.0 / 116.0
}
};
let (fx, fy, fz) = (f(x), f(y), f(z));
(116.0 * fy - 16.0, 500.0 * (fx - fy), 200.0 * (fy - fz))
}
fn delta_e(a: Rgba, b: Rgba) -> f32 {
let (l1, a1, b1) = lab(a);
let (l2, a2, b2) = lab(b);
((l1 - l2).powi(2) + (a1 - a2).powi(2) + (b1 - b2).powi(2)).sqrt()
}
fn relative_luminance(c: Rgba) -> f32 {
0.2126 * srgb_to_linear(c.r) + 0.7152 * srgb_to_linear(c.g) + 0.0722 * srgb_to_linear(c.b)
}
fn nearest_separating(orig: Rgba, other: Rgba) -> Option<(Rgba, f32)> {
const RADIUS: i32 = 14;
let stays_lighter = relative_luminance(orig) >= relative_luminance(other);
let q_other = nearest_xterm256(other);
let mut best: Option<(Rgba, f32)> = None;
for dr in -RADIUS..=RADIUS {
for dg in -RADIUS..=RADIUS {
for db in -RADIUS..=RADIUS {
let (r, g, b) = (orig.r as i32 + dr, orig.g as i32 + dg, orig.b as i32 + db);
if !(0..=255).contains(&r) || !(0..=255).contains(&g) || !(0..=255).contains(&b) {
continue;
}
let c = Rgba::rgb(r as u8, g as u8, b as u8);
if nearest_xterm256(c) == q_other {
continue;
}
if (relative_luminance(c) >= relative_luminance(other)) != stays_lighter {
continue;
}
let d = delta_e(orig, c);
if best.is_none_or(|(_, bd)| d < bd) {
best = Some((c, d));
}
}
}
}
best
}
fn collapsed_grounds() -> Vec<(&'static str, &'static str, Rgba, Rgba)> {
KNOWN_256_COLLAPSES
.iter()
.map(|(id, pair)| {
let t = abstracttui::theme::get(id).expect("known-set theme is registered");
let (anchor_name, mover_name) = pair.split_once(" vs ").expect("pair name is 'a vs b'");
let of = |want: &str| {
grounds(&t.tokens)
.into_iter()
.find(|(n, _)| *n == want)
.unwrap_or_else(|| panic!("{id}: no ground named {want}"))
.1
};
(*id, *pair, of(anchor_name), of(mover_name))
})
.collect()
}
const B_IS_NOT_FREE_FOR: &[(&str, &str)] = &[("solarized-dark", "surface_raised vs selection_bg")];
#[test]
fn every_collapsed_ground_separates_for_a_sub_jnd_move() {
let mut expensive = Vec::new();
for (id, pair, anchor, mover) in collapsed_grounds() {
let cheapest = [
nearest_separating(mover, anchor),
nearest_separating(anchor, mover),
]
.into_iter()
.flatten()
.map(|(_, d)| d)
.fold(f32::INFINITY, f32::min);
assert!(
cheapest.is_finite(),
"{id} ({pair}): NEITHER ground can be separated inside the ±14 \
box — option (b) cannot fix this theme by any small edit"
);
if cheapest >= JND {
expensive.push((id, pair));
}
}
assert_eq!(
expensive, B_IS_NOT_FREE_FOR,
"the set of pairs option (b) cannot fix invisibly changed.\n\
GREW: another theme now needs a VISIBLE edit to separate — (b) \
gets worse and the argument for fixing this at render time gets \
stronger.\n\
SHRANK: a theme became cheap to fix; update the list and say so \
on claim:tui-audit-does-not-survive-quantisation."
);
}
#[test]
fn solarized_darks_raised_ground_cannot_be_moved_at_all() {
let t = abstracttui::theme::get("solarized-dark").expect("house port");
assert!(
nearest_separating(t.tokens.surface_raised, t.tokens.selection_bg).is_none(),
"solarized-dark's surface_raised can now be separated from \
selection_bg by a small edit. That un-sticks the one case option \
(b) could not reach, so the claim that (b) is UNAVAILABLE for a \
theme — not just expensive — no longer holds and the costing on \
claim:tui-audit-does-not-survive-quantisation needs updating."
);
}
#[test]
fn the_existing_repick_lands_where_the_ideal_seed_edit_lands() {
for (id, pair, anchor, mover) in collapsed_grounds() {
let (q_mover, q_anchor) = quantize_pair_256(mover, anchor);
assert_ne!(
q_mover, q_anchor,
"{id} ({pair}): the existing re-pick policy failed to separate \
two grounds — option (c) needs a new policy after all"
);
let (ideal, _) = nearest_separating(mover, anchor)
.unwrap_or_else(|| panic!("{id} ({pair}): no separating edit"));
assert_eq!(
q_mover,
nearest_xterm256(ideal),
"{id} ({pair}): the re-pick chose a different entry than the \
optimal seed edit would reach — (b) and (c) stop being \
equivalent and the choice between them has to be re-argued"
);
}
}
#[test]
fn the_repick_sacrifices_an_exactly_representable_ground() {
let mut sacrificed = Vec::new();
for (id, _, anchor, mover) in collapsed_grounds() {
let exact = delta_e(XTERM_256[nearest_xterm256(mover) as usize], mover) == 0.0;
let (q_mover, _) = quantize_pair_256(mover, anchor);
if exact && q_mover != nearest_xterm256(mover) {
sacrificed.push(id);
}
}
assert_eq!(
sacrificed,
["abstract-light", "one-light"],
"the set of themes whose exactly-representable ground gets moved \
changed. EMPTY: the ground-aware separator landed — good, delete \
this test. OTHERWISE: a theme joined or left the case, and the \
rule 'nudge whichever member is already inexact' needs re-checking \
against it."
);
}
#[test]
fn the_reported_grey_is_the_selection_token_quantised() {
let t = abstracttui::theme::get("abstract-dark").expect("house theme");
let sel = t.tokens.selection_bg;
assert_eq!((sel.r, sel.g, sel.b), (88, 39, 61));
let idx = nearest_xterm256(sel);
assert_eq!(idx, 236, "xterm-256 index");
let q = XTERM_256[idx as usize];
assert_eq!(
(q.r, q.g, q.b),
(48, 48, 48),
"index 236 is the grey ramp at 8 + 10*4 — the reported colour"
);
}
fn assign_ground_indices(t: &TokenSet) -> [(&'static str, u8); 5] {
let g = grounds(t);
let natural: Vec<u8> = g.iter().map(|(_, c)| nearest_xterm256(*c)).collect();
let mut order: Vec<usize> = (0..g.len()).collect();
order.sort_by(|&a, &b| {
let e = |k: usize| delta_e(XTERM_256[natural[k] as usize], g[k].1);
e(a).total_cmp(&e(b)).then(a.cmp(&b))
});
let mut out: [(&'static str, u8); 5] = std::array::from_fn(|i| (g[i].0, 0));
let mut taken: Vec<(u8, usize)> = Vec::new(); for &k in &order {
if let Some(&(_, blocker)) = taken.iter().find(|(i, _)| *i == natural[k]) {
let anchor = relative_luminance(g[blocker].1);
let lighter = relative_luminance(g[k].1) >= anchor;
let mut best: Option<(u8, u32)> = None;
for idx in 16u16..=255 {
let idx = idx as u8;
if taken.iter().any(|(i, _)| *i == idx) {
continue;
}
if order
.iter()
.skip_while(|&&o| o != k)
.skip(1)
.any(|&o| natural[o] == idx)
{
continue;
}
let e = XTERM_256[idx as usize];
if (relative_luminance(e)
>= relative_luminance(XTERM_256[natural[blocker] as usize]))
!= lighter
{
continue;
}
let d = |x: u8, y: u8| {
let d = x as i32 - y as i32;
(d * d) as u32
};
let dist = d(e.r, g[k].1.r) + d(e.g, g[k].1.g) + d(e.b, g[k].1.b);
if best.is_none_or(|(_, bd)| dist < bd) {
best = Some((idx, dist));
}
}
let chosen = best
.expect("240 entries cannot all be taken by 5 grounds")
.0;
out[k].1 = chosen;
taken.push((chosen, k));
} else {
out[k].1 = natural[k];
taken.push((natural[k], k));
}
}
out
}
#[test]
fn a_distinct_index_per_ground_is_constructible_for_every_theme() {
for th in themes() {
let assigned = assign_ground_indices(&th.tokens);
let mut idx: Vec<u8> = assigned.iter().map(|(_, i)| *i).collect();
let before: Vec<u8> = grounds(&th.tokens)
.iter()
.map(|(_, c)| nearest_xterm256(*c))
.collect();
idx.sort_unstable();
idx.dedup();
assert_eq!(
idx.len(),
5,
"{}: could not give five grounds five distinct entries — the \
per-theme fix is not sufficient for this theme and the \
compositor route is back on the table",
th.id
);
let collided = before
.iter()
.collect::<std::collections::HashSet<_>>()
.len()
< 5;
let moved: Vec<&str> = assigned
.iter()
.zip(&before)
.filter(|((_, now), was)| now != *was)
.map(|((n, _), _)| *n)
.collect();
if collided {
assert_eq!(
moved.len(),
1,
"{}: one collision should cost exactly one move, got {:?}",
th.id,
moved
);
} else {
assert!(
moved.is_empty(),
"{}: nothing collided here, but the assignment moved {:?} \
— a fix that perturbs healthy themes is a regression",
th.id,
moved
);
}
}
}
const SET_QUANTISER_DIFFERS_FROM_PROTOTYPE_FOR: &[&str] = &["tokyo-night"];
#[test]
fn the_shipped_set_quantiser_matches_the_reference_prototype() {
let differ: Vec<&str> = themes()
.iter()
.filter(|th| {
let shipped = quantize_set_256(grounds(&th.tokens).map(|(_, c)| c));
shipped != assign_ground_indices(&th.tokens).map(|(_, i)| i)
})
.map(|th| th.id)
.collect();
assert_eq!(
differ, SET_QUANTISER_DIFFERS_FROM_PROTOTYPE_FOR,
"the set of themes where the shipped set quantiser and the \
reference prototype disagree changed.\n\
GREW: a theme landed where sq_dist/integer-luma and \
CIE76/relative-luminance part company. Measure what each choice \
COSTS the moved ground (see the table above) before picking a \
side — do not assume the shipped one is right because it ships.\n\
SHRANK: the two metrics converged; say so on \
claim:tui-audit-does-not-survive-quantisation."
);
}
#[test]
fn the_shipped_set_quantiser_separates_every_theme_without_perturbing_the_healthy() {
for th in themes() {
let g = grounds(&th.tokens);
let before: Vec<u8> = g.iter().map(|(_, c)| nearest_xterm256(*c)).collect();
let after = quantize_set_256(g.map(|(_, c)| c));
let mut distinct = after.to_vec();
distinct.sort_unstable();
distinct.dedup();
assert_eq!(
distinct.len(),
5,
"{}: shipped assignment left two grounds on one entry",
th.id
);
let moved: Vec<&str> = g
.iter()
.zip(before.iter().zip(after.iter()))
.filter(|(_, (was, now))| was != now)
.map(|((n, _), _)| *n)
.collect();
let collided = before
.iter()
.collect::<std::collections::HashSet<_>>()
.len()
< 5;
assert_eq!(
moved.len(),
usize::from(collided),
"{}: expected {} moved ground(s), got {:?}",
th.id,
usize::from(collided),
moved
);
}
}
const IDEAL_SEED_EDIT_IS_UNREACHABLE_FOR: &[(&str, &str)] = &[
("observer-night", "bg vs surface"),
("catppuccin-macchiato", "surface vs shadow_ground"),
("rose-pine", "bg vs surface"),
("everforest-light", "surface_raised vs shadow_ground"),
("abstract-midnight", "bg vs shadow_ground"),
];
#[test]
fn the_shipped_assignment_lands_where_the_ideal_seed_edit_lands() {
let mut unreachable = Vec::new();
for (id, pair, anchor, mover) in collapsed_grounds() {
let t = abstracttui::theme::get(id).expect("known-set theme");
let g = grounds(&t.tokens);
let after = quantize_set_256(g.map(|(_, c)| c));
let index_of = |c: Rgba| {
g.iter()
.position(|(_, x)| *x == c)
.map(|k| after[k])
.expect("collapsed pair members are grounds")
};
let (q_anchor, q_mover) = (index_of(anchor), index_of(mover));
assert_ne!(q_anchor, q_mover, "{id} ({pair}): still collapsed");
let (moved_now, stayed, landed) = if q_mover != nearest_xterm256(mover) {
(mover, anchor, q_mover)
} else {
(anchor, mover, q_anchor)
};
let (ideal, _) = nearest_separating(moved_now, stayed)
.unwrap_or_else(|| panic!("{id} ({pair}): no separating edit for the moved ground"));
let ideal = nearest_xterm256(ideal);
if landed == ideal {
continue;
}
let taken_by_a_third_ground = g
.iter()
.any(|(_, c)| *c != moved_now && *c != stayed && nearest_xterm256(*c) == ideal);
assert!(
taken_by_a_third_ground,
"{id} ({pair}): the set assignment landed on {landed} where the \
optimal seed edit reaches {ideal}, and no other ground holds \
{ideal}. Options (b) and (c) stop being equivalent for this \
theme and the choice between them has to be re-argued on \
claim:tui-audit-does-not-survive-quantisation."
);
unreachable.push((id, pair));
}
assert_eq!(
unreachable, IDEAL_SEED_EDIT_IS_UNREACHABLE_FOR,
"the set of themes whose pairwise-ideal entry is occupied by a \
third ground changed. Each entry here is a theme where option \
(b)'s costing was pairwise-blind and understated the edit it \
would really need, so the list is evidence about (b), not a \
waiver for (c)."
);
}
#[test]
fn the_assignment_never_sacrifices_an_exactly_representable_ground() {
for th in themes() {
let g = grounds(&th.tokens);
for (k, (name, idx)) in assign_ground_indices(&th.tokens).iter().enumerate() {
let own = nearest_xterm256(g[k].1);
let exact = delta_e(XTERM_256[own as usize], g[k].1) == 0.0;
assert!(
!exact || *idx == own,
"{}: {name} is represented exactly by the palette and the \
assignment moved it anyway — the ownership rule is not \
being applied",
th.id
);
}
}
}
fn assignment_for(t: &TokenSet) -> Vec<(Rgba, u8)> {
let g = t.grounds();
let idx = quantize_set_256(g.map(|(_, c)| c));
g.iter().map(|(_, c)| *c).zip(idx).collect()
}
fn painted_grounds(a: Rgba, b: Rgba, assignment: &[(Rgba, u8)]) -> (Option<Rgba>, Option<Rgba>) {
let caps = PresentCaps {
color: ColorDepth::Xterm256,
..PresentCaps::FULL
};
let size = Size::new(4, 1);
let mut surface = Surface::new(size, Cell::EMPTY);
let ink = Rgba::rgb(200, 200, 200);
for (x, ground) in [(0, a), (1, b)] {
surface.draw_text(
x,
0,
"x",
Style {
fg: Some(ink),
bg: Some(ground),
..Style::EMPTY
},
);
}
let mut presenter = Presenter::new();
presenter.set_palette_assignment(assignment);
let mut out = Vec::new();
presenter.emit(
FrameDiff::new().compute_full(&Surface::new(size, Cell::EMPTY), &surface),
&surface,
&caps,
&mut out,
);
let mut screen = VtScreen::new(size);
screen.feed(&out);
assert_eq!(screen.unknown_seq_count(), 0, "unmodeled bytes");
let paint = |x: i32| screen.cell(x, 0).expect("in bounds").paint.bg;
(paint(0), paint(1))
}
#[test]
fn the_default_themes_panel_survives_256_colours_only_with_the_assignment() {
let t = abstracttui::theme::get("abstract-dark")
.expect("house default")
.tokens;
let (bg_none, surface_none) = painted_grounds(t.bg, t.surface, &[]);
assert_eq!(
bg_none, surface_none,
"premise: with no assignment the default theme's panel and ground \
reach the terminal as one colour. If this now differs, the \
collapse was fixed somewhere else and KNOWN_256_COLLAPSES should \
have caught it first."
);
let assignment = assignment_for(&t);
let (bg_on, surface_on) = painted_grounds(t.bg, t.surface, &assignment);
assert_ne!(
bg_on, surface_on,
"the assignment is installed and the panel STILL renders as its \
own ground — resolve_pen is not consulting it"
);
assert_eq!(bg_on, bg_none, "an unmoved ground must not shift");
}
#[test]
fn an_unassigned_colour_is_unaffected_by_the_assignment() {
let t = abstracttui::theme::get("abstract-dark")
.expect("house default")
.tokens;
let stranger = Rgba::rgb(180, 20, 90);
assert!(
!t.grounds().iter().any(|(_, c)| *c == stranger),
"premise: not a ground"
);
let (plain, _) = painted_grounds(stranger, t.bg, &[]);
let (mapped, _) = painted_grounds(stranger, t.bg, &assignment_for(&t));
assert_eq!(plain, mapped);
assert_eq!(plain, Some(xterm_256(nearest_xterm256(stranger))));
}
#[test]
fn an_assignment_never_erases_text_it_collides_with() {
let ground = Rgba::rgb(60, 60, 60);
let ink = XTERM_256[238];
assert_eq!(nearest_xterm256(ink), 238, "premise: ink lands on 238");
let assignment = [(ground, 238u8)];
let caps = PresentCaps {
color: ColorDepth::Xterm256,
..PresentCaps::FULL
};
let size = Size::new(2, 1);
let mut surface = Surface::new(size, Cell::EMPTY);
surface.draw_text(
0,
0,
"x",
Style {
fg: Some(ink),
bg: Some(ground),
..Style::EMPTY
},
);
let mut presenter = Presenter::new();
presenter.set_palette_assignment(&assignment);
let mut out = Vec::new();
presenter.emit(
FrameDiff::new().compute_full(&Surface::new(size, Cell::EMPTY), &surface),
&surface,
&caps,
&mut out,
);
let mut screen = VtScreen::new(size);
screen.feed(&out);
let paint = screen.cell(0, 0).expect("in bounds").paint;
assert_eq!(
paint.bg,
Some(xterm_256(238)),
"the ground keeps its assigned entry — the assignment is a \
preference the pair rule must not silently override"
);
assert_ne!(
paint.fg, paint.bg,
"the assignment collided with the ink and the text was emitted \
invisible: the pair guarantee must outrank the ground preference"
);
}
fn ground_inversions() -> Vec<(String, String, String, f32, f32)> {
use abstracttui::base::palette::XTERM_256;
use abstracttui::render::color::quantize_set_256;
use abstracttui::theme::contrast::contrast_ratio;
let mut out = vec![];
for t in abstracttui::theme::themes() {
let g = t.tokens.grounds();
let a = quantize_set_256([g[0].1, g[1].1, g[2].1, g[3].1, g[4].1]);
for i in 0..5 {
for j in (i + 1)..5 {
let true_c = contrast_ratio(g[i].1, g[j].1);
let q_c = contrast_ratio(XTERM_256[a[i] as usize], XTERM_256[a[j] as usize]);
if true_c < 1.05 && q_c > true_c * 1.05 {
out.push((
t.id.to_string(),
g[i].0.name().to_string(),
g[j].0.name().to_string(),
true_c,
q_c,
));
}
}
}
}
out
}
#[test]
fn the_separator_invents_edges_in_exactly_seven_known_pairs() {
let found = ground_inversions();
let names: Vec<String> = found
.iter()
.map(|f| format!("{} {}/{}", f.0, f.1, f.2))
.collect();
assert_eq!(
found.len(),
7,
"the invented-edge set CHANGED. Fewer means the fix landed — update \
this to `assert!(found.is_empty())` and close the row. More means a \
new theme or a separator change widened a KNOWN defect. Found: {names:?}"
);
let worst = found
.iter()
.find(|f| f.0 == "solarized-dark")
.expect("solarized-dark surface_raised/selection_bg is the headline case");
assert!(
worst.3 < 1.05 && worst.4 > 1.4,
"solarized-dark: authored at {:.3} (one colour to any eye), rendered \
at 256 as {:.3} (a visible edge)",
worst.3,
worst.4
);
}
#[test]
fn no_single_colour_distance_threshold_can_separate_the_two_populations() {
use abstracttui::base::Rgba;
use abstracttui::theme::contrast::contrast_ratio;
fn sq(a: Rgba, b: Rgba) -> i32 {
let (dr, dg, db) = (
a.r as i32 - b.r as i32,
a.g as i32 - b.g as i32,
a.b as i32 - b.b as i32,
);
dr * dr + dg * dg + db * db
}
let (mut merge_max, mut keep_min) = (0i32, i32::MAX);
for t in abstracttui::theme::themes() {
let g = t.tokens.grounds();
for i in 0..g.len() {
for j in (i + 1)..g.len() {
let (c, d) = (contrast_ratio(g[i].1, g[j].1), sq(g[i].1, g[j].1));
if c < 1.05 {
merge_max = merge_max.max(d);
} else if c >= 1.15 {
keep_min = keep_min.min(d);
}
}
}
}
assert!(
merge_max > keep_min,
"the populations SEPARATED (merge_max={merge_max} <= keep_min={keep_min}). \
A single sq_dist threshold is now viable and this test is obsolete — \
re-open the row and take the simple fix."
);
}
#[test]
fn a_theme_can_pass_the_audit_with_two_grounds_a_reader_cannot_tell_apart() {
use abstracttui::theme::contrast::{audit, floors, ground_overlaps};
let worst = abstracttui::theme::themes()
.iter()
.filter_map(|t| {
ground_overlaps(t.id, &t.tokens, floors::GROUND_SEPARATION_REPORT)
.into_iter()
.min_by(|x, y| x.measured.partial_cmp(&y.measured).unwrap())
.map(|o| (t, o))
})
.min_by(|a, b| a.1.measured.partial_cmp(&b.1.measured).unwrap())
.expect("the registry has at least one overlapping ground pair");
let (theme, overlap) = worst;
assert!(
overlap.measured < 1.01,
"expected a pair that is effectively one colour, got {overlap}"
);
assert!(
audit(theme.id, &theme.tokens).is_empty(),
"{}: premise broken — this theme now HAS audit violations, so the \
clean-audit-plus-invisible-grounds case needs a different witness. \
Overlap was: {overlap}",
theme.id
);
}
#[test]
fn the_registry_ground_overlap_report_is_the_measured_size() {
use abstracttui::theme::contrast::{floors, ground_overlaps};
let all: Vec<_> = abstracttui::theme::themes()
.iter()
.flat_map(|t| ground_overlaps(t.id, &t.tokens, floors::GROUND_SEPARATION_REPORT))
.collect();
let themes_hit: std::collections::BTreeSet<&str> =
all.iter().map(|o| o.theme.as_str()).collect();
assert_eq!(
(all.len(), themes_hit.len()),
(44, 22),
"the authored ground-overlap set changed: {} pairs across {} of 26 themes",
all.len(),
themes_hit.len()
);
assert!(
ground_overlaps("none", &abstracttui::theme::themes()[0].tokens, 1.0).is_empty(),
"a floor of 1.0 must report nothing — contrast is never below 1.0, \
so a hit here means the comparison is inverted"
);
}
fn assign_grounds(tokens: &TokenSet, intent: GroundIntent) -> [u8; 5] {
let colors: Vec<Rgba> = tokens.grounds().iter().map(|(_, c)| *c).collect();
let mut out = [0u8; 5];
quantize_set_256_into_with(&colors, intent, &mut out);
out
}
fn every_pair(intent: PairIntent) -> Vec<(usize, usize, PairIntent)> {
let mut out = Vec::new();
for i in 0..5 {
for j in (i + 1)..5 {
out.push((i, j, intent));
}
}
out
}
const SHIPPED_GROUND_INDICES: &[(&str, [u8; 5])] = &[
("abstract-dark", [234, 235, 23, 236, 233]),
("abstract-light", [255, 231, 254, 218, 251]),
("observer-night", [233, 235, 234, 237, 232]),
("catppuccin-mocha", [235, 234, 236, 239, 233]),
("catppuccin-macchiato", [235, 236, 237, 59, 234]),
("catppuccin-frappe", [237, 236, 238, 60, 235]),
("rose-pine", [234, 236, 235, 238, 233]),
("rose-pine-moon", [235, 236, 237, 59, 233]),
("tokyo-night", [234, 235, 239, 238, 233]),
("nord", [236, 238, 239, 240, 234]),
("one-dark", [236, 235, 237, 238, 234]),
("dracula", [235, 237, 238, 59, 234]),
("monokai", [235, 236, 237, 240, 234]),
("gruvbox", [235, 237, 239, 58, 234]),
("solarized-dark", [235, 236, 23, 237, 233]),
("everforest-dark", [236, 237, 238, 239, 234]),
("catppuccin-latte", [255, 254, 188, 183, 252]),
("rose-pine-dawn", [255, 231, 254, 188, 252]),
("one-light", [255, 231, 254, 153, 252]),
("everforest-light", [230, 231, 253, 254, 188]),
("solarized-light", [230, 254, 188, 152, 251]),
("abstract-aurora", [233, 234, 235, 23, 232]),
("abstract-paper", [255, 231, 254, 181, 251]),
("abstract-ember", [233, 234, 235, 238, 232]),
("abstract-midnight", [235, 233, 234, 238, 232]),
("abstract-dawn", [255, 231, 254, 152, 251]),
];
#[test]
fn the_undeclared_default_is_byte_for_byte_the_shipped_assignment() {
let ids: Vec<&str> = themes().iter().map(|t| t.id).collect();
let pinned: Vec<&str> = SHIPPED_GROUND_INDICES.iter().map(|(id, _)| *id).collect();
assert_eq!(
ids, pinned,
"the registry changed shape — the baseline covers every theme or \
it covers nothing"
);
for (t, (id, want)) in themes().iter().zip(SHIPPED_GROUND_INDICES) {
let g = t.tokens.grounds();
assert_eq!(t.id, *id);
assert_eq!(
quantize_set_256([g[0].1, g[1].1, g[2].1, g[3].1, g[4].1]),
*want,
"{id}: the plain assignment moved off the bytes that shipped"
);
assert_eq!(
assign_grounds(&t.tokens, GroundIntent::UNDECLARED),
*want,
"{id}: silence must be the behaviour that shipped"
);
}
}
#[test]
fn opting_in_without_naming_a_pair_changes_nothing_in_any_theme() {
let all_distinct = every_pair(PairIntent::Distinct);
for t in themes() {
let silent = assign_grounds(&t.tokens, GroundIntent::UNDECLARED);
assert_eq!(
assign_grounds(&t.tokens, GroundIntent::new(&[])),
silent,
"{}: an empty declaration moved a ground — 'opted in' must not \
mean 'merge whatever I did not mention'",
t.id
);
assert_eq!(
assign_grounds(&t.tokens, GroundIntent::new(&all_distinct)),
silent,
"{}: declaring every pair Distinct moved a ground — declaring \
the default is not supposed to be a change",
t.id
);
}
}
#[test]
fn a_declaration_closes_three_of_the_seven_invented_edges_and_the_palette_owns_the_rest() {
use abstracttui::theme::contrast::contrast_ratio;
let inverted = |intent: GroundIntent| -> Vec<String> {
let mut out = vec![];
for t in themes() {
let g = t.tokens.grounds();
let a = assign_grounds(&t.tokens, intent);
for i in 0..5 {
for j in (i + 1)..5 {
let true_c = contrast_ratio(g[i].1, g[j].1);
let q_c = contrast_ratio(XTERM_256[a[i] as usize], XTERM_256[a[j] as usize]);
if true_c < 1.05 && q_c > true_c * 1.05 {
out.push(format!("{} {}/{}", t.id, g[i].0.name(), g[j].0.name()));
}
}
}
}
out
};
let all_same = every_pair(PairIntent::Same);
let silent = inverted(GroundIntent::UNDECLARED);
let declared = inverted(GroundIntent::new(&all_same));
assert_eq!(
silent.len(),
7,
"premise, and it agrees with the row: {silent:?}"
);
assert_eq!(
declared,
vec![
"gruvbox surface_raised/selection_bg",
"rose-pine-dawn selection_bg/shadow_ground",
"abstract-paper selection_bg/shadow_ground",
"abstract-dawn selection_bg/shadow_ground",
],
"a declaration closes the three collision-borne edges and only those"
);
for name in &declared {
let (id, pair) = name.split_once(' ').expect("id and pair");
let (an, bn) = pair.split_once('/').expect("two ground names");
let t = themes().iter().find(|t| t.id == id).expect("named theme");
let g = t.tokens.grounds();
let at = g
.iter()
.position(|(k, _)| k.name() == an)
.expect("ground a");
let bt = g
.iter()
.position(|(k, _)| k.name() == bn)
.expect("ground b");
let (na, nb) = (nearest_xterm256(g[at].1), nearest_xterm256(g[bt].1));
assert_ne!(
na, nb,
"{name}: no collision to merge — the edge is plain nearest's"
);
for intent in [GroundIntent::UNDECLARED, GroundIntent::new(&all_same)] {
let a = assign_grounds(&t.tokens, intent);
assert_eq!(
(a[at], a[bt]),
(na, nb),
"{name}: the set policy never moved either ground"
);
}
}
}
#[test]
fn declaring_every_pair_same_re_collapses_fifteen_ground_pairs() {
use abstracttui::theme::contrast::contrast_ratio;
let all_same = every_pair(PairIntent::Same);
let mut lost = vec![];
for t in themes() {
let g = t.tokens.grounds();
let (u, d) = (
assign_grounds(&t.tokens, GroundIntent::UNDECLARED),
assign_grounds(&t.tokens, GroundIntent::new(&all_same)),
);
for i in 0..5 {
for j in (i + 1)..5 {
if u[i] != u[j] && d[i] == d[j] {
lost.push((
format!("{} {}/{}", t.id, g[i].0.name(), g[j].0.name()),
contrast_ratio(g[i].1, g[j].1),
));
}
}
}
}
assert_eq!(
lost.len(),
15,
"the cost of the maximal opt-in changed: {lost:?}"
);
let loud: Vec<&str> = lost
.iter()
.filter(|(_, c)| *c >= 1.10)
.map(|(n, _)| n.as_str())
.collect();
assert_eq!(
loud,
vec!["catppuccin-frappe bg/surface"],
"the pairs given up above the report floor"
);
}
#[test]
fn every_built_in_theme_is_silent_about_ground_intent() {
let speaking: Vec<&str> = themes()
.iter()
.filter(|t| !t.ground_intent.is_empty())
.map(|t| t.id)
.collect();
assert!(
speaking.is_empty(),
"built-in themes declared ground intent: {speaking:?}. The ruling \
makes silence the default for every theme this crate ships — if a \
theme should now speak, that is a DESIGN decision and the shipped \
byte baseline moves with it."
);
}
#[test]
fn resolving_intent_maps_grounds_and_refuses_a_non_ground() {
use abstracttui::theme::{TokenId, TokenSet};
assert_eq!(
TokenSet::resolve_ground_intent(&[(
TokenId::SelectionBg,
TokenId::SurfaceRaised,
PairIntent::Same,
)]),
Ok(vec![(3, 2, PairIntent::Same)]),
"positions must be those of TokenSet::grounds, in its order"
);
assert_eq!(
TokenSet::resolve_ground_intent(&[(TokenId::Bg, TokenId::Border, PairIntent::Same)]),
Err(TokenId::Border)
);
assert_eq!(TokenSet::resolve_ground_intent(&[]), Ok(vec![]));
}
#[test]
fn register_refuses_a_declaration_over_a_non_ground_in_both_modes() {
use abstracttui::theme::{
default_theme, register, RegisterError, RegisterMode, ThemeCandidate, TokenId,
};
for (n, mode) in [RegisterMode::Strict, RegisterMode::Labeled]
.into_iter()
.enumerate()
{
let base = default_theme();
let candidate = ThemeCandidate {
id: format!("gi-non-ground-{n}"),
label: "Non-ground declaration".into(),
dark: base.dark,
tokens: base.tokens,
ground_intent: vec![(TokenId::Bg, TokenId::Text, PairIntent::Same)],
};
match register(candidate, mode) {
Err(RegisterError::NotAGround(id)) => assert_eq!(id, TokenId::Text),
other => panic!("{mode:?} accepted a declaration over `text`: {other:?}"),
}
}
let base = default_theme();
let ok = register(
ThemeCandidate {
id: "gi-non-ground-ok".into(),
label: "Clean declaration".into(),
dark: base.dark,
tokens: base.tokens,
ground_intent: vec![(TokenId::Bg, TokenId::Surface, PairIntent::Same)],
},
RegisterMode::Strict,
)
.expect("a declaration naming two real grounds is fine");
assert_eq!(ok.theme.ground_intent.len(), 1);
}
#[test]
fn the_driver_carries_a_themes_declaration_all_the_way_to_the_terminal() {
use abstracttui::app::{App, Driver, RunConfig};
use abstracttui::base::Rect;
use abstracttui::layout::Style as LayoutStyle;
use abstracttui::term::Capabilities;
use abstracttui::testing::CaptureTerm;
use abstracttui::theme::{register, RegisterMode, ThemeCandidate, TokenId};
use abstracttui::ui::Element;
let base = abstracttui::theme::get("solarized-dark").expect("house port");
let (raised, sel) = (base.tokens.surface_raised, base.tokens.selection_bg);
assert_eq!(
nearest_xterm256(raised),
nearest_xterm256(sel),
"premise: these two collide, so there is a merge available to release"
);
let mk = |id: &str, intent: Vec<(TokenId, TokenId, PairIntent)>| {
register(
ThemeCandidate {
id: id.into(),
label: id.into(),
dark: base.dark,
tokens: base.tokens,
ground_intent: intent,
},
RegisterMode::Labeled,
)
.expect("registers")
.theme
};
fn on_screen(theme: &'static abstracttui::theme::Theme) -> (Option<Rgba>, Option<Rgba>) {
let size = Size::new(4, 1);
let mut term = CaptureTerm::new(size);
let mut app = App::new(size);
let (a, b) = (theme.tokens.surface_raised, theme.tokens.selection_bg);
app.mount(move |_| {
Element::new()
.style(LayoutStyle::fill())
.draw(move |canvas, rect| {
canvas.fill(Rect::new(rect.x, rect.y, 1, 1), ' ', a, a);
canvas.fill(Rect::new(rect.x + 1, rect.y, 1, 1), ' ', b, b);
})
.build()
})
.expect("mount");
abstracttui::app::set_theme(theme);
let mut driver = Driver::new(
&mut app,
&mut term,
RunConfig {
caps: Some(Capabilities::with(|c| {
c.colors_256 = true;
c.unicode_ok = true;
})),
enter: None,
probe: false,
..RunConfig::default()
},
)
.expect("enter");
for _ in 0..4 {
if driver.turn(&mut app, &mut term).expect("turn").idle {
break;
}
}
let screen = term.screen();
assert_eq!(screen.unknown_seq_count(), 0, "unmodeled bytes");
(
screen.cell(0, 0).expect("in bounds").paint.bg,
screen.cell(1, 0).expect("in bounds").paint.bg,
)
}
let (a, b) = on_screen(mk("gi-wire-silent", vec![]));
assert_ne!(
a, b,
"control: an undeclared theme still separates them through the \
driver — if this stops being true the declared case proves nothing"
);
let (a, b) = on_screen(mk(
"gi-wire-declared",
vec![(
TokenId::SurfaceRaised,
TokenId::SelectionBg,
PairIntent::Same,
)],
));
assert_eq!(
a, b,
"the theme declared these two grounds Same and the terminal still \
shows an edge — Driver::sync_palette_assignment is not consulting \
Theme::ground_intent"
);
assert_eq!(
a,
Some(xterm_256(nearest_xterm256(raised))),
"and they share the entry the palette gave them, not a third one"
);
}
#[test]
fn declaring_distinct_over_one_colour_is_reported_as_a_contradiction() {
use abstracttui::theme::contrast::declaration_contradictions;
use abstracttui::theme::{default_theme, TokenId};
let mut t = default_theme().tokens;
t.surface = t.bg;
let found = declaration_contradictions(
"probe",
&t,
&[(TokenId::Bg, TokenId::Surface, PairIntent::Distinct)],
);
assert_eq!(found.len(), 1, "{found:?}");
assert!(
found[0].contains("bg") && found[0].contains("surface") && found[0].contains("Distinct"),
"the finding must name the pair and what was declared: {}",
found[0]
);
assert!(
declaration_contradictions(
"probe",
&t,
&[(TokenId::Bg, TokenId::Surface, PairIntent::Same)]
)
.is_empty(),
"declaring one colour Same is agreement with the artifact, not a contradiction"
);
assert!(
declaration_contradictions(
"probe",
&default_theme().tokens,
&[(TokenId::Bg, TokenId::Surface, PairIntent::Distinct)]
)
.is_empty(),
"two genuinely different grounds declared Distinct is the normal case"
);
assert!(
declaration_contradictions(
"probe",
&default_theme().tokens,
&[(TokenId::Bg, TokenId::SelectionBg, PairIntent::Same)]
)
.is_empty(),
"Same over two far-apart colours is INERT, not wrong — it asks for a \
merge that can never happen, and an author may mean it against a \
future re-tint"
);
}
#[test]
fn register_treats_a_self_contradicting_declaration_as_hygiene_not_a_hard_error() {
use abstracttui::theme::{
default_theme, register, RegisterError, RegisterMode, ThemeCandidate, TokenId,
};
let mut tokens = default_theme().tokens;
tokens.surface = tokens.bg;
let candidate = |id: &str| ThemeCandidate {
id: id.into(),
label: "Contradiction".into(),
dark: default_theme().dark,
tokens,
ground_intent: vec![(TokenId::Bg, TokenId::Surface, PairIntent::Distinct)],
};
match register(candidate("gi-contradiction-strict"), RegisterMode::Strict) {
Err(RegisterError::Rejected { hygiene, .. }) => assert!(
hygiene.iter().any(|h| h.contains("declared Distinct")),
"strict refused, but not for the contradiction: {hygiene:?}"
),
other => panic!("strict accepted a self-contradicting declaration: {other:?}"),
}
let reg = register(candidate("gi-contradiction-labeled"), RegisterMode::Labeled)
.expect("labeled registers rather than stranding the author");
assert!(
reg.warnings.iter().any(|w| w.contains("declared Distinct")),
"labeled registered but swallowed the contradiction: {:?}",
reg.warnings
);
}
#[test]
fn three_ground_pairs_are_an_inversion_and_a_rescue_at_once() {
use abstracttui::theme::contrast::contrast_ratio;
let mut both = Vec::new();
let mut collision_borne = Vec::new();
for t in themes() {
let g = t.tokens.grounds();
let assigned = quantize_set_256([g[0].1, g[1].1, g[2].1, g[3].1, g[4].1]);
for i in 0..5 {
for j in (i + 1)..5 {
let (na, nb) = (nearest_xterm256(g[i].1), nearest_xterm256(g[j].1));
let collides = na == nb;
let true_c = contrast_ratio(g[i].1, g[j].1);
let q_c = contrast_ratio(
XTERM_256[assigned[i] as usize],
XTERM_256[assigned[j] as usize],
);
let inversion = true_c < 1.05 && q_c > true_c * 1.05;
let rescue = collides && assigned[i] != assigned[j];
let name = format!("{} {}/{}", t.id, g[i].0.name(), g[j].0.name());
if inversion && rescue {
both.push(name.clone());
}
if inversion && collides {
collision_borne.push(name);
}
}
}
}
assert_eq!(
both,
vec![
"solarized-dark surface_raised/selection_bg",
"one-light bg/surface",
"abstract-midnight bg/shadow_ground",
],
"the set of pairs that are simultaneously an invented edge and a \
rescued elevation changed. This is the evidence that no predicate \
f(colour_a, colour_b) can be correct — it would have to merge and \
separate the same two colours — and it is what makes intent a \
DECLARATION rather than a measurement. EMPTY means the argument \
for the ruling no longer has a witness in this registry; say so on \
claim:tui-separator-invents-distinctions-the-author-did-not-draw \
before changing anything."
);
assert_eq!(
both, collision_borne,
"being in both populations must be EXACTLY the collision-borne \
inversions: a rescue is only possible where plain nearest \
collapsed, so an inversion that is in both without colliding \
would mean the rescue test is measuring something else"
);
}
fn plain_nearest_inversions() -> Vec<(String, f32, f32, u8, u8)> {
use abstracttui::theme::contrast::contrast_ratio;
let mut out = Vec::new();
for t in themes() {
let g = t.tokens.grounds();
for i in 0..5 {
for j in (i + 1)..5 {
let true_c = contrast_ratio(g[i].1, g[j].1);
let (na, nb) = (nearest_xterm256(g[i].1), nearest_xterm256(g[j].1));
let q_c = contrast_ratio(XTERM_256[na as usize], XTERM_256[nb as usize]);
if true_c < 1.05 && q_c > true_c * 1.05 {
out.push((
format!("{} {}/{}", t.id, g[i].0.name(), g[j].0.name()),
true_c,
q_c,
na,
nb,
));
}
}
}
}
out
}
#[test]
fn four_ground_pairs_are_inverted_by_the_palette_lookup_alone() {
let found = plain_nearest_inversions();
let names: Vec<&str> = found.iter().map(|f| f.0.as_str()).collect();
assert_eq!(
names,
vec![
"gruvbox surface_raised/selection_bg",
"rose-pine-dawn selection_bg/shadow_ground",
"abstract-paper selection_bg/shadow_ground",
"abstract-dawn selection_bg/shadow_ground",
],
"the set of pairs plain nearest inverts changed. These are the ones \
no ground declaration can reach — see \
claim:tui-nearest-inverts-across-the-cube-ramp-boundary."
);
let worst = found.iter().map(|f| f.2 / f.1).fold(0.0f32, f32::max);
assert!(
(1.20..1.30).contains(&worst),
"worst amplification moved to {worst:.3} — it was 1.231 (gruvbox)"
);
}
#[test]
fn every_plain_nearest_inversion_straddles_the_cube_ramp_boundary() {
use abstracttui::theme::contrast::contrast_ratio;
fn on_ramp(entry: u8) -> bool {
entry >= 232
}
for (name, _, _, na, nb) in plain_nearest_inversions() {
assert_ne!(
on_ramp(na),
on_ramp(nb),
"{name}: an inversion that does NOT straddle the cube/ramp \
boundary ({na}/{nb}). The mechanism this row is built on is \
that near-identical colours quantised against DIFFERENT \
lattices come out disagreeing — a same-lattice inversion \
would mean there is a second, unexamined cause."
);
}
let mut same_lattice_inversions = Vec::new();
let mut splits = 0;
for t in themes() {
let g = t.tokens.grounds();
for i in 0..5 {
for j in (i + 1)..5 {
let true_c = contrast_ratio(g[i].1, g[j].1);
if true_c >= 1.05 {
continue;
}
let (na, nb) = (nearest_xterm256(g[i].1), nearest_xterm256(g[j].1));
let q_c = contrast_ratio(XTERM_256[na as usize], XTERM_256[nb as usize]);
if on_ramp(na) != on_ramp(nb) {
splits += 1;
} else if q_c > true_c * 1.05 {
same_lattice_inversions.push(format!(
"{} {}/{}",
t.id,
g[i].0.name(),
g[j].0.name()
));
}
}
}
}
assert!(
same_lattice_inversions.is_empty(),
"a near-identical pair inverted while staying on ONE lattice: \
{same_lattice_inversions:?}. The cube/ramp boundary is no longer \
the whole story and the row needs re-measuring."
);
assert_eq!(
splits, 5,
"five near-identical ground pairs straddle the boundary and four of \
them invert. Straddling is NECESSARY but not SUFFICIENT — the fifth \
(one-light selection_bg/shadow_ground) comes out closer, so a fix \
that treats every split as a defect would move a pair that is fine."
);
}
fn ground_render_error(colors: &[Rgba; 5], idx: &[u8; 5]) -> f32 {
use abstracttui::theme::contrast::contrast_ratio;
let mut total = 0.0;
for i in 0..5 {
for j in (i + 1)..5 {
let authored = contrast_ratio(colors[i], colors[j]);
let rendered = contrast_ratio(XTERM_256[idx[i] as usize], XTERM_256[idx[j] as usize]);
total += (rendered / authored).max(authored / rendered) - 1.0;
}
}
total
}
fn nearest_on_lattice(c: Rgba, ramp: bool) -> u8 {
let range = if ramp { 232..256 } else { 16..232 };
range
.map(|i| {
let e = XTERM_256[i];
let d = |x: u8, y: u8| {
let d = x as i32 - y as i32;
d * d
};
(d(c.r, e.r) + d(c.g, e.g) + d(c.b, e.b), i as u8)
})
.min()
.expect("both lattices are non-empty")
.1
}
#[derive(Debug, PartialEq, Eq)]
enum SnapVerdict {
WouldMerge,
DoesNotCure,
FlipsOrdering,
WorsensTheSet,
Viable,
}
fn lattice_snap_candidates() -> Vec<(String, &'static str, u8, u8, SnapVerdict)> {
use abstracttui::theme::contrast::contrast_ratio;
let mut out = Vec::new();
for t in themes() {
let g = t.tokens.grounds();
let colors: [Rgba; 5] = core::array::from_fn(|k| g[k].1);
let natural: [u8; 5] = core::array::from_fn(|k| nearest_xterm256(colors[k]));
for i in 0..5 {
for j in (i + 1)..5 {
let authored = contrast_ratio(colors[i], colors[j]);
if authored >= 1.05 {
continue;
}
let rendered = contrast_ratio(
XTERM_256[natural[i] as usize],
XTERM_256[natural[j] as usize],
);
if rendered <= authored * 1.05 {
continue;
}
let base = ground_render_error(&colors, &natural);
for (m, a) in [(i, j), (j, i)] {
let snapped = nearest_on_lattice(colors[m], natural[a] >= 232);
let mut candidate = natural;
candidate[m] = snapped;
let paired =
contrast_ratio(XTERM_256[snapped as usize], XTERM_256[natural[a] as usize]);
let luma = |c: Rgba| 2126 * c.r as u32 + 7152 * c.g as u32 + 722 * c.b as u32;
let verdict = if snapped == natural[a] {
SnapVerdict::WouldMerge
} else if paired > authored * 1.05 {
SnapVerdict::DoesNotCure
} else if (luma(colors[m]) >= luma(colors[a]))
!= (luma(XTERM_256[snapped as usize])
>= luma(XTERM_256[natural[a] as usize]))
{
SnapVerdict::FlipsOrdering
} else if ground_render_error(&colors, &candidate) >= base {
SnapVerdict::WorsensTheSet
} else {
SnapVerdict::Viable
};
out.push((
format!("{} {}/{}", t.id, g[i].0.name(), g[j].0.name()),
g[m].0.name(),
natural[m],
snapped,
verdict,
));
}
}
}
}
out
}
#[test]
fn no_same_lattice_snap_repairs_a_cube_ramp_inversion() {
let found = lattice_snap_candidates();
let rendered: Vec<String> = found
.iter()
.map(|(pair, mover, from, to, verdict)| format!("{pair}: {mover} {from}->{to} {verdict:?}"))
.collect();
let expected = vec![
"gruvbox surface_raised/selection_bg: surface_raised 239->59 WorsensTheSet",
"gruvbox surface_raised/selection_bg: selection_bg 58->238 DoesNotCure",
"rose-pine-dawn selection_bg/shadow_ground: selection_bg 188->253 DoesNotCure",
"rose-pine-dawn selection_bg/shadow_ground: shadow_ground 252->188 WouldMerge",
"abstract-paper selection_bg/shadow_ground: selection_bg 181->251 WouldMerge",
"abstract-paper selection_bg/shadow_ground: shadow_ground 251->187 DoesNotCure",
"abstract-dawn selection_bg/shadow_ground: selection_bg 152->252 DoesNotCure",
"abstract-dawn selection_bg/shadow_ground: shadow_ground 251->188 FlipsOrdering",
];
assert_eq!(
rendered, expected,
"the same-lattice snap candidates moved. Each line is a candidate \
and the FIRST rule it broke — a new line, a missing line, or a \
changed verdict all mean the refutation in \
claim:tui-nearest-inverts-across-the-cube-ramp-boundary needs \
re-measuring before anything is built on it."
);
let viable: Vec<&String> = rendered
.iter()
.zip(&found)
.filter(|(_, (_, _, _, _, v))| *v == SnapVerdict::Viable)
.map(|(line, _)| line)
.collect();
assert!(
viable.is_empty(),
"a same-lattice snap became viable: {viable:?}. That is the fix \
delegate ruled available at dm:delegate--tui#13 and measurement \
refuted — if one exists now, BUILD IT."
);
}
#[test]
fn the_only_curing_snap_breaks_three_relations_to_fix_one() {
use abstracttui::theme::contrast::contrast_ratio;
let t = themes()
.iter()
.find(|t| t.id == "gruvbox")
.expect("gruvbox ships");
let g = t.tokens.grounds();
let colors: [Rgba; 5] = core::array::from_fn(|k| g[k].1);
let natural: [u8; 5] = core::array::from_fn(|k| nearest_xterm256(colors[k]));
let (raised, selection) = (2, 3);
assert_eq!(
(g[raised].0.name(), g[selection].0.name()),
("surface_raised", "selection_bg"),
"the ground order moved; the indices below name the wrong pair"
);
let mut snapped = natural;
snapped[raised] = nearest_on_lattice(colors[raised], false);
assert_eq!(
(natural[raised], snapped[raised]),
(239, 59),
"the snap no longer moves surface_raised from the ramp to cube 59"
);
let pair = |idx: &[u8; 5], a: usize, b: usize| {
contrast_ratio(XTERM_256[idx[a] as usize], XTERM_256[idx[b] as usize])
};
let authored = contrast_ratio(colors[raised], colors[selection]);
assert!(
(1.23..1.25).contains(&pair(&natural, raised, selection))
&& pair(&snapped, raised, selection) < authored * 1.05,
"the snap no longer cures the pair it was built for: {:.3} -> {:.3} \
against an authored {authored:.3}",
pair(&natural, raised, selection),
pair(&snapped, raised, selection)
);
for (other, was, now) in [(0, 1.819, 2.370), (1, 1.367, 1.781), (4, 2.048, 2.669)] {
let before = pair(&natural, raised.min(other), raised.max(other));
let after = pair(&snapped, raised.min(other), raised.max(other));
assert!(
(before - was).abs() < 0.01 && (after - now).abs() < 0.01,
"surface_raised/{} moved: {before:.3} -> {after:.3}, pinned {was} -> {now}",
g[other].0.name()
);
let authored_other = contrast_ratio(colors[raised], colors[other]);
assert!(
(after / authored_other) > (before / authored_other),
"surface_raised/{} no longer gets worse under the snap — the \
refutation rests on it doing so",
g[other].0.name()
);
}
assert!(
ground_render_error(&colors, &snapped) > ground_render_error(&colors, &natural),
"the snap stopped making gruvbox worse overall ({:.3} -> {:.3}); \
the fix may be back on the table",
ground_render_error(&colors, &natural),
ground_render_error(&colors, &snapped)
);
}