vtcode_core/ui/
spinner.rs

1//! Loading spinner utilities for terminal UI using indicatif crate
2
3use indicatif::{ProgressBar, ProgressStyle};
4use std::time::Duration;
5
6/// A wrapper around indicatif's ProgressBar for easy spinner management
7pub struct Spinner {
8    pb: ProgressBar,
9}
10
11impl Spinner {
12    /// Create a new spinner with the given message
13    pub fn new(message: &str) -> Self {
14        let pb = ProgressBar::new_spinner();
15        pb.set_style(
16            ProgressStyle::with_template("{spinner:.green} {msg}")
17                .unwrap()
18                .tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"),
19        );
20        pb.set_message(message.to_string());
21        pb.enable_steady_tick(Duration::from_millis(100));
22        pb.tick(); // Ensure spinner displays immediately
23
24        Self { pb }
25    }
26
27    /// Create a new spinner with a download-style progress bar
28    pub fn new_download_style(message: &str, total_size: u64) -> Self {
29        let pb = ProgressBar::new(total_size);
30        pb.set_style(ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})")
31            .unwrap()
32            .progress_chars("#>-"));
33        pb.set_message(message.to_string());
34
35        Self { pb }
36    }
37
38    /// Update the spinner message
39    pub fn set_message(&self, message: &str) {
40        self.pb.set_message(message.to_string());
41    }
42
43    /// Set the position for progress bar (useful for download-style spinners)
44    pub fn set_position(&self, pos: u64) {
45        self.pb.set_position(pos);
46    }
47
48    /// Set the total size for progress bar
49    pub fn set_length(&self, len: u64) {
50        self.pb.set_length(len);
51    }
52
53    /// Finish the spinner with a success message
54    pub fn finish_with_message(&self, message: &str) {
55        self.pb.finish_with_message(message.to_string());
56    }
57
58    /// Finish the spinner and clear the line
59    pub fn finish_and_clear(&self) {
60        self.pb.finish_and_clear();
61    }
62
63    /// Finish the spinner with an error message
64    pub fn finish_with_error(&self, message: &str) {
65        self.pb.abandon_with_message(format!("❌ {}", message));
66    }
67
68    /// Get a clone of the ProgressBar for use in other threads
69    pub fn clone_inner(&self) -> ProgressBar {
70        self.pb.clone()
71    }
72
73    /// Create a new spinner that runs in a tokio task for better async integration
74    pub fn new_async(message: &str) -> Self {
75        let pb = ProgressBar::new_spinner();
76        pb.set_style(
77            ProgressStyle::with_template("{spinner:.green} {msg}")
78                .unwrap()
79                .tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"),
80        );
81        pb.set_message(message.to_string());
82        pb.enable_steady_tick(Duration::from_millis(100));
83        pb.tick(); // Ensure spinner displays immediately
84
85        Self { pb }
86    }
87
88    /// Run the spinner in a tokio task and return a handle to control it
89    pub fn spawn_async(self) -> tokio::task::JoinHandle<()> {
90        tokio::spawn(async move {
91            // Keep the spinner running until the task is cancelled
92            loop {
93                tokio::time::sleep(Duration::from_millis(100)).await;
94                // The spinner will be automatically cleaned up when the task ends
95            }
96        })
97    }
98}
99
100/// Show a simple loading spinner with the given message
101/// This is a convenience function that creates and immediately starts a spinner
102pub fn show_loading_spinner(message: &str) -> Spinner {
103    let spinner = Spinner::new(message);
104    spinner.pb.tick();
105    spinner
106}
107
108/// Start a loading spinner in a background thread for long-running tasks
109/// Returns a handle that can be used to control the spinner
110pub fn start_loading_spinner(message: &str) -> Spinner {
111    let spinner = Spinner::new(message);
112    spinner.pb.tick();
113    spinner
114}
115
116/// Start a download-style progress bar spinner
117/// Useful for tasks with known total size
118pub fn start_download_spinner(message: &str, total_size: u64) -> Spinner {
119    let spinner = Spinner::new_download_style(message, total_size);
120    spinner.pb.tick();
121    spinner
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use std::thread;
128    use std::time::Duration;
129
130    #[test]
131    fn test_spinner_creation() {
132        let spinner = Spinner::new("Testing spinner");
133        assert!(spinner.clone_inner().length().is_none());
134        spinner.finish_and_clear();
135    }
136
137    #[test]
138    fn test_show_loading_spinner() {
139        let spinner = show_loading_spinner("Test message");
140        thread::sleep(Duration::from_millis(200));
141        spinner.finish_with_message("Done");
142    }
143
144    #[test]
145    fn test_download_spinner() {
146        let spinner = start_download_spinner("Downloading", 100);
147        spinner.set_position(50);
148        thread::sleep(Duration::from_millis(200));
149        spinner.finish_with_message("Download complete");
150    }
151}