1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum Theme {
7 Light,
8 Dark,
9}
10
11impl Theme {
12 pub fn as_str(self) -> &'static str {
13 match self {
14 Theme::Light => "light",
15 Theme::Dark => "dark",
16 }
17 }
18}
19
20pub fn parse_theme_str(value: &str) -> Option<Theme> {
24 match value {
25 "light" => Some(Theme::Light),
26 "dark" => Some(Theme::Dark),
27 _ => None,
28 }
29}
30
31#[cfg(unix)]
36pub fn parse_osc_rgb(body: &str) -> Option<(u8, u8, u8)> {
37 let rest = body.strip_prefix("rgb:")?;
38 let mut parts = rest.splitn(3, '/');
39 let r = parts.next()?;
40 let g = parts.next()?;
41 let b = parts.next()?;
42 if parts.next().is_some() {
43 return None;
44 }
45 Some((
46 scale_hex_component(r)?,
47 scale_hex_component(g)?,
48 scale_hex_component(b)?,
49 ))
50}
51
52#[cfg(unix)]
53fn scale_hex_component(hex: &str) -> Option<u8> {
54 if hex.is_empty() || hex.len() > 4 || !hex.bytes().all(|b| b.is_ascii_hexdigit()) {
55 return None;
56 }
57 let value = u32::from_str_radix(hex, 16).ok()?;
58 let max = 16u32.pow(hex.len() as u32) - 1;
59 Some(((value * 255) / max) as u8)
60}
61
62#[cfg(unix)]
69pub fn theme_from_rgb(r: u8, g: u8, b: u8) -> Theme {
70 let luma = 299 * u32::from(r) + 587 * u32::from(g) + 114 * u32::from(b);
71 if luma >= 128_000 {
72 Theme::Light
73 } else {
74 Theme::Dark
75 }
76}
77
78pub fn parse_colorfgbg(value: &str) -> Option<Theme> {
83 let bg = value.rsplit(';').next()?.trim();
84 let index: u8 = bg.parse().ok()?;
85 Some(if matches!(index, 7 | 9..=15) {
86 Theme::Light
87 } else {
88 Theme::Dark
89 })
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[test]
97 fn parse_theme_str_accepts_only_exact_light_or_dark() {
98 assert_eq!(parse_theme_str("light"), Some(Theme::Light));
99 assert_eq!(parse_theme_str("dark"), Some(Theme::Dark));
100 assert_eq!(parse_theme_str("Light"), None);
101 assert_eq!(parse_theme_str("DARK"), None);
102 assert_eq!(parse_theme_str(""), None);
103 assert_eq!(parse_theme_str("light "), None);
104 }
105
106 #[cfg(unix)]
107 #[test]
108 fn parse_osc_rgb_scales_16_bit_components_to_8_bit() {
109 assert_eq!(parse_osc_rgb("rgb:ffff/ffff/ffff"), Some((255, 255, 255)));
111 assert_eq!(parse_osc_rgb("rgb:0000/0000/0000"), Some((0, 0, 0)));
113 }
114
115 #[cfg(unix)]
116 #[test]
117 fn parse_osc_rgb_scales_differing_component_widths() {
118 assert_eq!(parse_osc_rgb("rgb:ff/ff/ff"), Some((255, 255, 255)));
121 assert_eq!(parse_osc_rgb("rgb:00/00/00"), Some((0, 0, 0)));
122 }
123
124 #[cfg(unix)]
125 #[test]
126 fn parse_osc_rgb_rejects_malformed_bodies() {
127 assert_eq!(parse_osc_rgb(""), None);
128 assert_eq!(parse_osc_rgb("rgb:ffff/ffff"), None); assert_eq!(parse_osc_rgb("rgb:ffff/ffff/ffff/ffff"), None); assert_eq!(parse_osc_rgb("rgb:gggg/ffff/ffff"), None); assert_eq!(parse_osc_rgb("rgb:fffff/ffff/ffff"), None); assert_eq!(parse_osc_rgb("rgb:/ffff/ffff"), None); assert_eq!(parse_osc_rgb("not-rgb-at-all"), None);
134 }
135
136 #[cfg(unix)]
137 #[test]
138 fn theme_from_rgb_matches_shell_luma_threshold() {
139 assert_eq!(theme_from_rgb(255, 255, 255), Theme::Light);
141 assert_eq!(theme_from_rgb(0, 0, 0), Theme::Dark);
143 assert_eq!(theme_from_rgb(128, 128, 128), Theme::Light);
146 assert_eq!(theme_from_rgb(127, 127, 127), Theme::Dark);
147 }
148
149 #[test]
150 fn parse_colorfgbg_reads_only_the_last_field_as_background() {
151 assert_eq!(parse_colorfgbg("15;0"), Some(Theme::Dark));
152 assert_eq!(parse_colorfgbg("0;15"), Some(Theme::Light));
153 assert_eq!(parse_colorfgbg("15;default;0"), Some(Theme::Dark));
155 }
156
157 #[test]
158 fn parse_colorfgbg_classifies_full_xterm_16_color_palette() {
159 for dark_index in [0, 1, 2, 3, 4, 5, 6, 8] {
160 assert_eq!(
161 parse_colorfgbg(&format!("15;{dark_index}")),
162 Some(Theme::Dark),
163 "index {dark_index} should be dark"
164 );
165 }
166 for light_index in [7, 9, 10, 11, 12, 13, 14, 15] {
167 assert_eq!(
168 parse_colorfgbg(&format!("0;{light_index}")),
169 Some(Theme::Light),
170 "index {light_index} should be light"
171 );
172 }
173 }
174
175 #[test]
176 fn parse_colorfgbg_rejects_malformed_values() {
177 assert_eq!(parse_colorfgbg(""), None);
178 assert_eq!(parse_colorfgbg("not-a-number"), None);
179 assert_eq!(parse_colorfgbg(";"), None);
180 }
181}