1use std::sync::atomic::{AtomicBool, Ordering};
4use std::sync::Arc;
5use std::time::Duration;
6
7pub struct Spinner {
9 message: String,
10 running: Arc<AtomicBool>,
11 handle: Option<tokio::task::JoinHandle<()>>,
12}
13
14impl Spinner {
15 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 pub fn start(&mut self) {
26 if self.running.load(Ordering::SeqCst) {
27 return; }
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)); });
45
46 self.handle = Some(handle);
47 }
48
49 pub fn stop(&mut self) {
51 self.running.store(false, Ordering::SeqCst);
52 if let Some(handle) = self.handle.take() {
53 std::thread::sleep(Duration::from_millis(100));
55 handle.abort();
56 }
57 }
58
59 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
72pub struct ProgressBar {
74 message: String,
75 total: u64,
76 current: u64,
77 width: usize,
78}
79
80impl ProgressBar {
81 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 pub fn set_progress(&mut self, current: u64) {
93 self.current = current.min(self.total);
94 self.render();
95 }
96
97 pub fn inc(&mut self) {
99 self.set_progress(self.current + 1);
100 }
101
102 pub fn inc_by(&mut self, n: u64) {
104 self.set_progress(self.current + n);
105 }
106
107 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!(); }
136 }
137
138 pub fn finish(&mut self) {
140 self.set_progress(self.total);
141 }
142
143 pub fn finish_with_message(&mut self, message: &str) {
145 self.finish();
146 eprintln!("✓ {}", message);
147 }
148}
149
150pub 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}