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,
};
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"
);
}