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)]
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
51pub 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 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
79fn 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 ColorDef::Indexed(rgb_to_ansi256(*r, *g, *b))
88 } else {
89 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 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 ColorDef::Named("white".to_string())
107 }
108 }
109 }
110}
111
112fn rgb_to_ansi256(r: u8, g: u8, b: u8) -> u8 {
114 if r == g && g == b {
116 if r < 8 {
118 16
119 } else if r > 248 {
120 231
121 } else {
122 232 + ((r - 8) / 10)
123 }
124 } else {
125 16 + (36 * (r / 51)) + (6 * (g / 51)) + (b / 51)
127 }
128}
129
130fn rgb_to_named(r: u8, g: u8, b: u8) -> String {
132 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
162fn indexed_to_named(index: u8) -> String {
164 match index {
165 0..=15 => {
166 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 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}