Skip to main content

cargo_plugin_utils/
progress_logger.rs

1//! Progress bar logger for cargo-style output with quiet mode support.
2
3use std::io::IsTerminal;
4
5use indicatif::{
6    ProgressBar,
7    ProgressStyle,
8};
9
10/// Logger for handling output with quiet mode and cargo-style progress bars.
11///
12/// This logger is designed for operations with known progress (like processing
13/// multiple files). It uses progress bars rather than spinners.
14pub struct ProgressLogger {
15    quiet: bool,
16    progress: Option<ProgressBar>,
17}
18
19impl ProgressLogger {
20    /// Create a new progress logger.
21    ///
22    /// * `quiet` - If true, suppresses all output
23    pub fn new(quiet: bool) -> Self {
24        Self {
25            quiet,
26            progress: None,
27        }
28    }
29
30    /// Check if progress should be shown based on cargo's term.progress.when
31    /// setting (respects CARGO_TERM_PROGRESS_WHEN environment variable).
32    ///
33    /// Returns `true` if progress should be shown, `false` otherwise.
34    #[allow(clippy::disallowed_methods)] // CLI tool needs direct env access
35    pub fn should_show_progress(&self) -> bool {
36        if self.quiet {
37            return false;
38        }
39        // Respect cargo's term.progress.when setting
40        // Values: "auto" (default), "always", "never"
41        match std::env::var("CARGO_TERM_PROGRESS_WHEN")
42            .as_deref()
43            .unwrap_or("auto")
44        {
45            "never" => false,
46            "always" => true,
47            "auto" => {
48                // Auto: show if stdout is a TTY (interactive terminal)
49                std::io::stdout().is_terminal()
50            }
51            _ => {
52                // Default to auto behavior for unknown values
53                std::io::stdout().is_terminal()
54            }
55        }
56    }
57
58    /// Set a status message with a progress bar (ephemeral, like cargo's
59    /// "Compiling").
60    ///
61    /// * `total` - Total number of items to process
62    pub fn set_progress(&mut self, total: u64) {
63        if !self.should_show_progress() {
64            return;
65        }
66        let pb = ProgressBar::new(total);
67        // Match cargo's progress bar style
68        pb.set_style(
69            ProgressStyle::default_bar()
70                .template("{spinner:.green} {msg} [{bar:40.cyan/blue}] {pos}/{len}")
71                .unwrap()
72                .progress_chars("#>-"),
73        );
74        self.progress = Some(pb);
75    }
76
77    /// Update progress status message.
78    pub fn set_message(&self, msg: &str) {
79        if let Some(pb) = &self.progress {
80            pb.set_message(msg.to_string());
81        }
82    }
83
84    /// Increment progress by 1.
85    pub fn inc(&self) {
86        if let Some(pb) = &self.progress {
87            pb.inc(1);
88        }
89    }
90
91    /// Print a permanent message (will be kept in output).
92    ///
93    /// Format matches cargo's style: "   ✓ message" or "   message"
94    pub fn println(&mut self, msg: &str) {
95        if !self.quiet {
96            // If we have an active progress bar, suspend it while printing
97            if let Some(pb) = &self.progress {
98                pb.suspend(|| {
99                    println!("{}", msg);
100                });
101            } else {
102                println!("{}", msg);
103            }
104        }
105    }
106
107    /// Print a status message in cargo's style: "   Compiling crate-name".
108    pub fn status(&mut self, action: &str, target: &str) {
109        if !self.quiet {
110            if let Some(pb) = &self.progress {
111                pb.suspend(|| {
112                    println!("   {} {}", action, target);
113                });
114            } else {
115                println!("   {} {}", action, target);
116            }
117        }
118    }
119
120    /// Clear/finish the progress bar.
121    pub fn finish(&mut self) {
122        if let Some(pb) = self.progress.take() {
123            pb.finish_and_clear();
124        }
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn test_progress_logger_new() {
134        let logger = ProgressLogger::new(false);
135        assert!(!logger.quiet);
136        assert!(logger.progress.is_none());
137    }
138
139    #[test]
140    fn test_progress_logger_quiet() {
141        let logger = ProgressLogger::new(true);
142        assert!(logger.quiet);
143        assert!(!logger.should_show_progress());
144    }
145
146    #[test]
147    fn test_progress_logger_set_progress() {
148        let mut logger = ProgressLogger::new(false);
149        logger.set_progress(10);
150        // Progress bar should be created if TTY or CARGO_TERM_PROGRESS_WHEN
151        // allows (we can't easily test this without mocking, but the
152        // function should complete)
153    }
154
155    #[test]
156    fn test_progress_logger_inc() {
157        let mut logger = ProgressLogger::new(false);
158        logger.set_progress(10);
159        logger.inc();
160        // Should not panic
161    }
162
163    #[test]
164    fn test_progress_logger_finish() {
165        let mut logger = ProgressLogger::new(false);
166        logger.set_progress(10);
167        logger.finish();
168        assert!(logger.progress.is_none());
169    }
170}