cargo_plugin_utils/
progress_logger.rs

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