cascade_cli/utils/
spinner.rs

1use indicatif::{ProgressBar, ProgressStyle};
2use std::time::Duration;
3
4/// Lightweight wrapper around `indicatif`'s spinner progress bar with
5/// convenience helpers for printing output while the spinner is active.
6pub struct Spinner {
7    pb: ProgressBar,
8}
9
10/// Cloneable handle that allows printing while a spinner is active.
11#[derive(Debug, Clone)]
12pub struct SpinnerPrinter {
13    pb: ProgressBar,
14}
15
16impl Spinner {
17    const TICK_RATE: Duration = Duration::from_millis(80);
18    const TEMPLATE: &'static str = "{spinner:.green} {msg}";
19
20    fn new_internal(message: String) -> Self {
21        let pb = ProgressBar::new_spinner();
22        pb.set_style(
23            ProgressStyle::with_template(Self::TEMPLATE)
24                .unwrap_or_else(|_| ProgressStyle::default_spinner()),
25        );
26        pb.set_message(message);
27        pb.enable_steady_tick(Self::TICK_RATE);
28        Spinner { pb }
29    }
30
31    /// Start a spinner with the provided message.
32    pub fn new(message: String) -> Self {
33        Self::new_internal(message)
34    }
35
36    /// Start a spinner intended to have output printed underneath it.
37    ///
38    /// (This currently behaves the same as `new`, but exists to preserve the
39    /// semantics of the previous implementation and allow future tweaks.)
40    pub fn new_with_output_below(message: String) -> Self {
41        Self::new_internal(message)
42    }
43
44    /// Print a line while keeping the spinner intact.
45    pub fn println<T: AsRef<str>>(&self, message: T) {
46        self.pb.println(message.as_ref());
47    }
48
49    /// Obtain a cloneable printer handle that can be used to emit lines from
50    /// other parts of the code while the spinner remains active.
51    pub fn printer(&self) -> SpinnerPrinter {
52        SpinnerPrinter {
53            pb: self.pb.clone(),
54        }
55    }
56
57    /// Temporarily suspend the spinner while executing the provided closure.
58    pub fn suspend<F: FnOnce()>(&self, f: F) {
59        self.pb.suspend(f);
60    }
61
62    /// Stop the spinner and clear it from the terminal.
63    pub fn stop(&self) {
64        self.pb.finish_and_clear();
65    }
66
67    /// Stop the spinner and replace it with a final message.
68    pub fn stop_with_message(&self, message: &str) {
69        self.pb.finish_with_message(message.to_string());
70    }
71
72    /// Update the spinner message while it is running.
73    pub fn update_message(&self, new_message: String) {
74        self.pb.set_message(new_message);
75    }
76}
77
78impl Drop for Spinner {
79    fn drop(&mut self) {
80        if !self.pb.is_finished() {
81            self.pb.finish_and_clear();
82        }
83    }
84}
85
86impl SpinnerPrinter {
87    /// Print a line beneath the spinner.
88    pub fn println<T: AsRef<str>>(&self, message: T) {
89        self.pb.println(message.as_ref());
90    }
91
92    /// Temporarily suspend the spinner while running the provided closure.
93    pub fn suspend<F: FnOnce()>(&self, f: F) {
94        self.pb.suspend(f);
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use std::thread;
102
103    #[test]
104    fn test_spinner_creation_and_stop() {
105        let spinner = Spinner::new("Testing".to_string());
106        thread::sleep(Duration::from_millis(200));
107        spinner.stop();
108    }
109
110    #[test]
111    fn test_spinner_with_message() {
112        let spinner = Spinner::new("Loading".to_string());
113        thread::sleep(Duration::from_millis(200));
114        spinner.stop_with_message("✓ Done");
115    }
116}