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
23        Self { pb }
24    }
25
26    /// Create a new spinner with a download-style progress bar
27    pub fn new_download_style(message: &str, total_size: u64) -> Self {
28        let pb = ProgressBar::new(total_size);
29        pb.set_style(ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})")
30            .unwrap()
31            .progress_chars("#>-"));
32        pb.set_message(message.to_string());
33
34        Self { pb }
35    }
36
37    /// Update the spinner message
38    pub fn set_message(&self, message: &str) {
39        self.pb.set_message(message.to_string());
40    }
41
42    /// Set the position for progress bar (useful for download-style spinners)
43    pub fn set_position(&self, pos: u64) {
44        self.pb.set_position(pos);
45    }
46
47    /// Set the total size for progress bar
48    pub fn set_length(&self, len: u64) {
49        self.pb.set_length(len);
50    }
51
52    /// Finish the spinner with a success message
53    pub fn finish_with_message(&self, message: &str) {
54        self.pb.finish_with_message(message.to_string());
55    }
56
57    /// Finish the spinner and clear the line
58    pub fn finish_and_clear(&self) {
59        self.pb.finish_and_clear();
60    }
61
62    /// Finish the spinner with an error message
63    pub fn finish_with_error(&self, message: &str) {
64        self.pb.abandon_with_message(format!("❌ {}", message));
65    }
66
67    /// Get a clone of the ProgressBar for use in other threads
68    pub fn clone_inner(&self) -> ProgressBar {
69        self.pb.clone()
70    }
71}
72
73/// Show a simple loading spinner with the given message
74/// This is a convenience function that creates and immediately starts a spinner
75pub fn show_loading_spinner(message: &str) -> Spinner {
76    let spinner = Spinner::new(message);
77    spinner.pb.tick();
78    spinner
79}
80
81/// Start a loading spinner in a background thread for long-running tasks
82/// Returns a handle that can be used to control the spinner
83pub fn start_loading_spinner(message: &str) -> Spinner {
84    let spinner = Spinner::new(message);
85    spinner.pb.tick();
86    spinner
87}
88
89/// Start a download-style progress bar spinner
90/// Useful for tasks with known total size
91pub fn start_download_spinner(message: &str, total_size: u64) -> Spinner {
92    let spinner = Spinner::new_download_style(message, total_size);
93    spinner.pb.tick();
94    spinner
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use std::thread;
101    use std::time::Duration;
102
103    #[test]
104    fn test_spinner_creation() {
105        let spinner = Spinner::new("Testing spinner");
106        assert!(spinner.clone_inner().length().is_none());
107        spinner.finish_and_clear();
108    }
109
110    #[test]
111    fn test_show_loading_spinner() {
112        let spinner = show_loading_spinner("Test message");
113        thread::sleep(Duration::from_millis(200));
114        spinner.finish_with_message("Done");
115    }
116
117    #[test]
118    fn test_download_spinner() {
119        let spinner = start_download_spinner("Downloading", 100);
120        spinner.set_position(50);
121        thread::sleep(Duration::from_millis(200));
122        spinner.finish_with_message("Download complete");
123    }
124}