use super::{assign, colour_for, supports_wide_palette, wide_colour, WIDE_PALETTE};
fn srgb(index: u8) -> (u8, u8, u8) {
const BASE: [(u8, u8, u8); 16] = [
(0, 0, 0),
(128, 0, 0),
(0, 128, 0),
(128, 128, 0),
(0, 0, 128),
(128, 0, 128),
(0, 128, 128),
(192, 192, 192),
(128, 128, 128),
(255, 0, 0),
(0, 255, 0),
(255, 255, 0),
(0, 0, 255),
(255, 0, 255),
(0, 255, 255),
(255, 255, 255),
];
const STEP: [u8; 6] = [0, 95, 135, 175, 215, 255];
match index {
0..=15 => BASE[index as usize],
16..=231 => {
let i = index - 16;
(
STEP[(i / 36) as usize],
STEP[((i / 6) % 6) as usize],
STEP[(i % 6) as usize],
)
}
_ => {
let v = 8 + (index - 232) * 10;
(v, v, v)
}
}
}
fn luminance((r, g, b): (u8, u8, u8)) -> f64 {
fn channel(c: u8) -> f64 {
let c = f64::from(c) / 255.0;
if c <= 0.03928 {
c / 12.92
} else {
((c + 0.055) / 1.055).powf(2.4)
}
}
0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b)
}
fn contrast(a: (u8, u8, u8), b: (u8, u8, u8)) -> f64 {
let (la, lb) = (luminance(a), luminance(b));
let (hi, lo) = if la > lb { (la, lb) } else { (lb, la) };
(hi + 0.05) / (lo + 0.05)
}
fn lab(c: (u8, u8, u8)) -> (f64, f64, f64) {
fn linear(c: u8) -> f64 {
let c = f64::from(c) / 255.0;
if c <= 0.03928 {
c / 12.92
} else {
((c + 0.055) / 1.055).powf(2.4)
}
}
let (r, g, b) = (linear(c.0), linear(c.1), linear(c.2));
let x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047;
let y = r * 0.2126 + g * 0.7152 + b * 0.0722;
let z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883;
fn f(t: f64) -> f64 {
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: (u8, u8, u8), b: (u8, u8, u8)) -> f64 {
let (la, aa, ba) = lab(a);
let (lb, ab, bb) = lab(b);
((la - lb).powi(2) + (aa - ab).powi(2) + (ba - bb).powi(2)).sqrt()
}
const WHITE: (u8, u8, u8) = (255, 255, 255);
const BLACK: (u8, u8, u8) = (0, 0, 0);
#[test]
fn every_palette_colour_reads_on_both_backgrounds() {
for &index in &WIDE_PALETTE {
let c = srgb(index);
let on_white = contrast(c, WHITE);
let on_black = contrast(c, BLACK);
assert!(
on_white >= 3.0,
"colour {index} scores {on_white:.2} against white, below the 3:1 bar"
);
assert!(
on_black >= 3.0,
"colour {index} scores {on_black:.2} against black, below the 3:1 bar"
);
}
}
#[test]
fn palette_colours_are_distinguishable_from_each_other() {
for (i, &a) in WIDE_PALETTE.iter().enumerate() {
for &b in &WIDE_PALETTE[i + 1..] {
let d = delta_e(srgb(a), srgb(b));
assert!(
d >= 22.0,
"colours {a} and {b} are only deltaE {d:.1} apart; a reader sees one colour"
);
}
}
}
#[test]
fn palette_avoids_the_semantic_colours() {
const SEMANTIC: [(u8, u8, u8); 6] = [
(255, 0, 0),
(128, 0, 0),
(0, 255, 0),
(0, 128, 0),
(255, 255, 0),
(128, 128, 0),
];
for &index in &WIDE_PALETTE {
for sem in SEMANTIC {
let d = delta_e(srgb(index), sem);
assert!(
d > 40.0,
"colour {index} is deltaE {d:.1} from a status colour; it would read as a state"
);
}
}
}
#[test]
fn wide_colour_wraps_at_the_palette_size() {
assert_eq!(wide_colour(0), wide_colour(WIDE_PALETTE.len()));
assert_eq!(
wide_colour(3),
anstyle::Ansi256Color(WIDE_PALETTE[3]).into(),
"the returned colour must be the palette entry, not the index"
);
}
#[test]
fn colorterm_truecolor_gets_the_wide_palette() {
assert!(supports_wide_palette(Some("truecolor"), None, false));
assert!(supports_wide_palette(Some("24bit"), None, false));
}
#[test]
fn term_256color_gets_the_wide_palette() {
assert!(supports_wide_palette(None, Some("xterm-256color"), false));
assert!(supports_wide_palette(None, Some("screen-256color"), false));
}
#[test]
fn windows_with_vt_enabled_gets_the_wide_palette() {
assert!(supports_wide_palette(None, None, true));
}
#[test]
fn an_unannounced_terminal_falls_back() {
assert!(!supports_wide_palette(None, None, false));
assert!(!supports_wide_palette(None, Some("vt100"), false));
assert!(!supports_wide_palette(Some(""), Some("dumb"), false));
}
#[test]
fn services_up_to_the_palette_size_never_share_a_colour() {
let names: Vec<String> = (0..20).map(|i| format!("svc{i:02}")).collect();
let map = assign(&names);
let mut seen = std::collections::HashSet::new();
for name in &names {
let idx = map.get(name).copied().expect("every service is assigned");
assert!(seen.insert(idx), "{name} reuses a colour already given out");
}
assert_eq!(seen.len(), 20);
}
#[test]
fn assignment_wraps_past_the_palette_size() {
let names: Vec<String> = (0..25).map(|i| format!("svc{i:02}")).collect();
let map = assign(&names);
assert_eq!(
map.len(),
25,
"every service is assigned even past the palette"
);
assert_eq!(
map["svc00"], map["svc20"],
"the twenty-first wraps onto the first"
);
}
#[test]
fn assignment_ignores_the_order_names_arrive_in() {
let forward: Vec<String> = ["web", "api", "db"].iter().map(|s| s.to_string()).collect();
let backward: Vec<String> = ["db", "api", "web"].iter().map(|s| s.to_string()).collect();
assert_eq!(assign(&forward), assign(&backward));
}
#[test]
fn an_unregistered_service_still_gets_a_stable_colour() {
let map = assign(&["web".to_string()]);
let a = colour_for("stranger", &map);
let b = colour_for("stranger", &map);
assert_eq!(
a, b,
"the same unknown label must always give the same colour"
);
}
#[test]
fn colour_for_prefers_the_registered_slot_over_the_hash() {
let map = assign(&[
"web".to_string(),
"db".to_string(),
"cache".to_string(),
"worker".to_string(),
"queue".to_string(),
]);
let registered = colour_for("db", &map);
let unregistered = colour_for("db", &std::collections::HashMap::new());
assert_eq!(
registered, map["db"],
"a registered label must return its own slot"
);
assert_ne!(
registered, unregistered,
"registration must change the answer, not just agree with the hash"
);
}