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