#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Rgb { pub r: u8, pub g: u8, pub b: u8 }
impl Rgb {
pub const fn new(r: u8, g: u8, b: u8) -> Self { Rgb { r, g, b } }
}
pub const RUST: Rgb = Rgb::new(160, 99, 75);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Tone {
Color(Rgb),
Terminal,
TerminalFaint,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum Theme {
#[default]
Rust,
HighContrast,
Mono,
}
impl Theme {
pub const NAMES: [&'static str; 3] = ["rust", "high-contrast", "mono"];
#[allow(clippy::should_implement_trait)] pub fn from_str(s: &str) -> Option<Theme> {
match s.trim().to_ascii_lowercase().as_str() {
"rust" => Some(Theme::Rust),
"high-contrast" => Some(Theme::HighContrast),
"mono" => Some(Theme::Mono),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Palette {
pub core: Tone,
pub dim: Tone,
pub edge: Tone,
}
pub fn palette(theme: Theme) -> Palette {
use Tone::{Color, Terminal, TerminalFaint};
match theme {
Theme::Rust => Palette {
core: Color(Rgb::new(210, 146, 118)),
dim: Color(Rgb::new(170, 124, 104)),
edge: Color(Rgb::new(150, 122, 112)),
},
Theme::HighContrast => Palette {
core: Color(Rgb::new(236, 205, 186)),
dim: Color(Rgb::new(198, 152, 124)),
edge: Color(Rgb::new(176, 142, 126)),
},
Theme::Mono => Palette { core: Terminal, dim: TerminalFaint, edge: TerminalFaint },
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rel_lum(c: Rgb) -> f64 {
fn ch(v: u8) -> f64 {
let s = v as f64 / 255.0;
if s <= 0.03928 { s / 12.92 } else { ((s + 0.055) / 1.055).powf(2.4) }
}
0.2126 * ch(c.r) + 0.7152 * ch(c.g) + 0.0722 * ch(c.b)
}
fn contrast(fg: Rgb, bg: Rgb) -> f64 {
let (a, b) = (rel_lum(fg), rel_lum(bg));
let (hi, lo) = if a >= b { (a, b) } else { (b, a) };
(hi + 0.05) / (lo + 0.05)
}
fn rgb_of(t: Tone) -> Rgb {
match t { Tone::Color(c) => c, _ => panic!("expected a Color tone") }
}
const DARK_BG: Rgb = Rgb::new(40, 44, 52);
#[test]
fn rust_is_the_brand_anchor() {
assert_eq!(RUST, Rgb::new(160, 99, 75));
}
#[test]
fn color_palettes_clear_the_contrast_targets() {
for theme in [Theme::Rust, Theme::HighContrast] {
let p = palette(theme);
assert!(contrast(rgb_of(p.core), DARK_BG) >= 4.5, "{theme:?} core too low");
assert!(contrast(rgb_of(p.dim), DARK_BG) >= 3.0, "{theme:?} dim too low");
assert!(contrast(rgb_of(p.edge), DARK_BG) >= 3.0, "{theme:?} edge too low");
}
}
#[test]
fn tones_keep_their_brightness_order() {
for theme in [Theme::Rust, Theme::HighContrast] {
let p = palette(theme);
assert!(rel_lum(rgb_of(p.core)) >= rel_lum(rgb_of(p.dim)));
assert!(rel_lum(rgb_of(p.dim)) >= rel_lum(rgb_of(p.edge)));
}
}
#[test]
fn mono_uses_the_terminal_foreground() {
let p = palette(Theme::Mono);
assert_eq!(p.core, Tone::Terminal);
assert_eq!(p.dim, Tone::TerminalFaint);
assert_eq!(p.edge, Tone::TerminalFaint);
}
#[test]
fn theme_from_str_parses_canonical_names() {
assert_eq!(Theme::from_str("rust"), Some(Theme::Rust));
assert_eq!(Theme::from_str("high-contrast"), Some(Theme::HighContrast));
assert_eq!(Theme::from_str(" Mono "), Some(Theme::Mono));
assert_eq!(Theme::from_str("bogus"), None);
assert_eq!(Theme::default(), Theme::Rust);
}
}