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 const COLOR_START: &'static str = "\x1b[38;5;35m";
35 const COLOR_END: &'static str = "\x1b[0m";
36
37 pub fn new(message: String) -> Self {
41 let running = Arc::new(AtomicBool::new(true));
42 let running_clone = Arc::clone(&running);
43 let message_clone = message.clone();
44
45 let handle = thread::spawn(move || {
46 let mut frame_idx = 0;
47 while running_clone.load(Ordering::Relaxed) {
48 let frame = Self::FRAMES[frame_idx % Self::FRAMES.len()];
49 print!(
50 "\r{}{}{} {}...",
51 Self::COLOR_START,
52 frame,
53 Self::COLOR_END,
54 message_clone
55 );
56 io::stdout().flush().ok();
57
58 frame_idx += 1;
59 thread::sleep(Duration::from_millis(Self::FRAME_DURATION_MS));
60 }
61 });
62
63 Spinner {
64 running,
65 handle: Some(handle),
66 message,
67 }
68 }
69
70 pub fn new_with_output_below(message: String) -> Self {
76 let running = Arc::new(AtomicBool::new(true));
77 let running_clone = Arc::clone(&running);
78 let message_clone = message.clone();
79
80 println!(
82 "{}{}{} {}...",
83 Self::COLOR_START,
84 Self::FRAMES[0],
85 Self::COLOR_END,
86 message_clone
87 );
88 io::stdout().flush().ok();
89
90 let handle = thread::spawn(move || {
91 let mut frame_idx = 1; while running_clone.load(Ordering::Relaxed) {
93 let frame = Self::FRAMES[frame_idx % Self::FRAMES.len()];
94
95 print!(
98 "\x1b[1A\x1b[0G{}{}{}\x1b[1B\x1b[0G",
99 Self::COLOR_START,
100 frame,
101 Self::COLOR_END
102 );
103 io::stdout().flush().ok();
104
105 frame_idx += 1;
106 thread::sleep(Duration::from_millis(Self::FRAME_DURATION_MS));
107 }
108 });
109
110 Spinner {
111 running,
112 handle: Some(handle),
113 message,
114 }
115 }
116
117 pub fn stop(&mut self) {
121 if !self.running.load(Ordering::Relaxed) {
122 return; }
124
125 self.running.store(false, Ordering::Relaxed);
126
127 if let Some(handle) = self.handle.take() {
128 handle.join().ok();
129 }
130
131 self.clear_line();
133 }
134
135 pub fn stop_with_message(&mut self, message: &str) {
139 if !self.running.load(Ordering::Relaxed) {
140 return; }
142
143 self.running.store(false, Ordering::Relaxed);
144
145 if let Some(handle) = self.handle.take() {
146 handle.join().ok();
147 }
148
149 self.clear_line();
151 println!("{}", message);
152 }
153
154 pub fn update_message(&mut self, new_message: String) {
159 self.stop();
160 *self = Self::new(new_message);
161 }
162
163 fn clear_line(&self) {
165 let clear_width = self.message.len() + 10;
167 print!("\r{}\r", " ".repeat(clear_width));
168 io::stdout().flush().ok();
169 }
170}
171
172impl Drop for Spinner {
173 fn drop(&mut self) {
174 if self.running.load(Ordering::Relaxed) {
177 self.stop();
178 }
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn test_spinner_creation_and_stop() {
188 let mut spinner = Spinner::new("Testing".to_string());
189 thread::sleep(Duration::from_millis(200));
190 spinner.stop();
191 }
192
193 #[test]
194 fn test_spinner_with_message() {
195 let mut spinner = Spinner::new("Loading".to_string());
196 thread::sleep(Duration::from_millis(200));
197 spinner.stop_with_message("✓ Done");
198 }
199}