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!((16..=231).contains(&red));
191
192        // Pure green
193        let green = ColorMode::rgb_to_256(0, 255, 0);
194        assert!((16..=231).contains(&green));
195
196        // Pure blue
197        let blue = ColorMode::rgb_to_256(0, 0, 255);
198        assert!((16..=231).contains(&blue));
199
200        // Magenta
201        let magenta = ColorMode::rgb_to_256(255, 0, 255);
202        assert!((16..=231).contains(&magenta));
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!((16..=231).contains(&orange));
393
394        // Purple-ish
395        let purple = ColorMode::rgb_to_256(128, 0, 255);
396        assert!((16..=231).contains(&purple));
397
398        // Teal-ish
399        let teal = ColorMode::rgb_to_256(0, 128, 128);
400        assert!((16..=231).contains(&teal));
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!((16..=231).contains(&c000));
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!((16..=231).contains(&c555));
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 || ((16..=231).contains(&v)));
570        }
571    }
572
573    #[test]
574    #[allow(clippy::absurd_extreme_comparisons)]
575    fn test_rgb_to_256_extensive() {
576        // Test various color combinations to ensure full cube coverage
577        for r in [0, 51, 102, 153, 204, 255] {
578            for g in [0, 51, 102, 153, 204, 255] {
579                for b in [0, 51, 102, 153, 204, 255] {
580                    let result = ColorMode::rgb_to_256(r, g, b);
581                    // Result should be a valid ANSI color (u8)
582                    let _ = result;
583                }
584            }
585        }
586    }
587
588    #[test]
589    fn test_rgb_to_16_extensive() {
590        // Test various color combinations
591        for r in [0, 64, 128, 192, 255] {
592            for g in [0, 64, 128, 192, 255] {
593                for b in [0, 64, 128, 192, 255] {
594                    let result = ColorMode::rgb_to_16(r, g, b);
595                    // Just verify it returns a valid CrosstermColor
596                    let _ = format!("{:?}", result);
597                }
598            }
599        }
600    }
601
602    #[test]
603    fn test_to_crossterm_all_modes() {
604        let test_colors = [
605            Color::BLACK,
606            Color::WHITE,
607            Color::RED,
608            Color::GREEN,
609            Color::BLUE,
610            Color::new(0.5, 0.5, 0.5, 1.0),
611            Color::new(0.25, 0.75, 0.5, 1.0),
612        ];
613
614        for mode in [
615            ColorMode::TrueColor,
616            ColorMode::Color256,
617            ColorMode::Color16,
618            ColorMode::Mono,
619        ] {
620            for color in &test_colors {
621                let result = mode.to_crossterm(*color);
622                // Verify it produces a valid result
623                let _ = format!("{:?}", result);
624            }
625        }
626    }
627
628    #[test]
629    #[allow(clippy::absurd_extreme_comparisons)]
630    fn test_grayscale_ramp_comprehensive() {
631        // Test the full grayscale ramp
632        for gray in 0..=255 {
633            let result = ColorMode::rgb_to_256(gray, gray, gray);
634            // Should be either 16 (black), 231 (white), or in grayscale range 232-255
635            assert!(result == 16 || result == 231 || ((232..=255).contains(&result)));
636        }
637    }
638
639    #[test]
640    #[allow(clippy::assertions_on_constants)]
641    fn test_detect_returns_valid() {
642        // Calling detect() should return one of the valid modes
643        // The exact result depends on the environment
644        let mode = ColorMode::detect();
645        match mode {
646            ColorMode::TrueColor => assert!(true),
647            ColorMode::Color256 => assert!(true),
648            ColorMode::Color16 => assert!(true),
649            ColorMode::Mono => assert!(true),
650        }
651    }
652
653    #[test]
654    fn test_color_mode_copy() {
655        // ColorMode should be Copy
656        let mode1 = ColorMode::TrueColor;
657        let mode2 = mode1; // Copy
658        assert_eq!(mode1, mode2);
659    }
660
661    // Tests for detect_with_env - all branches
662
663    #[test]
664    fn test_detect_colorterm_truecolor() {
665        let mode = ColorMode::detect_with_env(Some("truecolor".to_string()), None);
666        assert_eq!(mode, ColorMode::TrueColor);
667    }
668
669    #[test]
670    fn test_detect_colorterm_24bit() {
671        let mode = ColorMode::detect_with_env(Some("24bit".to_string()), None);
672        assert_eq!(mode, ColorMode::TrueColor);
673    }
674
675    #[test]
676    fn test_detect_colorterm_other_falls_through() {
677        // COLORTERM set but not truecolor/24bit - should fall through to TERM
678        let mode = ColorMode::detect_with_env(
679            Some("other".to_string()),
680            Some("xterm-256color".to_string()),
681        );
682        assert_eq!(mode, ColorMode::Color256);
683    }
684
685    #[test]
686    fn test_detect_term_256color() {
687        let mode = ColorMode::detect_with_env(None, Some("xterm-256color".to_string()));
688        assert_eq!(mode, ColorMode::Color256);
689
690        let mode2 = ColorMode::detect_with_env(None, Some("screen-256color".to_string()));
691        assert_eq!(mode2, ColorMode::Color256);
692    }
693
694    #[test]
695    fn test_detect_term_xterm() {
696        let mode = ColorMode::detect_with_env(None, Some("xterm".to_string()));
697        assert_eq!(mode, ColorMode::Color16);
698    }
699
700    #[test]
701    fn test_detect_term_color() {
702        let mode = ColorMode::detect_with_env(None, Some("linux-color".to_string()));
703        assert_eq!(mode, ColorMode::Color16);
704    }
705
706    #[test]
707    fn test_detect_term_dumb() {
708        let mode = ColorMode::detect_with_env(None, Some("dumb".to_string()));
709        assert_eq!(mode, ColorMode::Mono);
710    }
711
712    #[test]
713    fn test_detect_term_none() {
714        let mode = ColorMode::detect_with_env(None, None);
715        assert_eq!(mode, ColorMode::Mono);
716    }
717
718    #[test]
719    fn test_detect_term_unknown() {
720        // Unknown TERM value should default to Color16
721        let mode = ColorMode::detect_with_env(None, Some("vt100".to_string()));
722        assert_eq!(mode, ColorMode::Color16);
723    }
724
725    #[test]
726    fn test_detect_colorterm_priority() {
727        // COLORTERM should take priority over TERM
728        let mode =
729            ColorMode::detect_with_env(Some("truecolor".to_string()), Some("dumb".to_string()));
730        assert_eq!(mode, ColorMode::TrueColor);
731    }
732
733    #[test]
734    #[allow(clippy::manual_string_new)]
735    fn test_detect_colorterm_empty_string() {
736        // Empty COLORTERM string should fall through
737        let mode = ColorMode::detect_with_env(Some("".to_string()), None);
738        assert_eq!(mode, ColorMode::Mono);
739    }
740
741    #[test]
742    fn test_detect_term_various() {
743        // Test various TERM values
744        assert_eq!(
745            ColorMode::detect_with_env(None, Some("rxvt-256color".to_string())),
746            ColorMode::Color256
747        );
748        assert_eq!(
749            ColorMode::detect_with_env(None, Some("screen".to_string())),
750            ColorMode::Color16
751        );
752        assert_eq!(
753            ColorMode::detect_with_env(None, Some("ansi".to_string())),
754            ColorMode::Color16
755        );
756    }
757
758    #[test]
759    fn test_detect_colorterm_with_term_fallback() {
760        // Non-truecolor COLORTERM with TERM fallback
761        let mode =
762            ColorMode::detect_with_env(Some("something".to_string()), Some("xterm".to_string()));
763        assert_eq!(mode, ColorMode::Color16);
764    }
765
766    #[test]
767    fn test_to_crossterm_comprehensive() {
768        // Test all modes with a variety of colors
769        let colors = [
770            Color::new(0.0, 0.0, 0.0, 1.0),
771            Color::new(1.0, 1.0, 1.0, 1.0),
772            Color::new(1.0, 0.0, 0.0, 1.0),
773            Color::new(0.0, 1.0, 0.0, 1.0),
774            Color::new(0.0, 0.0, 1.0, 1.0),
775            Color::new(0.5, 0.5, 0.5, 1.0),
776            Color::new(0.25, 0.5, 0.75, 1.0),
777            Color::new(0.1, 0.2, 0.3, 1.0),
778        ];
779
780        for color in colors {
781            for mode in [
782                ColorMode::TrueColor,
783                ColorMode::Color256,
784                ColorMode::Color16,
785                ColorMode::Mono,
786            ] {
787                let _ = mode.to_crossterm(color);
788            }
789        }
790    }
791
792    #[test]
793    fn test_rgb_to_256_boundary_values() {
794        // Test at exact color cube boundaries
795        for v in [0, 51, 102, 153, 204, 255] {
796            let _ = ColorMode::rgb_to_256(v, 0, 0);
797            let _ = ColorMode::rgb_to_256(0, v, 0);
798            let _ = ColorMode::rgb_to_256(0, 0, v);
799        }
800    }
801
802    #[test]
803    fn test_rgb_to_16_all_combinations() {
804        // Test all 16 possible combinations of has_r, has_g, has_b, bright
805        let test_cases = [
806            (0, 0, 0),       // Black
807            (50, 50, 50),    // DarkGrey
808            (128, 0, 0),     // DarkRed
809            (255, 0, 0),     // Red
810            (0, 128, 0),     // DarkGreen
811            (0, 255, 0),     // Green
812            (128, 128, 0),   // DarkYellow
813            (255, 255, 0),   // Yellow
814            (0, 0, 128),     // DarkBlue
815            (0, 0, 255),     // Blue
816            (128, 0, 128),   // DarkMagenta
817            (255, 0, 255),   // Magenta
818            (0, 128, 128),   // DarkCyan
819            (0, 255, 255),   // Cyan
820            (192, 192, 192), // Grey
821            (255, 255, 255), // White
822        ];
823
824        for (r, g, b) in test_cases {
825            let _ = ColorMode::rgb_to_16(r, g, b);
826        }
827    }
828
829    #[test]
830    fn test_color_lerp_boundary() {
831        // Test lerp with boundary values
832        let c1 = Color::RED;
833        let c2 = Color::BLUE;
834        let _ = c1.lerp(&c2, 0.0);
835        let _ = c1.lerp(&c2, 1.0);
836        let _ = c1.lerp(&c2, 0.5);
837    }
838
839    #[test]
840    fn test_detect_original_still_works() {
841        // Ensure the original detect() still works
842        let _ = ColorMode::detect();
843    }
844}