1use std::sync::LazyLock;
18
19use crate::cell::Color;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
23pub enum ColorLevel {
24 None,
26 Basic,
28 Ansi256,
30 #[default]
32 TrueColor,
33}
34
35impl ColorLevel {
36 pub fn has_color(self) -> bool {
38 self >= Self::Basic
39 }
40
41 pub fn has_256(self) -> bool {
43 self >= Self::Ansi256
44 }
45
46 pub fn has_truecolor(self) -> bool {
48 self >= Self::TrueColor
49 }
50}
51
52static COLOR_LEVEL: LazyLock<ColorLevel> = LazyLock::new(detect_color_level_inner);
53
54pub fn detect_color_level() -> ColorLevel {
62 *COLOR_LEVEL
63}
64
65fn detect_color_level_inner() -> ColorLevel {
66 if std::env::var_os("NO_COLOR").is_some() {
68 return ColorLevel::None;
69 }
70
71 let level = match supports_color::on(supports_color::Stream::Stdout) {
72 None => ColorLevel::TrueColor,
75 Some(level) => {
76 if level.has_16m {
77 ColorLevel::TrueColor
78 } else if level.has_256 {
79 ColorLevel::Ansi256
80 } else if level.has_basic {
81 ColorLevel::Basic
82 } else {
83 ColorLevel::None
84 }
85 }
86 };
87
88 if level < ColorLevel::TrueColor && terminal_supports_truecolor() {
93 return ColorLevel::TrueColor;
94 }
95
96 level
97}
98
99fn terminal_supports_truecolor() -> bool {
105 const TRUECOLOR_TERMINALS: &[&str] = &["WezTerm", "ghostty", "kitty", "rio", "tabby", "vscode"];
106
107 if std::env::var_os("ITERM_SESSION_ID").is_some() {
109 return true;
110 }
111 if std::env::var_os("GHOSTTY_RESOURCES_DIR").is_some() {
113 return true;
114 }
115 if let Some(term_program) = std::env::var_os("TERM_PROGRAM")
117 && let Some(s) = term_program.to_str()
118 && TRUECOLOR_TERMINALS.contains(&s)
119 {
120 return true;
121 }
122 false
123}
124
125pub fn rgb_to_ansi256(r: u8, g: u8, b: u8) -> u8 {
130 if r == g && g == b {
132 if r < 8 {
134 return 16;
135 }
136 if r > 248 {
137 return 231;
138 }
139 return ((r as u16 - 8) * 24 / 240) as u8 + 232;
141 }
142 let cube = |c: u8| -> u8 {
144 if c < 48 {
145 0
146 } else if c < 115 {
147 1
148 } else {
149 (c as u16 * 5 / 255) as u8
150 }
151 };
152 16 + 36 * cube(r) + 6 * cube(g) + cube(b)
153}
154
155pub fn ansi256_to_basic(idx: u8) -> u8 {
157 match idx {
158 0..=7 => idx, 8 => 0, 9..=15 => idx, 16 => 0, 17..=21 => 4, 22..=27 => 2, 28..=51 => 2, 52..=87 => 3, 88..=123 => 1, 124..=159 => 1, 160..=195 => 3, 196..=231 => 9, 232..=237 => 0, 238..=243 => 8, 244..=249 => 7, 250..=255 => 15, }
175}
176
177pub fn adapt_color(color: Color, level: ColorLevel) -> Color {
187 match (color, level) {
188 (_, ColorLevel::None) => Color::Reset,
189 (Color::Reset, _) => Color::Reset,
190 (c, ColorLevel::TrueColor) => c,
191 (Color::Rgb(r, g, b), ColorLevel::Ansi256) => Color::Indexed(rgb_to_ansi256(r, g, b)),
192 (Color::Rgb(r, g, b), ColorLevel::Basic) => {
193 Color::Indexed(ansi256_to_basic(rgb_to_ansi256(r, g, b)))
194 }
195 (c @ Color::Indexed(_), ColorLevel::Ansi256) => c,
196 (Color::Indexed(idx), ColorLevel::Basic) => {
197 if idx < 16 {
198 Color::Indexed(idx) } else {
200 Color::Indexed(ansi256_to_basic(idx))
201 }
202 }
203 (c, ColorLevel::Basic | ColorLevel::Ansi256) => c,
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 #[test]
215 fn test_color_level_ordering() {
216 assert!(ColorLevel::TrueColor > ColorLevel::Ansi256);
217 assert!(ColorLevel::Ansi256 > ColorLevel::Basic);
218 assert!(ColorLevel::Basic > ColorLevel::None);
219 }
220
221 #[test]
222 fn test_color_level_predicates() {
223 assert!(ColorLevel::TrueColor.has_truecolor());
224 assert!(ColorLevel::TrueColor.has_256());
225 assert!(ColorLevel::TrueColor.has_color());
226
227 assert!(!ColorLevel::Ansi256.has_truecolor());
228 assert!(ColorLevel::Ansi256.has_256());
229 assert!(ColorLevel::Ansi256.has_color());
230
231 assert!(!ColorLevel::Basic.has_truecolor());
232 assert!(!ColorLevel::Basic.has_256());
233 assert!(ColorLevel::Basic.has_color());
234
235 assert!(!ColorLevel::None.has_color());
236 }
237
238 #[test]
239 fn test_detect_caches_after_first_call() {
240 let first = detect_color_level();
242 let second = detect_color_level();
243 assert_eq!(first, second);
244 }
245
246 #[test]
247 fn test_rgb_to_ansi256_pure_red() {
248 assert_eq!(rgb_to_ansi256(255, 0, 0), 196);
250 }
251
252 #[test]
253 fn test_rgb_to_ansi256_pure_green() {
254 assert_eq!(rgb_to_ansi256(0, 255, 0), 46);
256 }
257
258 #[test]
259 fn test_rgb_to_ansi256_pure_blue() {
260 assert_eq!(rgb_to_ansi256(0, 0, 255), 21);
262 }
263
264 #[test]
265 fn test_rgb_to_ansi256_black() {
266 assert_eq!(rgb_to_ansi256(0, 0, 0), 16);
268 }
269
270 #[test]
271 fn test_rgb_to_ansi256_white() {
272 assert_eq!(rgb_to_ansi256(255, 255, 255), 231);
274 }
275
276 #[test]
277 fn test_rgb_to_ansi256_midgray() {
278 let idx = rgb_to_ansi256(128, 128, 128);
280 assert!((232..=255).contains(&idx));
281 }
282
283 #[test]
284 fn test_ansi256_to_basic_corners() {
285 assert_eq!(ansi256_to_basic(0), 0);
287 assert_eq!(ansi256_to_basic(196), 9);
289 assert_eq!(ansi256_to_basic(46), 2);
291 assert_eq!(ansi256_to_basic(21), 4);
293 assert_eq!(ansi256_to_basic(255), 15);
295 }
296
297 #[test]
298 fn test_terminal_supports_truecolor_iterm() {
299 unsafe {
302 std::env::set_var("ITERM_SESSION_ID", "test:value");
303 }
304 assert!(terminal_supports_truecolor());
305 unsafe {
306 std::env::remove_var("ITERM_SESSION_ID");
307 }
308 }
309
310 #[test]
311 fn test_terminal_does_not_support_truecolor_unknown() {
312 unsafe {
314 std::env::remove_var("ITERM_SESSION_ID");
315 std::env::remove_var("GHOSTTY_RESOURCES_DIR");
316 std::env::remove_var("TERM_PROGRAM");
317 }
318 assert!(!terminal_supports_truecolor());
319 }
320
321 #[test]
322 fn test_adapt_color_truecolor_passes_through_rgb() {
323 let c = Color::Rgb(123, 45, 67);
325 assert_eq!(adapt_color(c, ColorLevel::TrueColor), c);
326 }
327
328 #[test]
329 fn test_adapt_color_ansi256_downgrades_rgb() {
330 let c = Color::Rgb(255, 0, 0);
332 let adapted = adapt_color(c, ColorLevel::Ansi256);
333 assert_eq!(adapted, Color::Indexed(196));
334 }
335
336 #[test]
337 fn test_adapt_color_basic_downgrades_rgb_to_basic() {
338 let c = Color::Rgb(255, 0, 0);
340 let adapted = adapt_color(c, ColorLevel::Basic);
341 match adapted {
343 Color::Indexed(idx) => assert!(idx < 16, "expected basic color, got {idx}"),
344 other => panic!("expected Indexed, got {other:?}"),
345 }
346 }
347
348 #[test]
349 fn test_adapt_color_none_returns_reset() {
350 let c = Color::Rgb(123, 45, 67);
352 assert_eq!(adapt_color(c, ColorLevel::None), Color::Reset);
353 }
354
355 #[test]
356 fn test_adapt_color_named_stays_named() {
357 let c = Color::Red;
359 assert_eq!(adapt_color(c, ColorLevel::Basic), c);
360 assert_eq!(adapt_color(c, ColorLevel::Ansi256), c);
361 assert_eq!(adapt_color(c, ColorLevel::TrueColor), c);
362 }
363
364 #[test]
365 fn test_adapt_color_none_named_to_reset() {
366 assert_eq!(adapt_color(Color::Red, ColorLevel::None), Color::Reset);
368 }
369
370 #[test]
371 fn test_adapt_color_reset_always_reset() {
372 assert_eq!(adapt_color(Color::Reset, ColorLevel::None), Color::Reset);
374 assert_eq!(
375 adapt_color(Color::Reset, ColorLevel::TrueColor),
376 Color::Reset
377 );
378 }
379
380 #[test]
381 fn test_adapt_color_ansi256_indexed_stays_at_ansi256() {
382 let c = Color::Indexed(196);
384 assert_eq!(adapt_color(c, ColorLevel::Ansi256), c);
385 }
386
387 #[test]
388 fn test_adapt_color_ansi256_indexed_downgrades_at_basic() {
389 let c = Color::Indexed(196);
391 assert_eq!(adapt_color(c, ColorLevel::Basic), Color::Indexed(9));
392 }
393}