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            if let Some(bg_str) = colorfgbg.split(';').nth(1) {
42                if let Ok(bg_code) = bg_str.parse::<u8>() {
43                    // Background colors 0-6 are dark, 7-15 are light/bright
44                    // Be more aggressive about detecting light backgrounds
45                    return if bg_code >= 7 {
46                        ColorScheme::Light
47                    } else {
48                        ColorScheme::Dark
49                    };
50                }
51            }
52        }
53
54        // Check for common light terminal setups
55        if let Ok(term_program) = env::var("TERM_PROGRAM") {
56            match term_program.as_str() {
57                "Apple_Terminal" => {
58                    // macOS Terminal.app - check for light theme indicators
59                    // Many users have light themes, so be more aggressive
60                    if let Ok(_term_session_id) = env::var("TERM_SESSION_ID") {
61                        // If we can't detect definitively, assume light for Terminal.app
62                        // since many users use the default light theme
63                        return ColorScheme::Light;
64                    }
65                    return ColorScheme::Light; // Default to light for Terminal.app
66                }
67                "iTerm.app" => {
68                    // iTerm2 - check for theme hints
69                    if let Ok(_iterm_session_id) = env::var("ITERM_SESSION_ID") {
70                        // Default to dark for iTerm as it's more commonly used with dark themes
71                        return ColorScheme::Dark;
72                    }
73                }
74                "vscode" | "code" => {
75                    // VS Code integrated terminal - often follows editor theme
76                    // VS Code light themes are common
77                    return ColorScheme::Light;
78                }
79                _ => {}
80            }
81        }
82
83        // Check terminal type and name hints
84        if let Ok(term) = env::var("TERM") {
85            match term.as_str() {
86                term if term.contains("light") => return ColorScheme::Light,
87                term if term.contains("256color") => {
88                    // Modern terminals with 256 color support
89                    // Check other indicators
90                    if env::var("TERM_PROGRAM")
91                        .unwrap_or_default()
92                        .contains("Terminal")
93                    {
94                        return ColorScheme::Light; // macOS Terminal default
95                    }
96                }
97                _ => {}
98            }
99        }
100
101        // Check for SSH session - often indicates server/dark environment
102        if env::var("SSH_CONNECTION").is_ok() || env::var("SSH_CLIENT").is_ok() {
103            return ColorScheme::Dark;
104        }
105
106        // Check background color hints from other variables
107        if let Ok(bg_hint) = env::var("BACKGROUND") {
108            match bg_hint.to_lowercase().as_str() {
109                "light" | "white" => return ColorScheme::Light,
110                "dark" | "black" => return ColorScheme::Dark,
111                _ => {}
112            }
113        }
114
115        // More aggressive light detection for common desktop environments
116        if let Ok(desktop) = env::var("XDG_CURRENT_DESKTOP") {
117            match desktop.to_lowercase().as_str() {
118                "gnome" | "kde" | "xfce" => {
119                    // Desktop environments often use light themes by default
120                    return ColorScheme::Light;
121                }
122                _ => {}
123            }
124        }
125
126        // Check if we're in a GUI environment (more likely to be light)
127        if env::var("DISPLAY").is_ok() || env::var("WAYLAND_DISPLAY").is_ok() {
128            // GUI environment - light themes are common
129            // But don't override if we have other strong indicators
130            if env::var("TERM_PROGRAM").is_err() {
131                return ColorScheme::Light;
132            }
133        }
134
135        // Default fallback - if we can't detect, prefer dark (most terminals)
136        // But add a bias toward light for macOS users
137        #[cfg(target_os = "macos")]
138        {
139            return ColorScheme::Light; // macOS Terminal.app default is light
140        }
141
142        #[cfg(not(target_os = "macos"))]
143        {
144            return ColorScheme::Dark; // Most other platforms default to dark
145        }
146    }
147
148    /// Get the current color scheme
149    pub fn scheme(&self) -> ColorScheme {
150        self.scheme
151    }
152
153    // Header and borders
154    pub fn header_text(&self, text: &str) -> ColoredString {
155        match self.scheme {
156            ColorScheme::Dark => text.bright_white().bold(),
157            ColorScheme::Light => text.black().bold(),
158        }
159    }
160
161    pub fn border(&self, text: &str) -> ColoredString {
162        match self.scheme {
163            ColorScheme::Dark => text.bright_blue(),
164            ColorScheme::Light => text.blue(),
165        }
166    }
167
168    // Primary content colors
169    pub fn primary(&self, text: &str) -> ColoredString {
170        match self.scheme {
171            ColorScheme::Dark => text.yellow(),
172            ColorScheme::Light => text.red().bold(),
173        }
174    }
175
176    pub fn secondary(&self, text: &str) -> ColoredString {
177        match self.scheme {
178            ColorScheme::Dark => text.green(),
179            ColorScheme::Light => text.green().bold(),
180        }
181    }
182
183    // Technology stack colors
184    pub fn language(&self, text: &str) -> ColoredString {
185        match self.scheme {
186            ColorScheme::Dark => text.blue(),
187            ColorScheme::Light => text.blue().bold(),
188        }
189    }
190
191    pub fn framework(&self, text: &str) -> ColoredString {
192        match self.scheme {
193            ColorScheme::Dark => text.magenta(),
194            ColorScheme::Light => text.magenta().bold(),
195        }
196    }
197
198    pub fn database(&self, text: &str) -> ColoredString {
199        match self.scheme {
200            ColorScheme::Dark => text.cyan(),
201            ColorScheme::Light => text.cyan().bold(),
202        }
203    }
204
205    pub fn technology(&self, text: &str) -> ColoredString {
206        match self.scheme {
207            ColorScheme::Dark => text.magenta(),
208            ColorScheme::Light => text.purple().bold(),
209        }
210    }
211
212    // Status and metadata colors
213    pub fn info(&self, text: &str) -> ColoredString {
214        match self.scheme {
215            ColorScheme::Dark => text.cyan(),
216            ColorScheme::Light => text.blue().bold(),
217        }
218    }
219
220    pub fn success(&self, text: &str) -> ColoredString {
221        match self.scheme {
222            ColorScheme::Dark => text.green(),
223            ColorScheme::Light => text.green().bold(),
224        }
225    }
226
227    pub fn warning(&self, text: &str) -> ColoredString {
228        match self.scheme {
229            ColorScheme::Dark => text.yellow(),
230            ColorScheme::Light => text.red(),
231        }
232    }
233
234    pub fn error(&self, text: &str) -> ColoredString {
235        match self.scheme {
236            ColorScheme::Dark => text.red(),
237            ColorScheme::Light => text.red().bold(),
238        }
239    }
240
241    // Label colors (for key-value pairs)
242    pub fn label(&self, text: &str) -> ColoredString {
243        match self.scheme {
244            ColorScheme::Dark => text.bright_white(),
245            ColorScheme::Light => text.black().bold(),
246        }
247    }
248
249    pub fn value(&self, text: &str) -> ColoredString {
250        match self.scheme {
251            ColorScheme::Dark => text.white(),
252            ColorScheme::Light => text.black(),
253        }
254    }
255
256    // Dimmed/subtle text
257    pub fn dimmed(&self, text: &str) -> ColoredString {
258        match self.scheme {
259            ColorScheme::Dark => text.dimmed(),
260            ColorScheme::Light => text.dimmed(),
261        }
262    }
263
264    // Architecture pattern colors
265    pub fn architecture_pattern(&self, text: &str) -> ColoredString {
266        match self.scheme {
267            ColorScheme::Dark => text.green(),
268            ColorScheme::Light => text.green().bold(),
269        }
270    }
271
272    // Project type colors
273    pub fn project_type(&self, text: &str) -> ColoredString {
274        match self.scheme {
275            ColorScheme::Dark => text.yellow(),
276            ColorScheme::Light => text.red().bold(),
277        }
278    }
279
280    // Metrics and numbers
281    pub fn metric(&self, text: &str) -> ColoredString {
282        match self.scheme {
283            ColorScheme::Dark => text.cyan(),
284            ColorScheme::Light => text.blue().bold(),
285        }
286    }
287
288    // File paths and names
289    pub fn path(&self, text: &str) -> ColoredString {
290        match self.scheme {
291            ColorScheme::Dark => text.cyan().bold(),
292            ColorScheme::Light => text.blue().bold(),
293        }
294    }
295
296    // Confidence indicators
297    pub fn confidence_high(&self, text: &str) -> ColoredString {
298        match self.scheme {
299            ColorScheme::Dark => text.green(),
300            ColorScheme::Light => text.green().bold(),
301        }
302    }
303
304    pub fn confidence_medium(&self, text: &str) -> ColoredString {
305        match self.scheme {
306            ColorScheme::Dark => text.yellow(),
307            ColorScheme::Light => text.red(),
308        }
309    }
310
311    pub fn confidence_low(&self, text: &str) -> ColoredString {
312        match self.scheme {
313            ColorScheme::Dark => text.red(),
314            ColorScheme::Light => text.red().bold(),
315        }
316    }
317}
318
319impl Default for ColorAdapter {
320    fn default() -> Self {
321        Self::new()
322    }
323}
324
325/// Global color adapter instance
326static COLOR_ADAPTER: std::sync::OnceLock<ColorAdapter> = std::sync::OnceLock::new();
327
328/// Get the global color adapter instance
329pub fn get_color_adapter() -> &'static ColorAdapter {
330    COLOR_ADAPTER.get_or_init(ColorAdapter::new)
331}
332
333/// Initialize the global color adapter with a specific scheme
334pub fn init_color_adapter(scheme: ColorScheme) {
335    let _ = COLOR_ADAPTER.set(ColorAdapter::with_scheme(scheme));
336}
337
338/// Helper macro for quick color access
339#[macro_export]
340macro_rules! color {
341    ($method:ident, $text:expr) => {
342        $crate::analyzer::display::color_adapter::get_color_adapter().$method($text)
343    };
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349
350    #[test]
351    fn test_color_adapter_creation() {
352        let adapter = ColorAdapter::new();
353        assert!(matches!(
354            adapter.scheme(),
355            ColorScheme::Dark | ColorScheme::Light
356        ));
357    }
358
359    #[test]
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}