cascade_cli/utils/
spinner.rs1use std::io::{self, Write};
2use std::sync::atomic::{AtomicBool, Ordering};
3use std::sync::Arc;
4use std::thread;
5use std::time::Duration;
6
7pub struct Spinner {
21 running: Arc<AtomicBool>,
22 handle: Option<thread::JoinHandle<()>>,
23 message: String,
24}
25
26impl Spinner {
27 const FRAMES: &'static [&'static str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
29
30 const FRAME_DURATION_MS: u64 = 80;
32
33 pub fn new(message: String) -> Self {
37 let running = Arc::new(AtomicBool::new(true));
38 let running_clone = Arc::clone(&running);
39 let message_clone = message.clone();
40
41 let handle = thread::spawn(move || {
42 let mut frame_idx = 0;
43 while running_clone.load(Ordering::Relaxed) {
44 let frame = Self::FRAMES[frame_idx % Self::FRAMES.len()];
45 print!("\r{} {}...", frame, message_clone);
46 io::stdout().flush().ok();
47
48 frame_idx += 1;
49 thread::sleep(Duration::from_millis(Self::FRAME_DURATION_MS));
50 }
51 });
52
53 Spinner {
54 running,
55 handle: Some(handle),
56 message,
57 }
58 }
59
60 pub fn stop(&mut self) {
64 if !self.running.load(Ordering::Relaxed) {
65 return; }
67
68 self.running.store(false, Ordering::Relaxed);
69
70 if let Some(handle) = self.handle.take() {
71 handle.join().ok();
72 }
73
74 self.clear_line();
76 }
77
78 pub fn stop_with_message(&mut self, message: &str) {
82 if !self.running.load(Ordering::Relaxed) {
83 return; }
85
86 self.running.store(false, Ordering::Relaxed);
87
88 if let Some(handle) = self.handle.take() {
89 handle.join().ok();
90 }
91
92 self.clear_line();
94 println!("{}", message);
95 }
96
97 pub fn update_message(&mut self, new_message: String) {
102 self.stop();
103 *self = Self::new(new_message);
104 }
105
106 fn clear_line(&self) {
108 let clear_width = self.message.len() + 10;
110 print!("\r{}\r", " ".repeat(clear_width));
111 io::stdout().flush().ok();
112 }
113}
114
115impl Drop for Spinner {
116 fn drop(&mut self) {
117 if self.running.load(Ordering::Relaxed) {
120 self.stop();
121 }
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn test_spinner_creation_and_stop() {
131 let mut spinner = Spinner::new("Testing".to_string());
132 thread::sleep(Duration::from_millis(200));
133 spinner.stop();
134 }
135
136 #[test]
137 fn test_spinner_with_message() {
138 let mut spinner = Spinner::new("Loading".to_string());
139 thread::sleep(Duration::from_millis(200));
140 spinner.stop_with_message("✓ Done");
141 }
142}