Skip to main content

presentar_terminal/
color.rs

1//! Color mode detection and conversion for terminals.
2
3use crossterm::style::Color as CrosstermColor;
4use presentar_core::Color;
5
6/// Terminal color capability mode.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
8pub enum ColorMode {
9    /// 24-bit true color (COLORTERM=truecolor or 24bit).
10    #[default]
11    TrueColor,
12    /// 256 color palette.
13    Color256,
14    /// 16 ANSI colors.
15    Color16,
16    /// Monochrome (no color).
17    Mono,
18}
19
20impl ColorMode {
21    /// Auto-detect terminal color capabilities.
22    #[must_use]
23    pub fn detect() -> Self {
24        Self::detect_with_env(std::env::var("COLORTERM").ok(), std::env::var("TERM").ok())
25    }
26
27    /// Detect color mode from environment variable values.
28    /// This is the testable core of `detect()`.
29    #[must_use]
30    #[allow(clippy::needless_pass_by_value)]
31    pub fn detect_with_env(colorterm: Option<String>, term: Option<String>) -> Self {
32        // Check COLORTERM first (most reliable)
33        if let Some(ref ct) = colorterm {
34            if ct == "truecolor" || ct == "24bit" {
35                return Self::TrueColor;
36            }
37        }
38
39        // Fall back to TERM
40        match term.as_deref() {
41            Some(t) if t.contains("256color") => Self::Color256,
42            Some(t) if t.contains("color") || t.contains("xterm") => Self::Color16,
43            Some("dumb") | None => Self::Mono,
44            _ => Self::Color16,
45        }
46    }
47
48    /// Convert a presentar Color to crossterm Color based on this mode.
49    ///
50    /// Note: Transparent colors (alpha = 0) return `CrosstermColor::Reset` which
51    /// uses the terminal's default background color instead of rendering as black.
52    #[must_use]
53    pub fn to_crossterm(&self, color: Color) -> CrosstermColor {
54        debug_assert!(color.r >= 0.0 && color.r <= 1.0, "r must be in 0.0-1.0");
55        debug_assert!(color.g >= 0.0 && color.g <= 1.0, "g must be in 0.0-1.0");
56        debug_assert!(color.b >= 0.0 && color.b <= 1.0, "b must be in 0.0-1.0");
57        debug_assert!(color.a >= 0.0 && color.a <= 1.0, "a must be in 0.0-1.0");
58
59        // CRITICAL: Handle transparent colors specially to avoid black squares!
60        // Color::TRANSPARENT is {r: 0, g: 0, b: 0, a: 0} - without this check,
61        // it would convert to RGB(0,0,0) = BLACK, creating ugly black artifacts.
62        if color.a == 0.0 {
63            return CrosstermColor::Reset;
64        }
65
66        let r = (color.r * 255.0).round() as u8;
67        let g = (color.g * 255.0).round() as u8;
68        let b = (color.b * 255.0).round() as u8;
69
70        match self {
71            Self::TrueColor => CrosstermColor::Rgb { r, g, b },
72            Self::Color256 => CrosstermColor::AnsiValue(Self::rgb_to_256(r, g, b)),
73            Self::Color16 => Self::rgb_to_16(r, g, b),
74            Self::Mono => CrosstermColor::White,
75        }
76    }
77
78    /// Convert RGB to 256-color palette index.
79    fn rgb_to_256(r: u8, g: u8, b: u8) -> u8 {
80        // Check for grayscale (r == g == b)
81        if r == g && g == b {
82            if r < 8 {
83                return 16; // black
84            }
85            if r > 248 {
86                return 231; // white
87            }
88            // Grayscale ramp: colors 232-255 (24 shades)
89            return 232 + ((r - 8) / 10).min(23);
90        }
91
92        // 6x6x6 color cube (colors 16-231)
93        let r_idx = (u16::from(r) * 5 / 255) as u8;
94        let g_idx = (u16::from(g) * 5 / 255) as u8;
95        let b_idx = (u16::from(b) * 5 / 255) as u8;
96        16 + 36 * r_idx + 6 * g_idx + b_idx
97    }
98
99    /// Convert RGB to 16-color ANSI.
100    fn rgb_to_16(r: u8, g: u8, b: u8) -> CrosstermColor {
101        let luminance = (u32::from(r) * 299 + u32::from(g) * 587 + u32::from(b) * 114) / 1000;
102        let bright = luminance > 127;
103
104        let max = r.max(g).max(b);
105        let threshold = max / 2;
106
107        let has_r = r > threshold;
108        let has_g = g > threshold;
109        let has_b = b > threshold;
110
111        match (has_r, has_g, has_b, bright) {
112            (false, false, false, false) => CrosstermColor::Black,
113            (false, false, false, true) => CrosstermColor::DarkGrey,
114            (true, false, false, false) => CrosstermColor::DarkRed,
115            (true, false, false, true) => CrosstermColor::Red,
116            (false, true, false, false) => CrosstermColor::DarkGreen,
117            (false, true, false, true) => CrosstermColor::Green,
118            (true, true, false, false) => CrosstermColor::DarkYellow,
119            (true, true, false, true) => CrosstermColor::Yellow,
120            (false, false, true, false) => CrosstermColor::DarkBlue,
121            (false, false, true, true) => CrosstermColor::Blue,
122            (true, false, true, false) => CrosstermColor::DarkMagenta,
123            (true, false, true, true) => CrosstermColor::Magenta,
124            (false, true, true, false) => CrosstermColor::DarkCyan,
125            (false, true, true, true) => CrosstermColor::Cyan,
126            (true, true, true, false) => CrosstermColor::Grey,
127            (true, true, true, true) => CrosstermColor::White,
128        }
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn test_color_mode_default() {
138        assert_eq!(ColorMode::default(), ColorMode::TrueColor);
139    }
140
141    #[test]
142    fn test_truecolor_conversion() {
143        let mode = ColorMode::TrueColor;
144        let color = Color::new(0.5, 0.25, 0.75, 1.0);
145        let result = mode.to_crossterm(color);
146        assert_eq!(
147            result,
148            CrosstermColor::Rgb {
149                r: 128,
150                g: 64,
151                b: 191
152            }
153        );
154    }
155
156    #[test]
157    fn test_256_grayscale() {
158        // Pure black
159        assert_eq!(ColorMode::rgb_to_256(0, 0, 0), 16);
160        // Pure white
161        assert_eq!(ColorMode::rgb_to_256(255, 255, 255), 231);
162        // Mid gray
163        let mid = ColorMode::rgb_to_256(128, 128, 128);
164        assert!(mid >= 232);
165    }
166
167    #[test]
168    fn test_256_grayscale_near_black() {
169        // Very dark gray (still mapped to black)
170        assert_eq!(ColorMode::rgb_to_256(5, 5, 5), 16);
171    }
172
173    #[test]
174    fn test_256_grayscale_ramp() {
175        // Test various gray values
176        let gray50 = ColorMode::rgb_to_256(50, 50, 50);
177        assert!(gray50 >= 232);
178
179        let gray100 = ColorMode::rgb_to_256(100, 100, 100);
180        assert!(gray100 >= 232);
181
182        let gray200 = ColorMode::rgb_to_256(200, 200, 200);
183        assert!(gray200 >= 232);
184    }
185
186    #[test]
187    fn test_256_color_cube() {
188        // Pure red (should be in color cube)
189        let red = ColorMode::rgb_to_256(255, 0, 0);
190        assert!(red >= 16 && red <= 231);
191
192        // Pure green
193        let green = ColorMode::rgb_to_256(0, 255, 0);
194        assert!(green >= 16 && green <= 231);
195
196        // Pure blue
197        let blue = ColorMode::rgb_to_256(0, 0, 255);
198        assert!(blue >= 16 && blue <= 231);
199
200        // Magenta
201        let magenta = ColorMode::rgb_to_256(255, 0, 255);
202        assert!(magenta >= 16 && magenta <= 231);
203    }
204
205    #[test]
206    fn test_16_color_mapping() {
207        // Black
208        assert_eq!(ColorMode::rgb_to_16(0, 0, 0), CrosstermColor::Black);
209        // White
210        assert_eq!(ColorMode::rgb_to_16(255, 255, 255), CrosstermColor::White);
211        // Pure red
212        assert!(matches!(
213            ColorMode::rgb_to_16(255, 0, 0),
214            CrosstermColor::Red | CrosstermColor::DarkRed
215        ));
216    }
217
218    #[test]
219    fn test_16_color_green() {
220        let result = ColorMode::rgb_to_16(0, 255, 0);
221        assert!(matches!(
222            result,
223            CrosstermColor::Green | CrosstermColor::DarkGreen
224        ));
225    }
226
227    #[test]
228    fn test_16_color_blue() {
229        let result = ColorMode::rgb_to_16(0, 0, 255);
230        assert!(matches!(
231            result,
232            CrosstermColor::Blue | CrosstermColor::DarkBlue
233        ));
234    }
235
236    #[test]
237    fn test_16_color_yellow() {
238        let result = ColorMode::rgb_to_16(255, 255, 0);
239        assert!(matches!(
240            result,
241            CrosstermColor::Yellow | CrosstermColor::DarkYellow
242        ));
243    }
244
245    #[test]
246    fn test_16_color_cyan() {
247        let result = ColorMode::rgb_to_16(0, 255, 255);
248        assert!(matches!(
249            result,
250            CrosstermColor::Cyan | CrosstermColor::DarkCyan
251        ));
252    }
253
254    #[test]
255    fn test_16_color_magenta() {
256        let result = ColorMode::rgb_to_16(255, 0, 255);
257        assert!(matches!(
258            result,
259            CrosstermColor::Magenta | CrosstermColor::DarkMagenta
260        ));
261    }
262
263    #[test]
264    fn test_16_color_dark_gray() {
265        let result = ColorMode::rgb_to_16(50, 50, 50);
266        // With the luminance-based algorithm, dark gray maps to the dark variants
267        assert!(matches!(
268            result,
269            CrosstermColor::Black | CrosstermColor::DarkGrey | CrosstermColor::Grey
270        ));
271    }
272
273    #[test]
274    fn test_16_color_gray() {
275        let result = ColorMode::rgb_to_16(192, 192, 192);
276        assert!(matches!(
277            result,
278            CrosstermColor::Grey | CrosstermColor::White
279        ));
280    }
281
282    #[test]
283    fn test_mono_conversion() {
284        let mode = ColorMode::Mono;
285        assert_eq!(mode.to_crossterm(Color::RED), CrosstermColor::White);
286        assert_eq!(mode.to_crossterm(Color::BLUE), CrosstermColor::White);
287        assert_eq!(mode.to_crossterm(Color::GREEN), CrosstermColor::White);
288    }
289
290    #[test]
291    fn test_transparent_returns_reset() {
292        // CRITICAL: Transparent colors must return Reset, NOT black!
293        // This prevents the "black squares behind panels" bug.
294        for mode in [
295            ColorMode::TrueColor,
296            ColorMode::Color256,
297            ColorMode::Color16,
298            ColorMode::Mono,
299        ] {
300            assert_eq!(
301                mode.to_crossterm(Color::TRANSPARENT),
302                CrosstermColor::Reset,
303                "Mode {:?} should return Reset for TRANSPARENT",
304                mode
305            );
306
307            // Also test any color with alpha=0
308            let zero_alpha = Color::new(1.0, 0.5, 0.25, 0.0);
309            assert_eq!(
310                mode.to_crossterm(zero_alpha),
311                CrosstermColor::Reset,
312                "Mode {:?} should return Reset for any color with alpha=0",
313                mode
314            );
315        }
316    }
317
318    #[test]
319    fn test_256_conversion() {
320        let mode = ColorMode::Color256;
321        let result = mode.to_crossterm(Color::new(0.5, 0.25, 0.75, 1.0));
322        assert!(matches!(result, CrosstermColor::AnsiValue(_)));
323    }
324
325    #[test]
326    fn test_16_conversion() {
327        let mode = ColorMode::Color16;
328        let result = mode.to_crossterm(Color::RED);
329        assert!(matches!(
330            result,
331            CrosstermColor::Red | CrosstermColor::DarkRed
332        ));
333    }
334
335    #[test]
336    fn test_color_mode_eq() {
337        assert_eq!(ColorMode::TrueColor, ColorMode::TrueColor);
338        assert_eq!(ColorMode::Color256, ColorMode::Color256);
339        assert_eq!(ColorMode::Color16, ColorMode::Color16);
340        assert_eq!(ColorMode::Mono, ColorMode::Mono);
341        assert_ne!(ColorMode::TrueColor, ColorMode::Color256);
342    }
343
344    #[test]
345    fn test_color_mode_clone() {
346        let mode = ColorMode::Color256;
347        let cloned = mode;
348        assert_eq!(mode, cloned);
349    }
350
351    #[test]
352    fn test_color_mode_debug() {
353        let mode = ColorMode::TrueColor;
354        assert!(format!("{:?}", mode).contains("TrueColor"));
355    }
356
357    #[test]
358    fn test_16_color_dim_colors() {
359        // Dark red (dim)
360        let dark_red = ColorMode::rgb_to_16(128, 0, 0);
361        assert!(matches!(
362            dark_red,
363            CrosstermColor::Red | CrosstermColor::DarkRed
364        ));
365
366        // Dark green (dim)
367        let dark_green = ColorMode::rgb_to_16(0, 100, 0);
368        assert!(matches!(
369            dark_green,
370            CrosstermColor::Green | CrosstermColor::DarkGreen
371        ));
372
373        // Dark blue (dim)
374        let dark_blue = ColorMode::rgb_to_16(0, 0, 128);
375        assert!(matches!(
376            dark_blue,
377            CrosstermColor::Blue | CrosstermColor::DarkBlue
378        ));
379    }
380
381    #[test]
382    fn test_256_near_white() {
383        // Near white (should be in grayscale ramp, near 231)
384        let near_white = ColorMode::rgb_to_256(250, 250, 250);
385        assert_eq!(near_white, 231);
386    }
387
388    #[test]
389    fn test_256_mixed_colors() {
390        // Orange-ish
391        let orange = ColorMode::rgb_to_256(255, 128, 0);
392        assert!(orange >= 16 && orange <= 231);
393
394        // Purple-ish
395        let purple = ColorMode::rgb_to_256(128, 0, 255);
396        assert!(purple >= 16 && purple <= 231);
397
398        // Teal-ish
399        let teal = ColorMode::rgb_to_256(0, 128, 128);
400        assert!(teal >= 16 && teal <= 231);
401    }
402
403    // Additional tests for better coverage
404
405    #[test]
406    fn test_color_mode_detect() {
407        // Just verify it doesn't panic and returns a valid mode
408        let mode = ColorMode::detect();
409        assert!(matches!(
410            mode,
411            ColorMode::TrueColor | ColorMode::Color256 | ColorMode::Color16 | ColorMode::Mono
412        ));
413    }
414
415    #[test]
416    fn test_16_color_all_dark_variants() {
417        // Test dark variants explicitly by keeping luminance low
418
419        // Dark red - high red, low luminance
420        let dark_red = ColorMode::rgb_to_16(180, 20, 20);
421        assert!(matches!(
422            dark_red,
423            CrosstermColor::DarkRed | CrosstermColor::Red
424        ));
425
426        // Dark green
427        let dark_green = ColorMode::rgb_to_16(20, 150, 20);
428        assert!(matches!(
429            dark_green,
430            CrosstermColor::DarkGreen | CrosstermColor::Green
431        ));
432
433        // Dark blue
434        let dark_blue = ColorMode::rgb_to_16(20, 20, 180);
435        assert!(matches!(
436            dark_blue,
437            CrosstermColor::DarkBlue | CrosstermColor::Blue
438        ));
439
440        // Dark yellow
441        let dark_yellow = ColorMode::rgb_to_16(150, 150, 20);
442        assert!(matches!(
443            dark_yellow,
444            CrosstermColor::DarkYellow | CrosstermColor::Yellow
445        ));
446
447        // Dark cyan
448        let dark_cyan = ColorMode::rgb_to_16(20, 150, 150);
449        assert!(matches!(
450            dark_cyan,
451            CrosstermColor::DarkCyan | CrosstermColor::Cyan
452        ));
453
454        // Dark magenta
455        let dark_magenta = ColorMode::rgb_to_16(150, 20, 150);
456        assert!(matches!(
457            dark_magenta,
458            CrosstermColor::DarkMagenta | CrosstermColor::Magenta
459        ));
460    }
461
462    #[test]
463    fn test_16_color_bright_variants() {
464        // Test bright variants - verifies the function returns valid colors
465        // The exact mapping depends on the threshold algorithm
466
467        // Bright red
468        let bright_red = ColorMode::rgb_to_16(255, 50, 50);
469        assert!(!matches!(bright_red, CrosstermColor::Black));
470
471        // Bright green
472        let bright_green = ColorMode::rgb_to_16(50, 255, 50);
473        assert!(!matches!(bright_green, CrosstermColor::Black));
474
475        // Bright blue
476        let bright_blue = ColorMode::rgb_to_16(50, 50, 255);
477        assert!(!matches!(bright_blue, CrosstermColor::Black));
478    }
479
480    #[test]
481    fn test_16_color_dark_grey_explicit() {
482        // Dark grey: no dominant color, but luminance > threshold for DarkGrey
483        let dark_grey = ColorMode::rgb_to_16(80, 80, 80);
484        assert!(matches!(
485            dark_grey,
486            CrosstermColor::DarkGrey | CrosstermColor::Black | CrosstermColor::Grey
487        ));
488    }
489
490    #[test]
491    fn test_to_crossterm_edge_values() {
492        // Test edge values for color conversion
493        let mode = ColorMode::TrueColor;
494
495        // Black
496        let black = mode.to_crossterm(Color::new(0.0, 0.0, 0.0, 1.0));
497        assert_eq!(black, CrosstermColor::Rgb { r: 0, g: 0, b: 0 });
498
499        // White
500        let white = mode.to_crossterm(Color::new(1.0, 1.0, 1.0, 1.0));
501        assert_eq!(
502            white,
503            CrosstermColor::Rgb {
504                r: 255,
505                g: 255,
506                b: 255
507            }
508        );
509    }
510
511    #[test]
512    fn test_256_grayscale_boundary() {
513        // Test grayscale at various boundaries
514        assert_eq!(ColorMode::rgb_to_256(7, 7, 7), 16); // < 8, should be black
515        assert_eq!(ColorMode::rgb_to_256(8, 8, 8), 232); // >= 8, first grayscale
516        assert_eq!(ColorMode::rgb_to_256(249, 249, 249), 231); // > 248, white
517    }
518
519    #[test]
520    fn test_256_color_cube_corners() {
521        // Test color cube corner values
522        // (0,0,0) in cube
523        let c000 = ColorMode::rgb_to_256(1, 1, 2); // Not grayscale, maps to cube
524        assert!(c000 >= 16 && c000 <= 231);
525
526        // (5,5,5) in cube = 16 + 36*5 + 6*5 + 5 = 16 + 180 + 30 + 5 = 231
527        let c555 = ColorMode::rgb_to_256(254, 254, 255); // Max non-grayscale
528        assert!(c555 >= 16 && c555 <= 231);
529    }
530
531    #[test]
532    fn test_color16_to_crossterm() {
533        let mode = ColorMode::Color16;
534
535        // Various colors through the mode
536        let red = mode.to_crossterm(Color::RED);
537        assert!(matches!(red, CrosstermColor::Red | CrosstermColor::DarkRed));
538
539        let green = mode.to_crossterm(Color::GREEN);
540        assert!(matches!(
541            green,
542            CrosstermColor::Green | CrosstermColor::DarkGreen
543        ));
544
545        let blue = mode.to_crossterm(Color::BLUE);
546        assert!(matches!(
547            blue,
548            CrosstermColor::Blue | CrosstermColor::DarkBlue
549        ));
550
551        let black = mode.to_crossterm(Color::BLACK);
552        assert!(matches!(black, CrosstermColor::Black));
553
554        let white = mode.to_crossterm(Color::WHITE);
555        assert!(matches!(white, CrosstermColor::White));
556    }
557
558    #[test]
559    fn test_color256_grayscale_through_mode() {
560        let mode = ColorMode::Color256;
561
562        // Black
563        let black = mode.to_crossterm(Color::BLACK);
564        assert!(matches!(black, CrosstermColor::AnsiValue(16)));
565
566        // Mid gray
567        let gray = mode.to_crossterm(Color::new(0.5, 0.5, 0.5, 1.0));
568        if let CrosstermColor::AnsiValue(v) = gray {
569            assert!(v >= 232 || (v >= 16 && v <= 231));
570        }
571    }
572
573    #[test]
574    fn test_rgb_to_256_extensive() {
575        // Test various color combinations to ensure full cube coverage
576        for r in [0, 51, 102, 153, 204, 255] {
577            for g in [0, 51, 102, 153, 204, 255] {
578                for b in [0, 51, 102, 153, 204, 255] {
579                    let result = ColorMode::rgb_to_256(r, g, b);
580                    // Result should always be in valid range
581                    assert!(result <= 255);
582                }
583            }
584        }
585    }
586
587    #[test]
588    fn test_rgb_to_16_extensive() {
589        // Test various color combinations
590        for r in [0, 64, 128, 192, 255] {
591            for g in [0, 64, 128, 192, 255] {
592                for b in [0, 64, 128, 192, 255] {
593                    let result = ColorMode::rgb_to_16(r, g, b);
594                    // Just verify it returns a valid CrosstermColor
595                    let _ = format!("{:?}", result);
596                }
597            }
598        }
599    }
600
601    #[test]
602    fn test_to_crossterm_all_modes() {
603        let test_colors = [
604            Color::BLACK,
605            Color::WHITE,
606            Color::RED,
607            Color::GREEN,
608            Color::BLUE,
609            Color::new(0.5, 0.5, 0.5, 1.0),
610            Color::new(0.25, 0.75, 0.5, 1.0),
611        ];
612
613        for mode in [
614            ColorMode::TrueColor,
615            ColorMode::Color256,
616            ColorMode::Color16,
617            ColorMode::Mono,
618        ] {
619            for color in &test_colors {
620                let result = mode.to_crossterm(*color);
621                // Verify it produces a valid result
622                let _ = format!("{:?}", result);
623            }
624        }
625    }
626
627    #[test]
628    fn test_grayscale_ramp_comprehensive() {
629        // Test the full grayscale ramp
630        for gray in 0..=255 {
631            let result = ColorMode::rgb_to_256(gray, gray, gray);
632            // Should be either 16 (black), 231 (white), or in grayscale range 232-255
633            assert!(result == 16 || result == 231 || (result >= 232 && result <= 255));
634        }
635    }
636
637    #[test]
638    fn test_detect_returns_valid() {
639        // Calling detect() should return one of the valid modes
640        // The exact result depends on the environment
641        let mode = ColorMode::detect();
642        match mode {
643            ColorMode::TrueColor => assert!(true),
644            ColorMode::Color256 => assert!(true),
645            ColorMode::Color16 => assert!(true),
646            ColorMode::Mono => assert!(true),
647        }
648    }
649
650    #[test]
651    fn test_color_mode_copy() {
652        // ColorMode should be Copy
653        let mode1 = ColorMode::TrueColor;
654        let mode2 = mode1; // Copy
655        assert_eq!(mode1, mode2);
656    }
657
658    // Tests for detect_with_env - all branches
659
660    #[test]
661    fn test_detect_colorterm_truecolor() {
662        let mode = ColorMode::detect_with_env(Some("truecolor".to_string()), None);
663        assert_eq!(mode, ColorMode::TrueColor);
664    }
665
666    #[test]
667    fn test_detect_colorterm_24bit() {
668        let mode = ColorMode::detect_with_env(Some("24bit".to_string()), None);
669        assert_eq!(mode, ColorMode::TrueColor);
670    }
671
672    #[test]
673    fn test_detect_colorterm_other_falls_through() {
674        // COLORTERM set but not truecolor/24bit - should fall through to TERM
675        let mode = ColorMode::detect_with_env(
676            Some("other".to_string()),
677            Some("xterm-256color".to_string()),
678        );
679        assert_eq!(mode, ColorMode::Color256);
680    }
681
682    #[test]
683    fn test_detect_term_256color() {
684        let mode = ColorMode::detect_with_env(None, Some("xterm-256color".to_string()));
685        assert_eq!(mode, ColorMode::Color256);
686
687        let mode2 = ColorMode::detect_with_env(None, Some("screen-256color".to_string()));
688        assert_eq!(mode2, ColorMode::Color256);
689    }
690
691    #[test]
692    fn test_detect_term_xterm() {
693        let mode = ColorMode::detect_with_env(None, Some("xterm".to_string()));
694        assert_eq!(mode, ColorMode::Color16);
695    }
696
697    #[test]
698    fn test_detect_term_color() {
699        let mode = ColorMode::detect_with_env(None, Some("linux-color".to_string()));
700        assert_eq!(mode, ColorMode::Color16);
701    }
702
703    #[test]
704    fn test_detect_term_dumb() {
705        let mode = ColorMode::detect_with_env(None, Some("dumb".to_string()));
706        assert_eq!(mode, ColorMode::Mono);
707    }
708
709    #[test]
710    fn test_detect_term_none() {
711        let mode = ColorMode::detect_with_env(None, None);
712        assert_eq!(mode, ColorMode::Mono);
713    }
714
715    #[test]
716    fn test_detect_term_unknown() {
717        // Unknown TERM value should default to Color16
718        let mode = ColorMode::detect_with_env(None, Some("vt100".to_string()));
719        assert_eq!(mode, ColorMode::Color16);
720    }
721
722    #[test]
723    fn test_detect_colorterm_priority() {
724        // COLORTERM should take priority over TERM
725        let mode =
726            ColorMode::detect_with_env(Some("truecolor".to_string()), Some("dumb".to_string()));
727        assert_eq!(mode, ColorMode::TrueColor);
728    }
729
730    #[test]
731    fn test_detect_colorterm_empty_string() {
732        // Empty COLORTERM string should fall through
733        let mode = ColorMode::detect_with_env(Some("".to_string()), None);
734        assert_eq!(mode, ColorMode::Mono);
735    }
736
737    #[test]
738    fn test_detect_term_various() {
739        // Test various TERM values
740        assert_eq!(
741            ColorMode::detect_with_env(None, Some("rxvt-256color".to_string())),
742            ColorMode::Color256
743        );
744        assert_eq!(
745            ColorMode::detect_with_env(None, Some("screen".to_string())),
746            ColorMode::Color16
747        );
748        assert_eq!(
749            ColorMode::detect_with_env(None, Some("ansi".to_string())),
750            ColorMode::Color16
751        );
752    }
753
754    #[test]
755    fn test_detect_colorterm_with_term_fallback() {
756        // Non-truecolor COLORTERM with TERM fallback
757        let mode =
758            ColorMode::detect_with_env(Some("something".to_string()), Some("xterm".to_string()));
759        assert_eq!(mode, ColorMode::Color16);
760    }
761
762    #[test]
763    fn test_to_crossterm_comprehensive() {
764        // Test all modes with a variety of colors
765        let colors = [
766            Color::new(0.0, 0.0, 0.0, 1.0),
767            Color::new(1.0, 1.0, 1.0, 1.0),
768            Color::new(1.0, 0.0, 0.0, 1.0),
769            Color::new(0.0, 1.0, 0.0, 1.0),
770            Color::new(0.0, 0.0, 1.0, 1.0),
771            Color::new(0.5, 0.5, 0.5, 1.0),
772            Color::new(0.25, 0.5, 0.75, 1.0),
773            Color::new(0.1, 0.2, 0.3, 1.0),
774        ];
775
776        for color in colors {
777            for mode in [
778                ColorMode::TrueColor,
779                ColorMode::Color256,
780                ColorMode::Color16,
781                ColorMode::Mono,
782            ] {
783                let _ = mode.to_crossterm(color);
784            }
785        }
786    }
787
788    #[test]
789    fn test_rgb_to_256_boundary_values() {
790        // Test at exact color cube boundaries
791        for v in [0, 51, 102, 153, 204, 255] {
792            let _ = ColorMode::rgb_to_256(v, 0, 0);
793            let _ = ColorMode::rgb_to_256(0, v, 0);
794            let _ = ColorMode::rgb_to_256(0, 0, v);
795        }
796    }
797
798    #[test]
799    fn test_rgb_to_16_all_combinations() {
800        // Test all 16 possible combinations of has_r, has_g, has_b, bright
801        let test_cases = [
802            (0, 0, 0),       // Black
803            (50, 50, 50),    // DarkGrey
804            (128, 0, 0),     // DarkRed
805            (255, 0, 0),     // Red
806            (0, 128, 0),     // DarkGreen
807            (0, 255, 0),     // Green
808            (128, 128, 0),   // DarkYellow
809            (255, 255, 0),   // Yellow
810            (0, 0, 128),     // DarkBlue
811            (0, 0, 255),     // Blue
812            (128, 0, 128),   // DarkMagenta
813            (255, 0, 255),   // Magenta
814            (0, 128, 128),   // DarkCyan
815            (0, 255, 255),   // Cyan
816            (192, 192, 192), // Grey
817            (255, 255, 255), // White
818        ];
819
820        for (r, g, b) in test_cases {
821            let _ = ColorMode::rgb_to_16(r, g, b);
822        }
823    }
824
825    #[test]
826    fn test_color_lerp_boundary() {
827        // Test lerp with boundary values
828        let c1 = Color::RED;
829        let c2 = Color::BLUE;
830        let _ = c1.lerp(&c2, 0.0);
831        let _ = c1.lerp(&c2, 1.0);
832        let _ = c1.lerp(&c2, 0.5);
833    }
834
835    #[test]
836    fn test_detect_original_still_works() {
837        // Ensure the original detect() still works
838        let _ = ColorMode::detect();
839    }
840}