fob_cli/ui/
mod.rs

1//! Terminal UI utilities for progress bars and formatted output.
2//!
3//! This module provides a clean API for displaying build progress, status messages,
4//! and formatted output in the terminal. It handles environment detection (CI, TTY)
5//! and gracefully degrades when terminal features aren't available.
6//!
7//! # Examples
8//!
9//! ```no_run
10//! use fob_cli::ui;
11//! use std::time::Duration;
12//!
13//! // Initialize color support
14//! ui::init_colors();
15//!
16//! // Show progress for multi-step operations
17//! let mut progress = ui::BundleProgress::new(3);
18//! let task = progress.add_task("Building modules");
19//! progress.finish_task(task, "Built 5 modules");
20//! progress.finish("Build complete!");
21//!
22//! // Simple spinner for quick tasks
23//! let spinner = ui::Spinner::new("Loading config...");
24//! spinner.finish("Config loaded");
25//!
26//! // Status messages
27//! ui::success("Build successful");
28//! ui::error("Failed to parse file");
29//! ```
30
31// Submodules
32mod format;
33mod messages;
34mod progress;
35mod spinner;
36
37// Re-exports for convenient access
38pub use format::{format_duration, format_size, print_build_summary};
39pub use messages::{debug, error, info, success, warning};
40pub use progress::BundleProgress;
41pub use spinner::Spinner;
42
43/// Check if running in a CI environment.
44///
45/// Detects common CI environment variables from GitHub Actions, GitLab CI,
46/// CircleCI, and Travis CI.
47///
48/// # Returns
49///
50/// `true` if running in CI
51pub fn is_ci() -> bool {
52    std::env::var("CI").is_ok()
53        || std::env::var("GITHUB_ACTIONS").is_ok()
54        || std::env::var("GITLAB_CI").is_ok()
55        || std::env::var("CIRCLECI").is_ok()
56        || std::env::var("TRAVIS").is_ok()
57}
58
59/// Check if color output should be enabled.
60///
61/// Respects NO_COLOR and FORCE_COLOR environment variables, falls back to
62/// terminal capability detection.
63///
64/// # Returns
65///
66/// `true` if colors should be used
67pub fn should_use_color() -> bool {
68    // NO_COLOR environment variable disables colors
69    if std::env::var("NO_COLOR").is_ok() {
70        return false;
71    }
72
73    // FORCE_COLOR enables colors even in non-TTY
74    if std::env::var("FORCE_COLOR").is_ok() {
75        return true;
76    }
77
78    // Check if stderr is a terminal
79    console::user_attended_stderr()
80}
81
82/// Initialize color support based on environment.
83///
84/// Should be called early in the application lifecycle (e.g., in main).
85/// Respects NO_COLOR and FORCE_COLOR environment variables.
86///
87/// **Note**: This function currently checks color support but doesn't modify
88/// global state. The `owo-colors` crate automatically respects NO_COLOR and
89/// terminal capabilities. This function is provided for explicit initialization
90/// and future extensibility.
91///
92/// # Examples
93///
94/// ```no_run
95/// use fob_cli::ui;
96///
97/// ui::init_colors();
98/// // ... rest of application
99/// ```
100pub fn init_colors() {
101    // owo-colors automatically respects NO_COLOR and terminal capabilities.
102    // This function performs validation and can be extended for custom logic.
103    let _ = should_use_color();
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn test_is_ci_no_env() {
112        // Remove CI vars if they exist
113        unsafe {
114            std::env::remove_var("CI");
115            std::env::remove_var("GITHUB_ACTIONS");
116            std::env::remove_var("GITLAB_CI");
117            std::env::remove_var("CIRCLECI");
118            std::env::remove_var("TRAVIS");
119        }
120
121        // This might be false or true depending on test environment
122        // Just verify it doesn't panic
123        let _ = is_ci();
124    }
125
126    #[test]
127    fn test_is_ci_with_ci_var() {
128        std::env::set_var("CI", "true");
129        assert!(is_ci());
130        unsafe {
131            std::env::remove_var("CI");
132        }
133    }
134
135    #[test]
136    fn test_is_ci_with_github_actions() {
137        std::env::set_var("GITHUB_ACTIONS", "true");
138        assert!(is_ci());
139        unsafe {
140            std::env::remove_var("GITHUB_ACTIONS");
141        }
142    }
143
144    #[test]
145    fn test_is_ci_with_gitlab() {
146        std::env::set_var("GITLAB_CI", "true");
147        assert!(is_ci());
148        unsafe {
149            std::env::remove_var("GITLAB_CI");
150        }
151    }
152
153    #[test]
154    fn test_should_use_color_no_color() {
155        std::env::set_var("NO_COLOR", "1");
156        unsafe {
157            std::env::remove_var("FORCE_COLOR");
158        }
159        assert!(!should_use_color());
160        unsafe {
161            std::env::remove_var("NO_COLOR");
162        }
163    }
164
165    #[test]
166    fn test_should_use_color_force_color() {
167        unsafe {
168            std::env::remove_var("NO_COLOR");
169        }
170        std::env::set_var("FORCE_COLOR", "1");
171        assert!(should_use_color());
172        unsafe {
173            std::env::remove_var("FORCE_COLOR");
174        }
175    }
176
177    #[test]
178    fn test_should_use_color_no_color_overrides_force() {
179        std::env::set_var("NO_COLOR", "1");
180        std::env::set_var("FORCE_COLOR", "1");
181        // NO_COLOR takes precedence
182        assert!(!should_use_color());
183        unsafe {
184            std::env::remove_var("NO_COLOR");
185            std::env::remove_var("FORCE_COLOR");
186        }
187    }
188
189    #[test]
190    fn test_init_colors() {
191        // Should not panic
192        init_colors();
193    }
194}