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: &str) {
50 self.bar.set_message(message.to_string());
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
73pub struct StepProgress {
75 bar: ProgressBar,
76}
77
78impl StepProgress {
79 pub fn new(total: u64, prefix: &str) -> Self {
81 let bar = if atty_stderr() {
82 let pb = ProgressBar::new(total);
83 pb.set_style(
84 ProgressStyle::with_template("{prefix} [{bar:30.cyan/dim}] {pos}/{len} {msg}")
85 .unwrap()
86 .progress_chars("━━╸"),
87 );
88 pb.set_prefix(prefix.to_string());
89 pb
90 } else {
91 eprintln!("{} (0/{})", prefix, total);
92 ProgressBar::hidden()
93 };
94 Self { bar }
95 }
96
97 pub fn inc(&self, message: &str) {
99 self.bar.set_message(message.to_string());
100 self.bar.inc(1);
101 }
102
103 pub fn finish(&self, message: &str) {
105 if !self.bar.is_hidden() {
106 self.bar.finish_with_message(format!("✓ {}", message));
107 }
108 }
109}
110
111fn atty_stderr() -> bool {
113 use std::io::IsTerminal;
114 std::io::stderr().is_terminal()
115}
116
117#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn test_spinner_create_and_finish() {
127 let sp = Spinner::new("Testing...");
129 sp.set_message("Updated");
130 sp.finish("Done");
131 }
132
133 #[test]
134 fn test_spinner_finish_and_clear() {
135 let sp = Spinner::new("Testing...");
136 sp.finish_and_clear();
137 }
138
139 #[test]
140 fn test_spinner_finish_warn() {
141 let sp = Spinner::new("Testing...");
142 sp.finish_warn("Warning");
143 }
144
145 #[test]
146 fn test_step_progress_create_and_finish() {
147 let prog = StepProgress::new(3, "Processing");
148 prog.inc("Step 1");
149 prog.inc("Step 2");
150 prog.inc("Step 3");
151 prog.finish("Done");
152 }
153
154 #[test]
155 fn test_spinner_multiple_set_message() {
156 let sp = Spinner::new("Initial");
157 sp.set_message("First update");
158 sp.set_message("Second update");
159 sp.set_message("Third update");
160 sp.finish("Complete");
161 }
162
163 #[test]
164 fn test_step_progress_multiple_inc() {
165 let prog = StepProgress::new(5, "Processing");
166 prog.inc("Step 1");
167 prog.inc("Step 2");
168 prog.inc("Step 3");
169 prog.inc("Step 4");
170 prog.inc("Step 5");
171 prog.finish("Done");
172 }
173
174 #[test]
175 fn test_step_progress_single_step() {
176 let prog = StepProgress::new(1, "Single");
177 prog.inc("Only step");
178 prog.finish("Complete");
179 }
180
181 #[test]
182 fn test_step_progress_large_total() {
183 let prog = StepProgress::new(100, "Large");
184 for i in 1..=100 {
185 prog.inc(&format!("Step {}", i));
186 }
187 prog.finish("Complete");
188 }
189
190 #[test]
191 fn test_step_progress_zero_total() {
192 let prog = StepProgress::new(0, "Empty");
193 prog.finish("Complete");
194 }
195
196 #[test]
197 fn test_spinner_finish_warn_with_message() {
198 let sp = Spinner::new("Warning test");
199 sp.finish_warn("Something went wrong");
200 }
201
202 #[test]
203 fn test_spinner_finish_warn_multiple_calls() {
204 let sp = Spinner::new("Test");
205 sp.finish_warn("First warning");
206 sp.finish_warn("Second warning");
208 }
209}