1use std::env;
9
10use super::Color;
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
14pub enum ColorMode {
15 #[default]
17 TrueColor,
18 Color256,
20 Basic16,
22}
23
24impl ColorMode {
25 pub fn detect() -> Self {
29 if let Ok(mode) = env::var("PHOTON_COLOR_MODE") {
30 match mode.as_str() {
31 | "truecolor" | "24bit" | "rgb" => return ColorMode::TrueColor,
32 | "256" | "256color" => return ColorMode::Color256,
33 | "16" | "basic" => return ColorMode::Basic16,
34 | _ => {},
35 }
36 }
37 if let Ok(ct) = env::var("COLORTERM") &&
38 (ct == "truecolor" || ct == "24bit")
39 {
40 return ColorMode::TrueColor;
41 }
42 if let Ok(term) = env::var("TERM") &&
43 term.contains("256color")
44 {
45 return ColorMode::Color256;
46 }
47 ColorMode::TrueColor
48 }
49}
50
51pub fn fg(color: Color, mode: ColorMode) -> String {
53 match mode {
54 | ColorMode::TrueColor => format!("\x1b[38;2;{};{};{}m", color.0, color.1, color.2),
55 | ColorMode::Color256 => format!("\x1b[38;5;{}m", rgb_to_256(color)),
56 | ColorMode::Basic16 => format!("\x1b[{}m", rgb_to_16_fg(color)),
57 }
58}
59
60pub fn bg(color: Color, mode: ColorMode) -> String {
62 match mode {
63 | ColorMode::TrueColor => format!("\x1b[48;2;{};{};{}m", color.0, color.1, color.2),
64 | ColorMode::Color256 => format!("\x1b[48;5;{}m", rgb_to_256(color)),
65 | ColorMode::Basic16 => format!("\x1b[{}m", rgb_to_16_bg(color)),
66 }
67}
68
69pub const RESET: &str = "\x1b[0m";
71
72fn rgb_to_256(color: Color) -> u8 {
75 let Color(r, g, b) = color;
76
77 if r == g && g == b {
79 if r < 8 {
80 return 16;
81 }
82 if r > 248 {
83 return 231;
84 }
85 return 232 + ((r - 8) / 10);
86 }
87
88 let r = closest_cube_level(r);
90 let g = closest_cube_level(g);
91 let b = closest_cube_level(b);
92 16 + 36 * r + 6 * g + b
93}
94
95fn closest_cube_level(v: u8) -> u8 {
96 if v < 48 {
98 0
99 } else if v < 115 {
100 1
101 } else if v < 155 {
102 2
103 } else if v < 195 {
104 3
105 } else if v < 235 {
106 4
107 } else {
108 5
109 }
110}
111
112fn rgb_to_16_fg(color: Color) -> u8 {
115 30 + rgb_to_16_index(color)
116}
117
118fn rgb_to_16_bg(color: Color) -> u8 {
119 40 + rgb_to_16_index(color)
120}
121
122fn rgb_to_16_index(color: Color) -> u8 {
123 let Color(r, g, b) = color;
124 let intensity = (r as u16 + g as u16 + b as u16) / 3;
125
126 let is_bright = intensity > 128;
128 let idx = if r > 128 && g < 128 && b < 128 {
129 1 } else if r < 128 && g > 128 && b < 128 {
131 2 } else if r > 128 && g > 128 && b < 128 {
133 3 } else if r < 128 && g < 128 && b > 128 {
135 4 } else if r > 128 && g < 128 && b > 128 {
137 5 } else if r < 128 && g > 128 && b > 128 {
139 6 } else {
141 0 };
143
144 if is_bright && idx == 0 {
145 7 } else if is_bright {
147 idx + 60 } else {
149 idx
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use std::{
156 env,
157 sync::Mutex,
158 };
159
160 use super::*;
161
162 static ENV_LOCK: Mutex<()> = Mutex::new(());
165
166 fn with_envs<F>(vars: &[(&str, Option<&str>)], f: F)
167 where
168 F: FnOnce(), {
169 let _guard = ENV_LOCK.lock().unwrap();
170 let originals: Vec<(String, Option<String>)> = vars
171 .iter()
172 .map(|(key, _)| ((*key).to_string(), env::var(key).ok()))
173 .collect();
174 for (key, value) in vars {
175 match value {
176 | Some(v) => unsafe { env::set_var(key, v) },
178 | None => unsafe { env::remove_var(key) },
180 }
181 }
182 f();
183 for (key, original) in originals {
184 match original {
185 | Some(v) => unsafe { env::set_var(&key, v) },
187 | None => unsafe { env::remove_var(&key) },
189 }
190 }
191 }
192
193 #[test]
194 fn truecolor_fg() {
195 let s = fg(Color::SUNBEAM_ORANGE, ColorMode::TrueColor);
196 assert_eq!(s, "\x1b[38;2;250;82;15m");
197 }
198
199 #[test]
200 fn truecolor_bg() {
201 let s = bg(Color::WARM_IVORY, ColorMode::TrueColor);
202 assert_eq!(s, "\x1b[48;2;255;250;237m");
203 }
204
205 #[test]
206 fn color256_produces_valid_codes() {
207 let s = fg(Color::SUNBEAM_ORANGE, ColorMode::Color256);
208 assert!(s.starts_with("\x1b[38;5;"));
209 assert!(s.ends_with('m'));
210 }
211
212 #[test]
213 fn color256_bg_produces_valid_codes() {
214 let s = bg(Color::SUNBEAM_ORANGE, ColorMode::Color256);
215 assert!(s.starts_with("\x1b[48;5;"));
216 assert!(s.ends_with('m'));
217 }
218
219 #[test]
220 fn basic16_produces_valid_codes() {
221 let s = fg(Color::WHITE, ColorMode::Basic16);
222 assert!(s.starts_with("\x1b["));
223 assert!(s.ends_with('m'));
224 }
225
226 #[test]
227 fn basic16_bg_produces_valid_codes() {
228 let s = bg(Color::WHITE, ColorMode::Basic16);
229 assert!(s.starts_with("\x1b["));
230 assert!(s.ends_with('m'));
231 }
232
233 #[test]
234 fn detect_respects_photon_color_mode() {
235 with_envs(&[("PHOTON_COLOR_MODE", Some("256"))], || {
236 assert_eq!(ColorMode::detect(), ColorMode::Color256);
237 });
238 with_envs(&[("PHOTON_COLOR_MODE", Some("basic"))], || {
239 assert_eq!(ColorMode::detect(), ColorMode::Basic16);
240 });
241 with_envs(&[("PHOTON_COLOR_MODE", Some("truecolor"))], || {
242 assert_eq!(ColorMode::detect(), ColorMode::TrueColor);
243 });
244 with_envs(
245 &[
246 ("PHOTON_COLOR_MODE", Some("unknown")),
247 ("COLORTERM", None),
248 ("TERM", None),
249 ],
250 || {
251 assert_eq!(ColorMode::detect(), ColorMode::TrueColor);
252 },
253 );
254 }
255
256 #[test]
257 fn detect_respects_colorterm() {
258 with_envs(
259 &[
260 ("PHOTON_COLOR_MODE", None),
261 ("COLORTERM", Some("truecolor")),
262 ("TERM", None),
263 ],
264 || {
265 assert_eq!(ColorMode::detect(), ColorMode::TrueColor);
266 },
267 );
268 with_envs(
269 &[
270 ("PHOTON_COLOR_MODE", None),
271 ("COLORTERM", Some("24bit")),
272 ("TERM", None),
273 ],
274 || {
275 assert_eq!(ColorMode::detect(), ColorMode::TrueColor);
276 },
277 );
278 }
279
280 #[test]
281 fn detect_respects_term() {
282 with_envs(
283 &[
284 ("PHOTON_COLOR_MODE", None),
285 ("COLORTERM", None),
286 ("TERM", Some("xterm-256color")),
287 ],
288 || {
289 assert_eq!(ColorMode::detect(), ColorMode::Color256);
290 },
291 );
292 with_envs(
293 &[
294 ("PHOTON_COLOR_MODE", None),
295 ("COLORTERM", None),
296 ("TERM", Some("xterm")),
297 ],
298 || {
299 assert_eq!(ColorMode::detect(), ColorMode::TrueColor);
300 },
301 );
302 }
303
304 #[test]
305 fn rgb_to_256_grayscale() {
306 assert_eq!(rgb_to_256(Color(0, 0, 0)), 16);
307 assert_eq!(rgb_to_256(Color(255, 255, 255)), 231);
308 assert_eq!(rgb_to_256(Color(128, 128, 128)), 232 + ((128 - 8) / 10));
309 }
310
311 #[test]
312 fn rgb_to_256_color_cube() {
313 let idx = rgb_to_256(Color::SUNBEAM_ORANGE);
315 assert!((16..=231).contains(&idx));
316 }
317
318 #[test]
319 fn closest_cube_level_branches() {
320 assert_eq!(closest_cube_level(0), 0);
321 assert_eq!(closest_cube_level(47), 0);
322 assert_eq!(closest_cube_level(48), 1);
323 assert_eq!(closest_cube_level(114), 1);
324 assert_eq!(closest_cube_level(115), 2);
325 assert_eq!(closest_cube_level(154), 2);
326 assert_eq!(closest_cube_level(155), 3);
327 assert_eq!(closest_cube_level(194), 3);
328 assert_eq!(closest_cube_level(195), 4);
329 assert_eq!(closest_cube_level(234), 4);
330 assert_eq!(closest_cube_level(235), 5);
331 assert_eq!(closest_cube_level(255), 5);
332 }
333
334 #[test]
335 fn rgb_to_16_fg_and_bg() {
336 assert_eq!(rgb_to_16_fg(Color::WHITE), 37);
338 assert_eq!(rgb_to_16_bg(Color::WHITE), 47);
339 }
340
341 #[test]
342 fn rgb_to_16_index_color_branches() {
343 assert_eq!(rgb_to_16_index(Color(255, 0, 0)), 1);
345 assert_eq!(rgb_to_16_index(Color(0, 255, 0)), 2);
347 assert_eq!(rgb_to_16_index(Color(255, 255, 0)), 63);
349 assert_eq!(rgb_to_16_index(Color(0, 0, 255)), 4);
351 assert_eq!(rgb_to_16_index(Color(255, 0, 255)), 65);
353 assert_eq!(rgb_to_16_index(Color(0, 255, 255)), 66);
355 assert_eq!(rgb_to_16_index(Color(0, 0, 0)), 0);
357 assert_eq!(rgb_to_16_index(Color(255, 255, 255)), 7);
359 assert_eq!(rgb_to_16_index(Color(192, 0, 0)), 1);
361 }
362}