Skip to main content

codetether_agent/tui/
theme_utils.rs

1//! Theme utilities for color support detection and validation
2
3use crate::tui::theme::{ColorDef, Theme};
4
5/// Check terminal color support via environment detection
6pub fn detect_color_support() -> ColorSupport {
7    // Check COLORTERM for truecolor
8    if let Ok(colorterm) = std::env::var("COLORTERM") {
9        if colorterm == "truecolor" || colorterm == "24bit" {
10            return ColorSupport::TrueColor;
11        }
12    }
13
14    // Check TERM for 256 color support
15    if let Ok(term) = std::env::var("TERM") {
16        if term.contains("256color") || term.contains("256") {
17            return ColorSupport::Ansi256;
18        }
19        if term.contains("color") || term.starts_with("xterm") || term.starts_with("screen") {
20            return ColorSupport::Ansi8;
21        }
22    }
23
24    // Default to 8 colors (most common minimum)
25    ColorSupport::Ansi8
26}
27
28/// Color support levels
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum ColorSupport {
31    Monochrome,
32    Ansi8,
33    Ansi256,
34    TrueColor,
35}
36
37impl ColorSupport {
38    pub fn supports_rgb(&self) -> bool {
39        matches!(self, ColorSupport::TrueColor)
40    }
41
42    pub fn supports_indexed(&self) -> bool {
43        matches!(self, ColorSupport::Ansi256 | ColorSupport::TrueColor)
44    }
45
46    pub fn supports_named(&self) -> bool {
47        !matches!(self, ColorSupport::Monochrome)
48    }
49}
50
51/// Validate and adjust theme based on terminal capabilities
52pub fn validate_theme(theme: &Theme) -> Theme {
53    let support = detect_color_support();
54    let mut validated = theme.clone();
55
56    if !support.supports_rgb() {
57        // Convert RGB colors to closest named colors
58        validated.user_color = fallback_color(&theme.user_color, &support);
59        validated.assistant_color = fallback_color(&theme.assistant_color, &support);
60        validated.system_color = fallback_color(&theme.system_color, &support);
61        validated.tool_color = fallback_color(&theme.tool_color, &support);
62        validated.error_color = fallback_color(&theme.error_color, &support);
63        validated.border_color = fallback_color(&theme.border_color, &support);
64        validated.input_border_color = fallback_color(&theme.input_border_color, &support);
65        validated.help_border_color = fallback_color(&theme.help_border_color, &support);
66        validated.timestamp_color = fallback_color(&theme.timestamp_color, &support);
67        validated.code_block_color = fallback_color(&theme.code_block_color, &support);
68        validated.status_bar_foreground = fallback_color(&theme.status_bar_foreground, &support);
69        validated.status_bar_background = fallback_color(&theme.status_bar_background, &support);
70
71        if let Some(bg) = &theme.background {
72            validated.background = Some(fallback_color(bg, &support));
73        }
74    }
75
76    validated
77}
78
79/// Convert colors to fallback based on terminal support
80fn fallback_color(color: &ColorDef, support: &ColorSupport) -> ColorDef {
81    match color {
82        ColorDef::Rgb(r, g, b) => {
83            if support.supports_rgb() {
84                ColorDef::Rgb(*r, *g, *b)
85            } else if support.supports_indexed() {
86                // Convert to closest indexed color
87                ColorDef::Indexed(rgb_to_ansi256(*r, *g, *b))
88            } else {
89                // Convert to closest named color
90                ColorDef::Named(rgb_to_named(*r, *g, *b))
91            }
92        }
93        ColorDef::Indexed(idx) => {
94            if support.supports_indexed() {
95                ColorDef::Indexed(*idx)
96            } else {
97                // Convert to named color
98                ColorDef::Named(indexed_to_named(*idx))
99            }
100        }
101        ColorDef::Named(name) => {
102            if support.supports_named() {
103                ColorDef::Named(name.clone())
104            } else {
105                // Fallback to safe defaults
106                ColorDef::Named("white".to_string())
107            }
108        }
109    }
110}
111
112/// Convert RGB to closest ANSI 256-color index
113fn rgb_to_ansi256(r: u8, g: u8, b: u8) -> u8 {
114    // Standard 256-color conversion
115    if r == g && g == b {
116        // Grayscale
117        if r < 8 {
118            16
119        } else if r > 248 {
120            231
121        } else {
122            232 + ((r - 8) / 10)
123        }
124    } else {
125        // Color
126        16 + (36 * (r / 51)) + (6 * (g / 51)) + (b / 51)
127    }
128}
129
130/// Convert RGB to closest named color
131fn rgb_to_named(r: u8, g: u8, b: u8) -> String {
132    // Simple mapping to standard 8 colors
133    let colors = [
134        ("black", (0, 0, 0)),
135        ("red", (128, 0, 0)),
136        ("green", (0, 128, 0)),
137        ("yellow", (128, 128, 0)),
138        ("blue", (0, 0, 128)),
139        ("magenta", (128, 0, 128)),
140        ("cyan", (0, 128, 128)),
141        ("white", (192, 192, 192)),
142    ];
143
144    let mut closest = "white";
145    let mut min_distance = u32::MAX;
146
147    for (name, (cr, cg, cb)) in colors.iter() {
148        let dr = (r as i32 - *cr as i32).unsigned_abs();
149        let dg = (g as i32 - *cg as i32).unsigned_abs();
150        let db = (b as i32 - *cb as i32).unsigned_abs();
151        let distance = dr * dr + dg * dg + db * db;
152
153        if distance < min_distance {
154            min_distance = distance;
155            closest = *name;
156        }
157    }
158
159    closest.to_string()
160}
161
162/// Convert indexed color to named color
163fn indexed_to_named(index: u8) -> String {
164    match index {
165        0..=15 => {
166            // Standard 16 colors
167            match index {
168                0 => "black",
169                1 => "red",
170                2 => "green",
171                3 => "yellow",
172                4 => "blue",
173                5 => "magenta",
174                6 => "cyan",
175                7 => "white",
176                8 => "darkgray",
177                9 => "lightred",
178                10 => "lightgreen",
179                11 => "lightyellow",
180                12 => "lightblue",
181                13 => "lightmagenta",
182                14 => "lightcyan",
183                15 => "lightgray",
184                _ => "white",
185            }
186        }
187        _ => "white",
188    }
189    .to_string()
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn test_color_support_detection() {
198        let support = detect_color_support();
199        // Just ensure it doesn't panic
200        assert!(matches!(
201            support,
202            ColorSupport::Monochrome
203                | ColorSupport::Ansi8
204                | ColorSupport::Ansi256
205                | ColorSupport::TrueColor
206        ));
207    }
208
209    #[test]
210    fn test_fallback_color() {
211        let support = ColorSupport::Ansi8;
212        let rgb_color = ColorDef::Rgb(255, 0, 0);
213        let fallback = fallback_color(&rgb_color, &support);
214        assert!(matches!(fallback, ColorDef::Named(_)));
215    }
216}