syncable_cli/analyzer/display/
color_adapter.rs

1//! Color adaptation for different terminal backgrounds
2//!
3//! This module provides color schemes that work well on both light and dark terminal backgrounds,
4//! ensuring good readability regardless of the user's terminal theme.
5
6use colored::*;
7use std::env;
8
9/// Represents the detected or configured terminal background type
10#[derive(Debug, Clone, Copy, PartialEq)]
11pub enum ColorScheme {
12    /// Dark background terminal (default assumption)
13    Dark,
14    /// Light background terminal
15    Light,
16}
17
18/// Color adapter that provides appropriate colors based on terminal background
19#[derive(Debug, Clone)]
20pub struct ColorAdapter {
21    scheme: ColorScheme,
22}
23
24impl ColorAdapter {
25    /// Create a new ColorAdapter with automatic background detection
26    pub fn new() -> Self {
27        Self {
28            scheme: Self::detect_terminal_background(),
29        }
30    }
31
32    /// Create a ColorAdapter with a specific color scheme
33    pub fn with_scheme(scheme: ColorScheme) -> Self {
34        Self { scheme }
35    }
36
37    /// Detect terminal background based on environment variables and heuristics
38    fn detect_terminal_background() -> ColorScheme {
39        // Check COLORFGBG environment variable (format: "foreground;background")
40        if let Ok(colorfgbg) = env::var("COLORFGBG")
41            && let Some(bg_str) = colorfgbg.split(';').nth(1)
42            && let Ok(bg_code) = bg_str.parse::<u8>()
43        {
44            // Background colors 0-6 are dark, 7-15 are light/bright
45            // Be more aggressive about detecting light backgrounds
46            return if bg_code >= 7 {
47                ColorScheme::Light
48            } else {
49                ColorScheme::Dark
50            };
51        }
52
53        // Check for common light terminal setups
54        if let Ok(term_program) = env::var("TERM_PROGRAM") {
55            match term_program.as_str() {
56                "Apple_Terminal" => {
57                    // macOS Terminal.app - check for light theme indicators
58                    // Many users have light themes, so be more aggressive
59                    if let Ok(_term_session_id) = env::var("TERM_SESSION_ID") {
60                        // If we can't detect definitively, assume light for Terminal.app
61                        // since many users use the default light theme
62                        return ColorScheme::Light;
63                    }
64                    return ColorScheme::Light; // Default to light for Terminal.app
65                }
66                "iTerm.app" => {
67                    // iTerm2 - check for theme hints
68                    if let Ok(_iterm_session_id) = env::var("ITERM_SESSION_ID") {
69                        // Default to dark for iTerm as it's more commonly used with dark themes
70                        return ColorScheme::Dark;
71                    }
72                }
73                "vscode" | "code" => {
74                    // VS Code integrated terminal - often follows editor theme
75                    // VS Code light themes are common
76                    return ColorScheme::Light;
77                }
78                _ => {}
79            }
80        }
81
82        // Check terminal type and name hints
83        if let Ok(term) = env::var("TERM") {
84            match term.as_str() {
85                term if term.contains("light") => return ColorScheme::Light,
86                term if term.contains("256color") => {
87                    // Modern terminals with 256 color support
88                    // Check other indicators
89                    if env::var("TERM_PROGRAM")
90                        .unwrap_or_default()
91                        .contains("Terminal")
92                    {
93                        return ColorScheme::Light; // macOS Terminal default
94                    }
95                }
96                _ => {}
97            }
98        }
99
100        // Check for SSH session - often indicates server/dark environment
101        if env::var("SSH_CONNECTION").is_ok() || env::var("SSH_CLIENT").is_ok() {
102            return ColorScheme::Dark;
103        }
104
105        // Check background color hints from other variables
106        if let Ok(bg_hint) = env::var("BACKGROUND") {
107            match bg_hint.to_lowercase().as_str() {
108                "light" | "white" => return ColorScheme::Light,
109                "dark" | "black" => return ColorScheme::Dark,
110                _ => {}
111            }
112        }
113
114        // More aggressive light detection for common desktop environments
115        if let Ok(desktop) = env::var("XDG_CURRENT_DESKTOP") {
116            match desktop.to_lowercase().as_str() {
117                "gnome" | "kde" | "xfce" => {
118                    // Desktop environments often use light themes by default
119                    return ColorScheme::Light;
120                }
121                _ => {}
122            }
123        }
124
125        // Check if we're in a GUI environment (more likely to be light)
126        if env::var("DISPLAY").is_ok() || env::var("WAYLAND_DISPLAY").is_ok() {
127            // GUI environment - light themes are common
128            // But don't override if we have other strong indicators
129            if env::var("TERM_PROGRAM").is_err() {
130                return ColorScheme::Light;
131            }
132        }
133
134        // Default fallback - if we can't detect, prefer dark (most terminals)
135        // But add a bias toward light for macOS users
136        #[cfg(target_os = "macos")]
137        {
138            ColorScheme::Light // macOS Terminal.app default is light
139        }
140
141        #[cfg(not(target_os = "macos"))]
142        {
143            ColorScheme::Dark // Most other platforms default to dark
144        }
145    }
146
147    /// Get the current color scheme
148    pub fn scheme(&self) -> ColorScheme {
149        self.scheme
150    }
151
152    // Header and borders
153    pub fn header_text(&self, text: &str) -> ColoredString {
154        match self.scheme {
155            ColorScheme::Dark => text.bright_white().bold(),
156            ColorScheme::Light => text.black().bold(),
157        }
158    }
159
160    pub fn border(&self, text: &str) -> ColoredString {
161        match self.scheme {
162            ColorScheme::Dark => text.bright_blue(),
163            ColorScheme::Light => text.blue(),
164        }
165    }
166
167    // Primary content colors
168    pub fn primary(&self, text: &str) -> ColoredString {
169        match self.scheme {
170            ColorScheme::Dark => text.yellow(),
171            ColorScheme::Light => text.red().bold(),
172        }
173    }
174
175    pub fn secondary(&self, text: &str) -> ColoredString {
176        match self.scheme {
177            ColorScheme::Dark => text.green(),
178            ColorScheme::Light => text.green().bold(),
179        }
180    }
181
182    // Technology stack colors
183    pub fn language(&self, text: &str) -> ColoredString {
184        match self.scheme {
185            ColorScheme::Dark => text.blue(),
186            ColorScheme::Light => text.blue().bold(),
187        }
188    }
189
190    pub fn framework(&self, text: &str) -> ColoredString {
191        match self.scheme {
192            ColorScheme::Dark => text.magenta(),
193            ColorScheme::Light => text.magenta().bold(),
194        }
195    }
196
197    pub fn database(&self, text: &str) -> ColoredString {
198        match self.scheme {
199            ColorScheme::Dark => text.cyan(),
200            ColorScheme::Light => text.cyan().bold(),
201        }
202    }
203
204    pub fn technology(&self, text: &str) -> ColoredString {
205        match self.scheme {
206            ColorScheme::Dark => text.magenta(),
207            ColorScheme::Light => text.purple().bold(),
208        }
209    }
210
211    // Status and metadata colors
212    pub fn info(&self, text: &str) -> ColoredString {
213        match self.scheme {
214            ColorScheme::Dark => text.cyan(),
215            ColorScheme::Light => text.blue().bold(),
216        }
217    }
218
219    pub fn success(&self, text: &str) -> ColoredString {
220        match self.scheme {
221            ColorScheme::Dark => text.green(),
222            ColorScheme::Light => text.green().bold(),
223        }
224    }
225
226    pub fn warning(&self, text: &str) -> ColoredString {
227        match self.scheme {
228            ColorScheme::Dark => text.yellow(),
229            ColorScheme::Light => text.red(),
230        }
231    }
232
233    pub fn error(&self, text: &str) -> ColoredString {
234        match self.scheme {
235            ColorScheme::Dark => text.red(),
236            ColorScheme::Light => text.red().bold(),
237        }
238    }
239
240    // Label colors (for key-value pairs)
241    pub fn label(&self, text: &str) -> ColoredString {
242        match self.scheme {
243            ColorScheme::Dark => text.bright_white(),
244            ColorScheme::Light => text.black().bold(),
245        }
246    }
247
248    pub fn value(&self, text: &str) -> ColoredString {
249        match self.scheme {
250            ColorScheme::Dark => text.white(),
251            ColorScheme::Light => text.black(),
252        }
253    }
254
255    // Dimmed/subtle text
256    pub fn dimmed(&self, text: &str) -> ColoredString {
257        match self.scheme {
258            ColorScheme::Dark => text.dimmed(),
259            ColorScheme::Light => text.dimmed(),
260        }
261    }
262
263    // Architecture pattern colors
264    pub fn architecture_pattern(&self, text: &str) -> ColoredString {
265        match self.scheme {
266            ColorScheme::Dark => text.green(),
267            ColorScheme::Light => text.green().bold(),
268        }
269    }
270
271    // Project type colors
272    pub fn project_type(&self, text: &str) -> ColoredString {
273        match self.scheme {
274            ColorScheme::Dark => text.yellow(),
275            ColorScheme::Light => text.red().bold(),
276        }
277    }
278
279    // Metrics and numbers
280    pub fn metric(&self, text: &str) -> ColoredString {
281        match self.scheme {
282            ColorScheme::Dark => text.cyan(),
283            ColorScheme::Light => text.blue().bold(),
284        }
285    }
286
287    // File paths and names
288    pub fn path(&self, text: &str) -> ColoredString {
289        match self.scheme {
290            ColorScheme::Dark => text.cyan().bold(),
291            ColorScheme::Light => text.blue().bold(),
292        }
293    }
294
295    // Confidence indicators
296    pub fn confidence_high(&self, text: &str) -> ColoredString {
297        match self.scheme {
298            ColorScheme::Dark => text.green(),
299            ColorScheme::Light => text.green().bold(),
300        }
301    }
302
303    pub fn confidence_medium(&self, text: &str) -> ColoredString {
304        match self.scheme {
305            ColorScheme::Dark => text.yellow(),
306            ColorScheme::Light => text.red(),
307        }
308    }
309
310    pub fn confidence_low(&self, text: &str) -> ColoredString {
311        match self.scheme {
312            ColorScheme::Dark => text.red(),
313            ColorScheme::Light => text.red().bold(),
314        }
315    }
316}
317
318impl Default for ColorAdapter {
319    fn default() -> Self {
320        Self::new()
321    }
322}
323
324/// Global color adapter instance
325static COLOR_ADAPTER: std::sync::OnceLock<ColorAdapter> = std::sync::OnceLock::new();
326
327/// Get the global color adapter instance
328pub fn get_color_adapter() -> &'static ColorAdapter {
329    COLOR_ADAPTER.get_or_init(ColorAdapter::new)
330}
331
332/// Initialize the global color adapter with a specific scheme
333pub fn init_color_adapter(scheme: ColorScheme) {
334    let _ = COLOR_ADAPTER.set(ColorAdapter::with_scheme(scheme));
335}
336
337/// Helper macro for quick color access
338#[macro_export]
339macro_rules! color {
340    ($method:ident, $text:expr) => {
341        $crate::analyzer::display::color_adapter::get_color_adapter().$method($text)
342    };
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348
349    #[test]
350    fn test_color_adapter_creation() {
351        let adapter = ColorAdapter::new();
352        assert!(matches!(
353            adapter.scheme(),
354            ColorScheme::Dark | ColorScheme::Light
355        ));
356    }
357
358    #[test]
359    #[ignore] // Flaky in CI - color codes stripped without terminal
360    fn test_color_scheme_specific() {
361        let dark_adapter = ColorAdapter::with_scheme(ColorScheme::Dark);
362        let light_adapter = ColorAdapter::with_scheme(ColorScheme::Light);
363
364        assert_eq!(dark_adapter.scheme(), ColorScheme::Dark);
365        assert_eq!(light_adapter.scheme(), ColorScheme::Light);
366
367        // Test that different schemes produce different outputs
368        let test_text = "test";
369        let dark_result = dark_adapter.header_text(test_text).to_string();
370        let light_result = light_adapter.header_text(test_text).to_string();
371
372        // Results should be different (due to different color codes)
373        assert_ne!(dark_result, light_result);
374    }
375
376    #[test]
377    fn test_color_methods() {
378        let adapter = ColorAdapter::with_scheme(ColorScheme::Dark);
379        let text = "test";
380
381        // Ensure all color methods work without panicking
382        let _ = adapter.header_text(text);
383        let _ = adapter.border(text);
384        let _ = adapter.primary(text);
385        let _ = adapter.secondary(text);
386        let _ = adapter.language(text);
387        let _ = adapter.framework(text);
388        let _ = adapter.database(text);
389        let _ = adapter.technology(text);
390        let _ = adapter.info(text);
391        let _ = adapter.success(text);
392        let _ = adapter.warning(text);
393        let _ = adapter.error(text);
394        let _ = adapter.label(text);
395        let _ = adapter.value(text);
396        let _ = adapter.dimmed(text);
397        let _ = adapter.architecture_pattern(text);
398        let _ = adapter.project_type(text);
399        let _ = adapter.metric(text);
400        let _ = adapter.path(text);
401        let _ = adapter.confidence_high(text);
402        let _ = adapter.confidence_medium(text);
403        let _ = adapter.confidence_low(text);
404    }
405}