data_gov/
colors.rs

1use colored::{ColoredString, Colorize};
2use is_terminal::IsTerminal;
3use std::env;
4use std::io::{stderr, stdout};
5
6/// Color mode configuration
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum ColorMode {
9    Auto,   // Use TTY detection
10    Always, // Always use colors
11    Never,  // Never use colors
12}
13
14impl Default for ColorMode {
15    fn default() -> Self {
16        ColorMode::Auto
17    }
18}
19
20impl std::str::FromStr for ColorMode {
21    type Err = String;
22
23    fn from_str(s: &str) -> Result<Self, Self::Err> {
24        match s.to_lowercase().as_str() {
25            "auto" => Ok(ColorMode::Auto),
26            "always" => Ok(ColorMode::Always),
27            "never" => Ok(ColorMode::Never),
28            _ => Err(format!(
29                "Invalid color mode: '{}'. Valid options: auto, always, never",
30                s
31            )),
32        }
33    }
34}
35
36/// TTY-aware color helper that respects NO_COLOR and terminal detection
37#[derive(Clone)]
38pub struct ColorHelper {
39    mode: ColorMode,
40    stdout_is_terminal: bool,
41    stderr_is_terminal: bool,
42    no_color: bool,
43}
44
45impl ColorHelper {
46    /// Create a new color helper with the specified mode
47    pub fn new(mode: ColorMode) -> Self {
48        Self {
49            mode,
50            stdout_is_terminal: stdout().is_terminal(),
51            stderr_is_terminal: stderr().is_terminal(),
52            no_color: env::var("NO_COLOR").is_ok()
53                && !env::var("NO_COLOR").unwrap_or_default().is_empty(),
54        }
55    }
56
57    /// Check if colors should be used for stdout
58    pub fn should_color_stdout(&self) -> bool {
59        self.should_use_colors(self.stdout_is_terminal)
60    }
61
62    /// Check if colors should be used for stderr
63    pub fn should_color_stderr(&self) -> bool {
64        self.should_use_colors(self.stderr_is_terminal)
65    }
66
67    /// Internal logic for color determination
68    fn should_use_colors(&self, is_terminal: bool) -> bool {
69        // Respect NO_COLOR environment variable (standard)
70        if self.no_color {
71            return false;
72        }
73
74        match self.mode {
75            ColorMode::Never => false,
76            ColorMode::Always => true,
77            ColorMode::Auto => is_terminal,
78        }
79    }
80
81    /// Apply red color if colors are enabled
82    pub fn red(&self, text: &str) -> ColoredString {
83        if self.should_color_stdout() {
84            text.red()
85        } else {
86            text.normal()
87        }
88    }
89
90    /// Apply green color if colors are enabled
91    pub fn green(&self, text: &str) -> ColoredString {
92        if self.should_color_stdout() {
93            text.green()
94        } else {
95            text.normal()
96        }
97    }
98
99    /// Apply blue color if colors are enabled
100    pub fn blue(&self, text: &str) -> ColoredString {
101        if self.should_color_stdout() {
102            text.blue()
103        } else {
104            text.normal()
105        }
106    }
107
108    /// Apply yellow color if colors are enabled
109    pub fn yellow(&self, text: &str) -> ColoredString {
110        if self.should_color_stdout() {
111            text.yellow()
112        } else {
113            text.normal()
114        }
115    }
116
117    /// Apply cyan color if colors are enabled
118    pub fn cyan(&self, text: &str) -> ColoredString {
119        if self.should_color_stdout() {
120            text.cyan()
121        } else {
122            text.normal()
123        }
124    }
125
126    /// Apply magenta color if colors are enabled
127    pub fn magenta(&self, text: &str) -> ColoredString {
128        if self.should_color_stdout() {
129            text.magenta()
130        } else {
131            text.normal()
132        }
133    }
134
135    /// Apply bold formatting if colors are enabled
136    pub fn bold(&self, text: &str) -> ColoredString {
137        if self.should_color_stdout() {
138            text.bold()
139        } else {
140            text.normal()
141        }
142    }
143
144    /// Apply dimmed formatting if colors are enabled
145    pub fn dimmed(&self, text: &str) -> ColoredString {
146        if self.should_color_stdout() {
147            text.dimmed()
148        } else {
149            text.normal()
150        }
151    }
152
153    /// Chainable color and formatting methods
154    pub fn style(&self) -> StyleBuilder {
155        StyleBuilder::new(self.should_color_stdout())
156    }
157}
158
159/// Builder for chaining color and formatting operations
160pub struct StyleBuilder {
161    should_color: bool,
162}
163
164impl StyleBuilder {
165    pub fn new(should_color: bool) -> Self {
166        Self { should_color }
167    }
168
169    pub fn red(self, text: &str) -> ChainedStyle {
170        ChainedStyle::new(text, self.should_color).red()
171    }
172
173    pub fn green(self, text: &str) -> ChainedStyle {
174        ChainedStyle::new(text, self.should_color).green()
175    }
176
177    pub fn blue(self, text: &str) -> ChainedStyle {
178        ChainedStyle::new(text, self.should_color).blue()
179    }
180
181    pub fn yellow(self, text: &str) -> ChainedStyle {
182        ChainedStyle::new(text, self.should_color).yellow()
183    }
184
185    pub fn cyan(self, text: &str) -> ChainedStyle {
186        ChainedStyle::new(text, self.should_color).cyan()
187    }
188
189    pub fn magenta(self, text: &str) -> ChainedStyle {
190        ChainedStyle::new(text, self.should_color).magenta()
191    }
192
193    pub fn bold(self, text: &str) -> ChainedStyle {
194        ChainedStyle::new(text, self.should_color).bold()
195    }
196
197    pub fn dimmed(self, text: &str) -> ChainedStyle {
198        ChainedStyle::new(text, self.should_color).dimmed()
199    }
200}
201
202/// Chainable style operations
203pub struct ChainedStyle {
204    text: String,
205    should_color: bool,
206}
207
208impl ChainedStyle {
209    fn new(text: &str, should_color: bool) -> Self {
210        Self {
211            text: text.to_string(),
212            should_color,
213        }
214    }
215
216    pub fn red(mut self) -> Self {
217        if self.should_color {
218            self.text = self.text.red().to_string();
219        }
220        self
221    }
222
223    pub fn green(mut self) -> Self {
224        if self.should_color {
225            self.text = self.text.green().to_string();
226        }
227        self
228    }
229
230    pub fn blue(mut self) -> Self {
231        if self.should_color {
232            self.text = self.text.blue().to_string();
233        }
234        self
235    }
236
237    pub fn yellow(mut self) -> Self {
238        if self.should_color {
239            self.text = self.text.yellow().to_string();
240        }
241        self
242    }
243
244    pub fn cyan(mut self) -> Self {
245        if self.should_color {
246            self.text = self.text.cyan().to_string();
247        }
248        self
249    }
250
251    pub fn magenta(mut self) -> Self {
252        if self.should_color {
253            self.text = self.text.magenta().to_string();
254        }
255        self
256    }
257
258    pub fn bold(mut self) -> Self {
259        if self.should_color {
260            self.text = self.text.bold().to_string();
261        }
262        self
263    }
264
265    pub fn dimmed(mut self) -> Self {
266        if self.should_color {
267            self.text = self.text.dimmed().to_string();
268        }
269        self
270    }
271}
272
273impl std::fmt::Display for ChainedStyle {
274    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
275        write!(f, "{}", self.text)
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282
283    #[test]
284    fn test_color_mode_parsing() {
285        assert_eq!("auto".parse::<ColorMode>().unwrap(), ColorMode::Auto);
286        assert_eq!("always".parse::<ColorMode>().unwrap(), ColorMode::Always);
287        assert_eq!("never".parse::<ColorMode>().unwrap(), ColorMode::Never);
288        assert!("invalid".parse::<ColorMode>().is_err());
289    }
290
291    #[test]
292    fn test_color_helper_never() {
293        let helper = ColorHelper::new(ColorMode::Never);
294        assert!(!helper.should_color_stdout());
295        assert!(!helper.should_color_stderr());
296    }
297
298    #[test]
299    fn test_color_helper_always() {
300        let helper = ColorHelper::new(ColorMode::Always);
301        // Should be true unless NO_COLOR is set
302        if !helper.no_color {
303            assert!(helper.should_color_stdout());
304            assert!(helper.should_color_stderr());
305        }
306    }
307}