Skip to main content

atlassian_cli_output/
colors.rs

1use colored::Colorize;
2use std::env;
3use std::io::{self, IsTerminal};
4
5/// Detects whether colors should be used based on environment variables and TTY detection
6pub fn should_use_colors() -> bool {
7    // NO_COLOR takes precedence (https://no-color.org/)
8    if env::var("NO_COLOR").is_ok() {
9        return false;
10    }
11
12    // TERM=dumb means no color support
13    if env::var("TERM")
14        .map(|t| t.eq_ignore_ascii_case("dumb"))
15        .unwrap_or(false)
16    {
17        return false;
18    }
19
20    // CLICOLOR_FORCE overrides TTY detection
21    if env::var("CLICOLOR_FORCE").is_ok() {
22        return true;
23    }
24
25    // Auto-detect TTY
26    io::stdout().is_terminal()
27}
28
29/// Status formatter with color support
30pub struct StatusFormatter {
31    use_colors: bool,
32}
33
34impl StatusFormatter {
35    /// Create a new status formatter with color detection
36    pub fn new() -> Self {
37        Self {
38            use_colors: should_use_colors(),
39        }
40    }
41
42    /// Create a status formatter with explicit color setting (useful for testing)
43    pub fn with_colors(use_colors: bool) -> Self {
44        Self { use_colors }
45    }
46
47    /// Format a status string with appropriate color and icon
48    pub fn format(&self, status: &str, icon: &str) -> String {
49        if !self.use_colors {
50            return format!("{} {}", status, icon);
51        }
52
53        let status_upper = status.to_uppercase();
54        match status_upper.as_str() {
55            "SUCCESSFUL" | "COMPLETED" => {
56                format!("{} {}", status.green().bold(), icon)
57            }
58            "FAILED" | "ERROR" => {
59                format!("{} {}", status.red().bold(), icon)
60            }
61            "IN_PROGRESS" | "RUNNING" => {
62                format!("{} {}", status.yellow().bold(), icon)
63            }
64            "STOPPED" => {
65                format!("{} {}", status.bright_red().bold(), icon)
66            }
67            "PENDING" | "NOT_RUN" => {
68                format!("{} {}", status.cyan(), icon)
69            }
70            "PAUSED" => {
71                format!("{} {}", status.magenta(), icon)
72            }
73            _ => {
74                format!("{} {}", status.white(), icon)
75            }
76        }
77    }
78
79    /// Format just the status text without an icon
80    pub fn format_status(&self, status: &str) -> String {
81        if !self.use_colors {
82            return status.to_string();
83        }
84
85        let status_upper = status.to_uppercase();
86        match status_upper.as_str() {
87            "SUCCESSFUL" | "COMPLETED" => status.green().bold().to_string(),
88            "FAILED" | "ERROR" => status.red().bold().to_string(),
89            "IN_PROGRESS" | "RUNNING" => status.yellow().bold().to_string(),
90            "STOPPED" => status.bright_red().bold().to_string(),
91            "PENDING" | "NOT_RUN" => status.cyan().to_string(),
92            "PAUSED" => status.magenta().to_string(),
93            _ => status.white().to_string(),
94        }
95    }
96}
97
98impl Default for StatusFormatter {
99    fn default() -> Self {
100        Self::new()
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    // Helper to save and restore environment variables
109    struct EnvGuard {
110        vars: Vec<(String, Option<String>)>,
111    }
112
113    impl EnvGuard {
114        fn new() -> Self {
115            Self { vars: Vec::new() }
116        }
117
118        fn set(&mut self, key: &str, value: &str) {
119            let old_value = env::var(key).ok();
120            self.vars.push((key.to_string(), old_value));
121            env::set_var(key, value);
122        }
123
124        fn remove(&mut self, key: &str) {
125            let old_value = env::var(key).ok();
126            self.vars.push((key.to_string(), old_value));
127            env::remove_var(key);
128        }
129    }
130
131    impl Drop for EnvGuard {
132        fn drop(&mut self) {
133            for (key, value) in self.vars.iter().rev() {
134                match value {
135                    Some(v) => env::set_var(key, v),
136                    None => env::remove_var(key),
137                }
138            }
139        }
140    }
141
142    #[test]
143    fn test_no_color_env_disables_colors() {
144        let mut guard = EnvGuard::new();
145        guard.set("NO_COLOR", "1");
146        assert!(!should_use_colors());
147    }
148
149    #[test]
150    fn test_term_dumb_disables_colors() {
151        let mut guard = EnvGuard::new();
152        guard.remove("NO_COLOR");
153        guard.remove("CLICOLOR_FORCE");
154        guard.set("TERM", "dumb");
155        assert!(!should_use_colors());
156    }
157
158    #[test]
159    fn test_clicolor_force_enables_colors() {
160        let mut guard = EnvGuard::new();
161        guard.remove("NO_COLOR");
162        guard.set("CLICOLOR_FORCE", "1");
163        assert!(should_use_colors());
164    }
165
166    #[test]
167    fn test_no_color_takes_precedence_over_clicolor_force() {
168        let mut guard = EnvGuard::new();
169        guard.set("NO_COLOR", "1");
170        guard.set("CLICOLOR_FORCE", "1");
171        assert!(!should_use_colors());
172    }
173
174    #[test]
175    fn test_formatter_successful_status() {
176        let formatter = StatusFormatter::with_colors(false);
177        let result = formatter.format("SUCCESSFUL", "✅");
178        assert!(result.contains("SUCCESSFUL"));
179        assert!(result.contains("✅"));
180    }
181
182    #[test]
183    fn test_formatter_failed_status() {
184        let formatter = StatusFormatter::with_colors(false);
185        let result = formatter.format("FAILED", "❌");
186        assert!(result.contains("FAILED"));
187        assert!(result.contains("❌"));
188    }
189
190    #[test]
191    fn test_formatter_in_progress_status() {
192        let formatter = StatusFormatter::with_colors(false);
193        let result = formatter.format("IN_PROGRESS", "🔄");
194        assert!(result.contains("IN_PROGRESS"));
195        assert!(result.contains("🔄"));
196    }
197
198    #[test]
199    fn test_formatter_pending_status() {
200        let formatter = StatusFormatter::with_colors(false);
201        let result = formatter.format("PENDING", "⏳");
202        assert!(result.contains("PENDING"));
203        assert!(result.contains("⏳"));
204    }
205
206    #[test]
207    fn test_formatter_stopped_status() {
208        let formatter = StatusFormatter::with_colors(false);
209        let result = formatter.format("STOPPED", "⏹");
210        assert!(result.contains("STOPPED"));
211        assert!(result.contains("⏹"));
212    }
213
214    #[test]
215    fn test_formatter_paused_status() {
216        let formatter = StatusFormatter::with_colors(false);
217        let result = formatter.format("PAUSED", "⏸");
218        assert!(result.contains("PAUSED"));
219        assert!(result.contains("⏸"));
220    }
221
222    #[test]
223    fn test_formatter_unknown_status() {
224        let formatter = StatusFormatter::with_colors(false);
225        let result = formatter.format("UNKNOWN", "❓");
226        assert!(result.contains("UNKNOWN"));
227        assert!(result.contains("❓"));
228    }
229
230    #[test]
231    fn test_formatter_with_colors_enabled() {
232        let formatter = StatusFormatter::with_colors(true);
233        let result = formatter.format("SUCCESSFUL", "✅");
234        // When colors are enabled, the result contains ANSI codes
235        // We can't easily test the exact ANSI codes, but we can verify content is present
236        assert!(result.contains("✅"));
237    }
238
239    #[test]
240    fn test_format_status_without_icon() {
241        let formatter = StatusFormatter::with_colors(false);
242        let result = formatter.format_status("FAILED");
243        assert_eq!(result, "FAILED");
244    }
245
246    #[test]
247    fn test_formatter_case_insensitive() {
248        let formatter = StatusFormatter::with_colors(false);
249        let result1 = formatter.format("successful", "✅");
250        let result2 = formatter.format("SUCCESSFUL", "✅");
251        let result3 = formatter.format("Successful", "✅");
252
253        assert!(result1.contains("successful"));
254        assert!(result2.contains("SUCCESSFUL"));
255        assert!(result3.contains("Successful"));
256    }
257}