mielin_cli/
progress.rs

1//! Progress indicators for long-running operations
2
3use std::sync::atomic::{AtomicBool, Ordering};
4use std::sync::Arc;
5use std::time::Duration;
6
7/// Simple spinner for progress indication
8pub struct Spinner {
9    message: String,
10    running: Arc<AtomicBool>,
11    handle: Option<tokio::task::JoinHandle<()>>,
12}
13
14impl Spinner {
15    /// Create a new spinner with a message
16    pub fn new(message: impl Into<String>) -> Self {
17        Self {
18            message: message.into(),
19            running: Arc::new(AtomicBool::new(false)),
20            handle: None,
21        }
22    }
23
24    /// Start the spinner
25    pub fn start(&mut self) {
26        if self.running.load(Ordering::SeqCst) {
27            return; // Already running
28        }
29
30        self.running.store(true, Ordering::SeqCst);
31        let running = Arc::clone(&self.running);
32        let message = self.message.clone();
33
34        let handle = tokio::spawn(async move {
35            let frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
36            let mut frame_idx = 0;
37
38            while running.load(Ordering::SeqCst) {
39                eprint!("\r{} {}", frames[frame_idx], message);
40                frame_idx = (frame_idx + 1) % frames.len();
41                tokio::time::sleep(Duration::from_millis(80)).await;
42            }
43            eprint!("\r{}\r", " ".repeat(message.len() + 10)); // Clear the line
44        });
45
46        self.handle = Some(handle);
47    }
48
49    /// Stop the spinner
50    pub fn stop(&mut self) {
51        self.running.store(false, Ordering::SeqCst);
52        if let Some(handle) = self.handle.take() {
53            // Wait a bit for the spinner to clean up
54            std::thread::sleep(Duration::from_millis(100));
55            handle.abort();
56        }
57    }
58
59    /// Stop the spinner with a completion message
60    pub fn finish_with_message(&mut self, message: &str) {
61        self.stop();
62        eprintln!("✓ {}", message);
63    }
64}
65
66impl Drop for Spinner {
67    fn drop(&mut self) {
68        self.stop();
69    }
70}
71
72/// Progress bar for operations with known total
73pub struct ProgressBar {
74    message: String,
75    total: u64,
76    current: u64,
77    width: usize,
78}
79
80impl ProgressBar {
81    /// Create a new progress bar
82    pub fn new(message: impl Into<String>, total: u64) -> Self {
83        Self {
84            message: message.into(),
85            total,
86            current: 0,
87            width: 40,
88        }
89    }
90
91    /// Update progress
92    pub fn set_progress(&mut self, current: u64) {
93        self.current = current.min(self.total);
94        self.render();
95    }
96
97    /// Increment progress by 1
98    pub fn inc(&mut self) {
99        self.set_progress(self.current + 1);
100    }
101
102    /// Increment progress by n
103    pub fn inc_by(&mut self, n: u64) {
104        self.set_progress(self.current + n);
105    }
106
107    /// Render the progress bar
108    fn render(&self) {
109        let percentage = if self.total > 0 {
110            (self.current as f64 / self.total as f64 * 100.0) as u64
111        } else {
112            0
113        };
114
115        let filled = if self.total > 0 {
116            (self.current as f64 / self.total as f64 * self.width as f64) as usize
117        } else {
118            0
119        };
120
121        let empty = self.width.saturating_sub(filled);
122
123        eprint!(
124            "\r{} [{}{}] {}/{}  ({}%)",
125            self.message,
126            "█".repeat(filled),
127            "░".repeat(empty),
128            self.current,
129            self.total,
130            percentage
131        );
132
133        if self.current >= self.total {
134            eprintln!(); // New line when complete
135        }
136    }
137
138    /// Finish the progress bar
139    pub fn finish(&mut self) {
140        self.set_progress(self.total);
141    }
142
143    /// Finish with a custom message
144    pub fn finish_with_message(&mut self, message: &str) {
145        self.finish();
146        eprintln!("✓ {}", message);
147    }
148}
149
150/// Execute a future with a spinner
151pub async fn with_spinner<F, T>(message: impl Into<String>, future: F) -> T
152where
153    F: std::future::Future<Output = T>,
154{
155    let mut spinner = Spinner::new(message);
156    spinner.start();
157    let result = future.await;
158    spinner.stop();
159    result
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[tokio::test]
167    async fn test_spinner_creation() {
168        let spinner = Spinner::new("Testing");
169        assert!(!spinner.running.load(Ordering::SeqCst));
170    }
171
172    #[tokio::test]
173    async fn test_progress_bar() {
174        let mut bar = ProgressBar::new("Testing", 100);
175        assert_eq!(bar.current, 0);
176        assert_eq!(bar.total, 100);
177
178        bar.set_progress(50);
179        assert_eq!(bar.current, 50);
180
181        bar.inc();
182        assert_eq!(bar.current, 51);
183
184        bar.inc_by(10);
185        assert_eq!(bar.current, 61);
186
187        bar.finish();
188        assert_eq!(bar.current, 100);
189    }
190
191    #[tokio::test]
192    async fn test_with_spinner() {
193        let result = with_spinner("Processing", async {
194            tokio::time::sleep(Duration::from_millis(100)).await;
195            42
196        })
197        .await;
198
199        assert_eq!(result, 42);
200    }
201}