1#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2pub struct Rgb { pub r: u8, pub g: u8, pub b: u8 }
3
4impl Rgb {
5 pub const fn new(r: u8, g: u8, b: u8) -> Self { Rgb { r, g, b } }
6}
7
8pub const RUST: Rgb = Rgb::new(160, 99, 75);
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum Tone {
16 Color(Rgb),
17 Terminal,
18 TerminalFaint,
19}
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
23pub enum Theme {
24 #[default]
25 Rust,
26 HighContrast,
27 Mono,
28}
29
30impl Theme {
31 pub const NAMES: [&'static str; 3] = ["rust", "high-contrast", "mono"];
32
33 #[allow(clippy::should_implement_trait)] pub fn from_str(s: &str) -> Option<Theme> {
36 match s.trim().to_ascii_lowercase().as_str() {
37 "rust" => Some(Theme::Rust),
38 "high-contrast" => Some(Theme::HighContrast),
39 "mono" => Some(Theme::Mono),
40 _ => None,
41 }
42 }
43}
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub struct Palette {
49 pub core: Tone,
50 pub dim: Tone,
51 pub edge: Tone,
52}
53
54pub fn palette(theme: Theme) -> Palette {
58 use Tone::{Color, Terminal, TerminalFaint};
59 match theme {
60 Theme::Rust => Palette {
61 core: Color(Rgb::new(210, 146, 118)),
62 dim: Color(Rgb::new(170, 124, 104)),
63 edge: Color(Rgb::new(150, 122, 112)),
64 },
65 Theme::HighContrast => Palette {
66 core: Color(Rgb::new(236, 205, 186)),
67 dim: Color(Rgb::new(198, 152, 124)),
68 edge: Color(Rgb::new(176, 142, 126)),
69 },
70 Theme::Mono => Palette { core: Terminal, dim: TerminalFaint, edge: TerminalFaint },
71 }
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 fn rel_lum(c: Rgb) -> f64 {
80 fn ch(v: u8) -> f64 {
81 let s = v as f64 / 255.0;
82 if s <= 0.03928 { s / 12.92 } else { ((s + 0.055) / 1.055).powf(2.4) }
83 }
84 0.2126 * ch(c.r) + 0.7152 * ch(c.g) + 0.0722 * ch(c.b)
85 }
86 fn contrast(fg: Rgb, bg: Rgb) -> f64 {
87 let (a, b) = (rel_lum(fg), rel_lum(bg));
88 let (hi, lo) = if a >= b { (a, b) } else { (b, a) };
89 (hi + 0.05) / (lo + 0.05)
90 }
91 fn rgb_of(t: Tone) -> Rgb {
92 match t { Tone::Color(c) => c, _ => panic!("expected a Color tone") }
93 }
94
95 const DARK_BG: Rgb = Rgb::new(40, 44, 52);
98
99 #[test]
100 fn rust_is_the_brand_anchor() {
101 assert_eq!(RUST, Rgb::new(160, 99, 75));
102 }
103
104 #[test]
105 fn color_palettes_clear_the_contrast_targets() {
106 for theme in [Theme::Rust, Theme::HighContrast] {
107 let p = palette(theme);
108 assert!(contrast(rgb_of(p.core), DARK_BG) >= 4.5, "{theme:?} core too low");
109 assert!(contrast(rgb_of(p.dim), DARK_BG) >= 3.0, "{theme:?} dim too low");
110 assert!(contrast(rgb_of(p.edge), DARK_BG) >= 3.0, "{theme:?} edge too low");
111 }
112 }
113
114 #[test]
115 fn tones_keep_their_brightness_order() {
116 for theme in [Theme::Rust, Theme::HighContrast] {
117 let p = palette(theme);
118 assert!(rel_lum(rgb_of(p.core)) >= rel_lum(rgb_of(p.dim)));
119 assert!(rel_lum(rgb_of(p.dim)) >= rel_lum(rgb_of(p.edge)));
120 }
121 }
122
123 #[test]
124 fn mono_uses_the_terminal_foreground() {
125 let p = palette(Theme::Mono);
126 assert_eq!(p.core, Tone::Terminal);
127 assert_eq!(p.dim, Tone::TerminalFaint);
128 assert_eq!(p.edge, Tone::TerminalFaint);
129 }
130
131 #[test]
132 fn theme_from_str_parses_canonical_names() {
133 assert_eq!(Theme::from_str("rust"), Some(Theme::Rust));
134 assert_eq!(Theme::from_str("high-contrast"), Some(Theme::HighContrast));
135 assert_eq!(Theme::from_str(" Mono "), Some(Theme::Mono));
136 assert_eq!(Theme::from_str("bogus"), None);
137 assert_eq!(Theme::default(), Theme::Rust);
138 }
139}