1use std::io::{self, Write};
7
8#[derive(Debug)]
10pub struct ProgressBar {
11 total: u64,
12 current: u64,
13 width: usize,
14 message: String,
15 enabled: bool,
16}
17
18impl ProgressBar {
19 pub fn new(total: u64) -> Self {
21 Self {
22 total,
23 current: 0,
24 width: 40,
25 message: String::new(),
26 enabled: true,
27 }
28 }
29
30 pub fn with_width(mut self, width: usize) -> Self {
32 self.width = width;
33 self
34 }
35
36 pub fn with_enabled(mut self, enabled: bool) -> Self {
38 self.enabled = enabled;
39 self
40 }
41
42 pub fn set_message(&mut self, message: impl Into<String>) {
44 self.message = message.into();
45 self.render();
46 }
47
48 pub fn inc(&mut self, amount: u64) {
50 self.current = (self.current + amount).min(self.total);
51 self.render();
52 }
53
54 pub fn set(&mut self, current: u64) {
56 self.current = current.min(self.total);
57 self.render();
58 }
59
60 pub fn percentage(&self) -> f64 {
62 if self.total == 0 {
63 return 100.0;
64 }
65 (self.current as f64 / self.total as f64) * 100.0
66 }
67
68 pub fn finish(&mut self) {
70 self.current = self.total;
71 self.render();
72 if self.enabled {
73 println!();
74 }
75 }
76
77 pub fn finish_with_message(&mut self, message: impl Into<String>) {
79 self.message = message.into();
80 self.finish();
81 }
82
83 fn render(&self) {
84 if !self.enabled {
85 return;
86 }
87
88 let percentage = self.percentage();
89 let filled = (percentage / 100.0 * self.width as f64) as usize;
90 let empty = self.width - filled;
91
92 let bar = format!(
93 "\r[{}{}] {:>5.1}% {}",
94 "█".repeat(filled),
95 "░".repeat(empty),
96 percentage,
97 self.message
98 );
99
100 print!("{bar}");
101 let _ = io::stdout().flush();
102 }
103}
104
105#[derive(Debug)]
107pub struct Spinner {
108 frames: Vec<&'static str>,
109 current_frame: usize,
110 message: String,
111 enabled: bool,
112}
113
114impl Default for Spinner {
115 fn default() -> Self {
116 Self::new()
117 }
118}
119
120impl Spinner {
121 pub fn new() -> Self {
123 Self {
124 frames: vec!["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
125 current_frame: 0,
126 message: String::new(),
127 enabled: true,
128 }
129 }
130
131 pub fn with_enabled(mut self, enabled: bool) -> Self {
133 self.enabled = enabled;
134 self
135 }
136
137 pub fn set_message(&mut self, message: impl Into<String>) {
139 self.message = message.into();
140 }
141
142 pub fn tick(&mut self) {
144 if !self.enabled {
145 return;
146 }
147
148 self.current_frame = (self.current_frame + 1) % self.frames.len();
149 print!("\r{} {}", self.frames[self.current_frame], self.message);
150 let _ = io::stdout().flush();
151 }
152
153 pub fn finish_with_message(&mut self, message: impl Into<String>) {
155 if self.enabled {
156 println!("\r✓ {}", message.into());
157 }
158 }
159
160 pub fn finish_with_error(&mut self, message: impl Into<String>) {
162 if self.enabled {
163 println!("\r✗ {}", message.into());
164 }
165 }
166}
167
168#[derive(Debug)]
170pub struct StepTracker {
171 steps: Vec<String>,
172 current: usize,
173 enabled: bool,
174}
175
176impl StepTracker {
177 pub fn new(steps: Vec<impl Into<String>>) -> Self {
179 Self {
180 steps: steps.into_iter().map(Into::into).collect(),
181 current: 0,
182 enabled: true,
183 }
184 }
185
186 pub fn with_enabled(mut self, enabled: bool) -> Self {
188 self.enabled = enabled;
189 self
190 }
191
192 pub fn next_step(&mut self) {
194 if self.current < self.steps.len() {
195 if self.enabled {
196 println!(
197 "[{}/{}] {}...",
198 self.current + 1,
199 self.steps.len(),
200 self.steps[self.current]
201 );
202 }
203 self.current += 1;
204 }
205 }
206
207 pub fn complete_step(&self, message: impl Into<String>) {
209 if self.enabled {
210 println!(" ✓ {}", message.into());
211 }
212 }
213
214 pub fn fail_step(&self, message: impl Into<String>) {
216 if self.enabled {
217 println!(" ✗ {}", message.into());
218 }
219 }
220
221 pub fn is_complete(&self) -> bool {
223 self.current >= self.steps.len()
224 }
225
226 pub fn total_steps(&self) -> usize {
228 self.steps.len()
229 }
230
231 pub fn current_step(&self) -> usize {
233 self.current
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 #[test]
242 fn test_progress_bar_percentage() {
243 let mut bar = ProgressBar::new(100).with_enabled(false);
244 assert_eq!(bar.percentage(), 0.0);
245
246 bar.set(50);
247 assert_eq!(bar.percentage(), 50.0);
248
249 bar.set(100);
250 assert_eq!(bar.percentage(), 100.0);
251 }
252
253 #[test]
254 fn test_progress_bar_increment() {
255 let mut bar = ProgressBar::new(100).with_enabled(false);
256 bar.inc(25);
257 assert_eq!(bar.current, 25);
258 bar.inc(25);
259 assert_eq!(bar.current, 50);
260 }
261
262 #[test]
263 fn test_progress_bar_overflow_protection() {
264 let mut bar = ProgressBar::new(100).with_enabled(false);
265 bar.set(200);
266 assert_eq!(bar.current, 100);
267 assert_eq!(bar.percentage(), 100.0);
268 }
269
270 #[test]
271 fn test_progress_bar_zero_total() {
272 let bar = ProgressBar::new(0).with_enabled(false);
273 assert_eq!(bar.percentage(), 100.0);
274 }
275
276 #[test]
277 fn test_step_tracker_progression() {
278 let mut tracker = StepTracker::new(vec!["Step 1", "Step 2", "Step 3"]).with_enabled(false);
279
280 assert_eq!(tracker.total_steps(), 3);
281 assert_eq!(tracker.current_step(), 0);
282 assert!(!tracker.is_complete());
283
284 tracker.next_step();
285 assert_eq!(tracker.current_step(), 1);
286
287 tracker.next_step();
288 tracker.next_step();
289 assert!(tracker.is_complete());
290 }
291
292 #[test]
293 fn test_spinner_frames() {
294 let spinner = Spinner::new();
295 assert!(!spinner.frames.is_empty());
296 }
297
298 #[test]
299 fn test_spinner_default() {
300 let spinner = Spinner::default();
301 assert_eq!(spinner.frames.len(), 10);
302 assert!(spinner.enabled);
303 }
304
305 #[test]
306 fn test_spinner_disabled() {
307 let spinner = Spinner::new().with_enabled(false);
308 assert!(!spinner.enabled);
309 }
310
311 #[test]
312 fn test_spinner_message() {
313 let mut spinner = Spinner::new().with_enabled(false);
314 spinner.set_message("Loading...");
315 assert_eq!(spinner.message, "Loading...");
316 }
317
318 #[test]
319 fn test_spinner_tick_cycles_frames() {
320 let mut spinner = Spinner::new();
321 spinner.enabled = false; spinner.current_frame = (spinner.current_frame + 1) % spinner.frames.len();
324 assert_eq!(spinner.current_frame, 1);
325 spinner.current_frame = (spinner.current_frame + 1) % spinner.frames.len();
326 assert_eq!(spinner.current_frame, 2);
327 }
328
329 #[test]
330 fn test_spinner_tick_wraps_around() {
331 let spinner = Spinner::new();
332 let frame_count = spinner.frames.len();
334 let after_15_ticks = 15 % frame_count;
335 assert_eq!(after_15_ticks, 5);
336 }
337
338 #[test]
339 fn test_progress_bar_width() {
340 let bar = ProgressBar::new(100).with_width(20);
341 assert_eq!(bar.width, 20);
342 }
343
344 #[test]
345 fn test_progress_bar_message() {
346 let mut bar = ProgressBar::new(100).with_enabled(false);
347 bar.set_message("Downloading");
348 assert_eq!(bar.message, "Downloading");
349 }
350
351 #[test]
352 fn test_progress_bar_finish() {
353 let mut bar = ProgressBar::new(100).with_enabled(false);
354 bar.set(50);
355 bar.finish();
356 assert_eq!(bar.current, 100);
357 }
358
359 #[test]
360 fn test_progress_bar_finish_with_message() {
361 let mut bar = ProgressBar::new(100).with_enabled(false);
362 bar.finish_with_message("Done!");
363 assert_eq!(bar.message, "Done!");
364 assert_eq!(bar.current, 100);
365 }
366
367 #[test]
368 fn test_step_tracker_empty() {
369 let tracker = StepTracker::new(Vec::<String>::new()).with_enabled(false);
370 assert_eq!(tracker.total_steps(), 0);
371 assert!(tracker.is_complete());
372 }
373
374 #[test]
375 fn test_step_tracker_current_step_1_indexed() {
376 let mut tracker = StepTracker::new(vec!["A", "B"]).with_enabled(false);
377 assert_eq!(tracker.current_step(), 0);
378 tracker.next_step();
379 assert_eq!(tracker.current_step(), 1);
380 tracker.next_step();
381 assert_eq!(tracker.current_step(), 2);
382 }
383
384 #[test]
385 fn test_step_tracker_overflow_protection() {
386 let mut tracker = StepTracker::new(vec!["Only"]).with_enabled(false);
387 tracker.next_step();
388 tracker.next_step(); tracker.next_step();
390 assert!(tracker.is_complete());
391 assert_eq!(tracker.current_step(), 1);
392 }
393}