1use indicatif::{ProgressBar, ProgressStyle};
18use std::time::Duration;
19
20pub struct Spinner {
24 bar: ProgressBar,
25}
26
27impl Spinner {
28 pub fn new(message: &str) -> Self {
30 let bar = if atty_stderr() {
31 let pb = ProgressBar::new_spinner();
32 pb.set_style(
33 ProgressStyle::with_template("{spinner:.cyan} {msg}")
34 .unwrap()
35 .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
36 );
37 pb.set_message(message.to_string());
38 pb.enable_steady_tick(Duration::from_millis(80));
39 pb
40 } else {
41 eprintln!("{}", message);
43 ProgressBar::hidden()
44 };
45 Self { bar }
46 }
47
48 pub fn set_message(&self, message: impl Into<String>) {
50 self.bar.set_message(message.into());
51 }
52
53 pub fn finish(&self, message: &str) {
55 if !self.bar.is_hidden() {
56 self.bar.finish_with_message(format!("✓ {}", message));
57 }
58 }
59
60 pub fn finish_warn(&self, message: &str) {
62 if !self.bar.is_hidden() {
63 self.bar.finish_with_message(format!("⚠ {}", message));
64 }
65 }
66
67 pub fn finish_and_clear(&self) {
69 self.bar.finish_and_clear();
70 }
71
72 pub fn println(&self, message: &str) {
77 if self.bar.is_hidden() {
78 eprintln!("{}", message);
79 } else {
80 self.bar.println(message);
81 }
82 }
83
84 pub fn suspend<R, F: FnOnce() -> R>(&self, f: F) -> R {
89 if self.bar.is_hidden() {
90 f()
91 } else {
92 self.bar.suspend(f)
93 }
94 }
95}
96
97pub struct StepProgress {
99 bar: ProgressBar,
100}
101
102impl StepProgress {
103 pub fn new(total: u64, prefix: &str) -> Self {
105 let bar = if atty_stderr() {
106 let pb = ProgressBar::new(total);
107 pb.set_style(
108 ProgressStyle::with_template("{prefix} [{bar:30.cyan/dim}] {pos}/{len} {msg}")
109 .unwrap()
110 .progress_chars("━━╸"),
111 );
112 pb.set_prefix(prefix.to_string());
113 pb
114 } else {
115 eprintln!("{} (0/{})", prefix, total);
116 ProgressBar::hidden()
117 };
118 Self { bar }
119 }
120
121 pub fn inc(&self, message: &str) {
123 self.bar.set_message(message.to_string());
124 self.bar.inc(1);
125 }
126
127 pub fn finish(&self, message: &str) {
129 if !self.bar.is_hidden() {
130 self.bar.finish_with_message(format!("✓ {}", message));
131 }
132 }
133}
134
135fn atty_stderr() -> bool {
137 use std::io::IsTerminal;
138 std::io::stderr().is_terminal()
139}
140
141#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn test_spinner_create_and_finish() {
151 let sp = Spinner::new("Testing...");
153 sp.set_message("Updated");
154 sp.finish("Done");
155 }
156
157 #[test]
158 fn test_spinner_finish_and_clear() {
159 let sp = Spinner::new("Testing...");
160 sp.finish_and_clear();
161 }
162
163 #[test]
164 fn test_spinner_finish_warn() {
165 let sp = Spinner::new("Testing...");
166 sp.finish_warn("Warning");
167 }
168
169 #[test]
170 fn test_step_progress_create_and_finish() {
171 let prog = StepProgress::new(3, "Processing");
172 prog.inc("Step 1");
173 prog.inc("Step 2");
174 prog.inc("Step 3");
175 prog.finish("Done");
176 }
177
178 #[test]
179 fn test_spinner_multiple_set_message() {
180 let sp = Spinner::new("Initial");
181 sp.set_message("First update");
182 sp.set_message("Second update");
183 sp.set_message("Third update");
184 sp.finish("Complete");
185 }
186
187 #[test]
188 fn test_step_progress_multiple_inc() {
189 let prog = StepProgress::new(5, "Processing");
190 prog.inc("Step 1");
191 prog.inc("Step 2");
192 prog.inc("Step 3");
193 prog.inc("Step 4");
194 prog.inc("Step 5");
195 prog.finish("Done");
196 }
197
198 #[test]
199 fn test_step_progress_single_step() {
200 let prog = StepProgress::new(1, "Single");
201 prog.inc("Only step");
202 prog.finish("Complete");
203 }
204
205 #[test]
206 fn test_step_progress_large_total() {
207 let prog = StepProgress::new(100, "Large");
208 for i in 1..=100 {
209 prog.inc(&format!("Step {}", i));
210 }
211 prog.finish("Complete");
212 }
213
214 #[test]
215 fn test_step_progress_zero_total() {
216 let prog = StepProgress::new(0, "Empty");
217 prog.finish("Complete");
218 }
219
220 #[test]
221 fn test_spinner_finish_warn_with_message() {
222 let sp = Spinner::new("Warning test");
223 sp.finish_warn("Something went wrong");
224 }
225
226 #[test]
227 fn test_spinner_finish_warn_multiple_calls() {
228 let sp = Spinner::new("Test");
229 sp.finish_warn("First warning");
230 sp.finish_warn("Second warning");
232 }
233
234 #[test]
235 fn test_spinner_println() {
236 let sp = Spinner::new("Working...");
237 sp.println("A message above the spinner");
238 sp.finish("Done");
239 }
240
241 #[test]
242 fn test_spinner_suspend() {
243 let sp = Spinner::new("Working...");
244 sp.suspend(|| {
245 eprintln!("Suspended output");
247 });
248 sp.finish("Done");
249 }
250}