scirs2_neural/utils/
colors.rs

1//! Terminal color utilities for visualization output
2//!
3//! This module provides utilities for adding colors to terminal output.
4
5use std::fmt::Display;
6
7/// ANSI color codes
8#[derive(Debug, Clone, Copy)]
9pub enum Color {
10    /// Black color
11    Black,
12    /// Red color
13    Red,
14    /// Green color
15    Green,
16    /// Yellow color
17    Yellow,
18    /// Blue color
19    Blue,
20    /// Magenta color
21    Magenta,
22    /// Cyan color
23    Cyan,
24    /// White color
25    White,
26    /// Bright black (gray) color
27    BrightBlack,
28    /// Bright red color
29    BrightRed,
30    /// Bright green color
31    BrightGreen,
32    /// Bright yellow color
33    BrightYellow,
34    /// Bright blue color
35    BrightBlue,
36    /// Bright magenta color
37    BrightMagenta,
38    /// Bright cyan color
39    BrightCyan,
40    /// Bright white color
41    BrightWhite,
42}
43
44impl Color {
45    /// Get the ANSI foreground color code
46    pub fn fg_code(&self) -> &'static str {
47        match self {
48            Color::Black => "\x1b[30m",
49            Color::Red => "\x1b[31m",
50            Color::Green => "\x1b[32m",
51            Color::Yellow => "\x1b[33m",
52            Color::Blue => "\x1b[34m",
53            Color::Magenta => "\x1b[35m",
54            Color::Cyan => "\x1b[36m",
55            Color::White => "\x1b[37m",
56            Color::BrightBlack => "\x1b[90m",
57            Color::BrightRed => "\x1b[91m",
58            Color::BrightGreen => "\x1b[92m",
59            Color::BrightYellow => "\x1b[93m",
60            Color::BrightBlue => "\x1b[94m",
61            Color::BrightMagenta => "\x1b[95m",
62            Color::BrightCyan => "\x1b[96m",
63            Color::BrightWhite => "\x1b[97m",
64        }
65    }
66
67    /// Get the ANSI background color code
68    pub fn bg_code(&self) -> &'static str {
69        match self {
70            Color::Black => "\x1b[40m",
71            Color::Red => "\x1b[41m",
72            Color::Green => "\x1b[42m",
73            Color::Yellow => "\x1b[43m",
74            Color::Blue => "\x1b[44m",
75            Color::Magenta => "\x1b[45m",
76            Color::Cyan => "\x1b[46m",
77            Color::White => "\x1b[47m",
78            Color::BrightBlack => "\x1b[100m",
79            Color::BrightRed => "\x1b[101m",
80            Color::BrightGreen => "\x1b[102m",
81            Color::BrightYellow => "\x1b[103m",
82            Color::BrightBlue => "\x1b[104m",
83            Color::BrightMagenta => "\x1b[105m",
84            Color::BrightCyan => "\x1b[106m",
85            Color::BrightWhite => "\x1b[107m",
86        }
87    }
88}
89
90/// ANSI text style
91#[derive(Debug, Clone, Copy)]
92pub enum Style {
93    /// Bold text
94    Bold,
95    /// Italic text
96    Italic,
97    /// Underlined text
98    Underline,
99    /// Blinking text
100    Blink,
101    /// Inverted colors
102    Reverse,
103    /// Dim text
104    Dim,
105}
106
107impl Style {
108    /// Get the ANSI style code
109    pub fn code(&self) -> &'static str {
110        match self {
111            Style::Bold => "\x1b[1m",
112            Style::Italic => "\x1b[3m",
113            Style::Underline => "\x1b[4m",
114            Style::Blink => "\x1b[5m",
115            Style::Reverse => "\x1b[7m",
116            Style::Dim => "\x1b[2m",
117        }
118    }
119}
120
121/// ANSI reset code
122pub const RESET: &str = "\x1b[0m";
123
124/// Colorize text with a foreground color
125#[allow(dead_code)]
126pub fn colorize<T: Display>(text: T, color: Color) -> String {
127    format!("{}{}{}", color.fg_code(), text, RESET)
128}
129
130/// Colorize text with a background color
131#[allow(dead_code)]
132pub fn colorize_bg<T: Display>(text: T, color: Color) -> String {
133    format!("{}{}{}", color.bg_code(), text, RESET)
134}
135
136/// Style text with a text style
137#[allow(dead_code)]
138pub fn stylize<T: Display>(text: T, style: Style) -> String {
139    format!("{}{}{}", style.code(), text, RESET)
140}
141
142/// Colorize and style text
143#[allow(dead_code)]
144pub fn colorize_and_style<T: Display>(
145    text: T,
146    fg_color: Option<Color>,
147    bg_color: Option<Color>,
148    style: Option<Style>,
149) -> String {
150    let mut result = String::new();
151    if let Some(fg) = fg_color {
152        result.push_str(fg.fg_code());
153    }
154    if let Some(bg) = bg_color {
155        result.push_str(bg.bg_code());
156    }
157    if let Some(s) = style {
158        result.push_str(s.code());
159    }
160    result.push_str(&format!("{text}"));
161    result.push_str(RESET);
162    result
163}
164
165/// Detect if the terminal supports colors
166///
167/// This is a simple heuristic based on environment variables
168#[allow(dead_code)]
169pub fn supports_color() -> bool {
170    if let Ok(term) = std::env::var("TERM") {
171        if term == "dumb" {
172            return false;
173        }
174    }
175    if let Ok(no_color) = std::env::var("NO_COLOR") {
176        if !no_color.is_empty() {
177            return false;
178        }
179    }
180    if let Ok(color) = std::env::var("FORCE_COLOR") {
181        if !color.is_empty() {
182            return true;
183        }
184    }
185    // Check if running on GitHub Actions
186    if std::env::var("GITHUB_ACTIONS").is_ok() {
187        return true;
188    }
189    // Use cfg to branch platform-specific code
190    #[cfg(not(target_os = "windows"))]
191    {
192        // Simple heuristic: assume color is supported on non-Windows platforms
193        true
194    }
195    #[cfg(target_os = "windows")]
196    {
197        // On Windows, check for common terminals that support colors
198        if let Ok(term) = std::env::var("TERM_PROGRAM") {
199            if term == "vscode" || term == "mintty" || term == "alacritty" {
200                true
201            } else {
202                false
203            }
204        } else {
205            false
206        }
207    }
208}
209
210/// Colorization options for terminal output
211pub struct ColorOptions {
212    /// Whether to use colors
213    pub enabled: bool,
214    /// Whether to use background colors
215    pub use_background: bool,
216    /// Whether to use bright colors
217    pub use_bright: bool,
218}
219
220impl Default for ColorOptions {
221    fn default() -> Self {
222        Self {
223            enabled: supports_color(),
224            use_background: true,
225            use_bright: true,
226        }
227    }
228}
229
230/// Get the appropriate color based on a value's position in a range
231/// Returns red for values close to 0.0, yellow for values around 0.5,
232/// and green for values close to 1.0
233#[allow(dead_code)]
234pub fn gradient_color(value: f64, options: &ColorOptions) -> Option<Color> {
235    if !options.enabled {
236        return None;
237    }
238    if !(0.0..=1.0).contains(&value) {
239        return None;
240    }
241    // Red -> Yellow -> Green gradient
242    if value < 0.5 {
243        // Red to Yellow (0.0 -> 0.5)
244        if options.use_bright {
245            Some(Color::BrightRed)
246        } else {
247            Some(Color::Red)
248        }
249    } else if value < 0.7 {
250        // Yellow (0.5 -> 0.7)
251        if options.use_bright {
252            Some(Color::BrightYellow)
253        } else {
254            Some(Color::Yellow)
255        }
256    } else {
257        // Green (0.7 -> 1.0)
258        if options.use_bright {
259            Some(Color::BrightGreen)
260        } else {
261            Some(Color::Green)
262        }
263    }
264}
265
266/// Generate a more fine-grained gradient color for heatmap visualizations
267/// This provides a more detailed color spectrum for visualizing data with subtle differences
268/// Returns a spectrum from cool (blues/purples) for low values to warm (reds/yellows) for high values
269#[allow(dead_code)]
270pub fn heatmap_gradient_color(value: f64, options: &ColorOptions) -> Option<Color> {
271    if !options.enabled {
272        return None;
273    }
274    if !(0.0..=1.0).contains(&value) {
275        return None;
276    }
277    // More detailed gradient with 5 color stops
278    if value < 0.2 {
279        // Very low values (0.0 -> 0.2)
280        if options.use_bright {
281            Some(Color::BrightBlue)
282        } else {
283            Some(Color::Blue)
284        }
285    } else if value < 0.4 {
286        // Low values (0.2 -> 0.4)
287        if options.use_bright {
288            Some(Color::BrightCyan)
289        } else {
290            Some(Color::Cyan)
291        }
292    } else if value < 0.6 {
293        // Medium values (0.4 -> 0.6)
294        if options.use_bright {
295            Some(Color::BrightYellow)
296        } else {
297            Some(Color::Yellow)
298        }
299    } else if value < 0.8 {
300        // High values (0.6 -> 0.8)
301        if options.use_bright {
302            Some(Color::BrightRed)
303        } else {
304            Some(Color::Red)
305        }
306    } else {
307        // Very high values (0.8 -> 1.0)
308        if options.use_bright {
309            Some(Color::BrightMagenta)
310        } else {
311            Some(Color::Magenta)
312        }
313    }
314}
315
316/// Generate table cell content with appropriate color based on value
317#[allow(dead_code)]
318pub fn colored_metric_cell<T: Display>(
319    value: T,
320    normalized_value: f64,
321    options: &ColorOptions,
322) -> String {
323    if !options.enabled {
324        return format!("{value}");
325    }
326    if let Some(color) = gradient_color(normalized_value, options) {
327        colorize(value, color)
328    } else {
329        format!("{value}")
330    }
331}
332
333/// Generate a heatmap cell with color gradient for confusion matrix
334#[allow(dead_code)]
335pub fn heatmap_cell<T: Display>(
336    _value: T,
337    normalized_value: f64,
338    options: &ColorOptions,
339) -> String {
340    if !options.enabled {
341        return format!("{_value}");
342    }
343    if let Some(color) = heatmap_gradient_color(normalized_value, options) {
344        // For higher values, use bold to emphasize importance
345        if normalized_value > 0.7 {
346            colorize(stylize(_value, Style::Bold), color)
347        } else {
348            colorize(_value, color)
349        }
350    } else {
351        format!("{_value}")
352    }
353}
354
355/// Build a color legend for confusion matrix or other visualizations
356#[allow(dead_code)]
357pub fn color_legend(options: &ColorOptions) -> Option<String> {
358    if !options.enabled {
359        return None;
360    }
361    let mut legend = String::from("Color Legend: ");
362    let low_color = if options.use_bright {
363        Color::BrightRed
364    } else {
365        Color::Red
366    };
367    let mid_color = if options.use_bright {
368        Color::BrightYellow
369    } else {
370        Color::Yellow
371    };
372    let high_color = if options.use_bright {
373        Color::BrightGreen
374    } else {
375        Color::Green
376    };
377    legend.push_str(&format!("{} Low (0.0-0.5) ", colorize("■", low_color)));
378    legend.push_str(&format!("{} Medium (0.5-0.7) ", colorize("■", mid_color)));
379    legend.push_str(&format!("{} High (0.7-1.0)", colorize("■", high_color)));
380    Some(legend)
381}
382
383/// Build a detailed heatmap color legend
384#[allow(dead_code)]
385pub fn heatmap_color_legend(options: &ColorOptions) -> Option<String> {
386    if !options.enabled {
387        return None;
388    }
389    let mut legend = String::from("Heatmap Legend: ");
390    let colors = [
391        (
392            if options.use_bright {
393                Color::BrightBlue
394            } else {
395                Color::Blue
396            },
397            "Very Low (0.0-0.2)",
398        ),
399        (
400            if options.use_bright {
401                Color::BrightCyan
402            } else {
403                Color::Cyan
404            },
405            "Low (0.2-0.4)",
406        ),
407        (
408            if options.use_bright {
409                Color::BrightYellow
410            } else {
411                Color::Yellow
412            },
413            "Medium (0.4-0.6)",
414        ),
415        (
416            if options.use_bright {
417                Color::BrightRed
418            } else {
419                Color::Red
420            },
421            "High (0.6-0.8)",
422        ),
423        (
424            if options.use_bright {
425                Color::BrightMagenta
426            } else {
427                Color::Magenta
428            },
429            "Very High (0.8-1.0)",
430        ),
431    ];
432    for (i, (color, label)) in colors.iter().enumerate() {
433        if i > 0 {
434            legend.push(' ');
435        }
436        legend.push_str(&format!("{} {}", colorize("■", *color), label));
437    }
438    Some(legend)
439}
440
441/// Helper function to conditionally create a string with ANSI coloring for terminal output
442#[allow(dead_code)]
443pub fn colored_string<T: Display>(
444    content: T,
445    color: Option<Color>,
446    style: Option<Style>,
447) -> String {
448    match (color, style) {
449        (Some(c), Some(s)) => colorize_and_style(content, Some(c), None, Some(s)),
450        (Some(c), None) => colorize(content, c),
451        (None, Some(s)) => stylize(content, s),
452        (None, None) => content.to_string(),
453    }
454}