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}