1use crate::progress::{ProgressBar, Task};
5use crate::spinner::Spinner;
6use crate::style::Style;
7
8pub trait ProgressColumn: std::fmt::Debug {
14 fn render(&self, task: &Task, width: usize, elapsed: std::time::Duration) -> String;
16}
17
18#[derive(Debug, Clone)]
25pub struct TextColumn {
26 pub key: String,
28 pub format: String,
30 pub style: Style,
32}
33
34impl TextColumn {
35 pub fn new(key: impl Into<String>) -> Self {
37 Self {
38 key: key.into(),
39 format: "{:>11}".to_string(),
40 style: Style::new(),
41 }
42 }
43
44 pub fn format(mut self, fmt: impl Into<String>) -> Self {
46 self.format = fmt.into();
47 self
48 }
49 pub fn style(mut self, s: Style) -> Self {
51 self.style = s;
52 self
53 }
54}
55
56impl ProgressColumn for TextColumn {
57 fn render(&self, task: &Task, _width: usize, _elapsed: std::time::Duration) -> String {
58 let value = task
59 .fields
60 .get(&self.key)
61 .map(|s| s.as_str())
62 .unwrap_or("?");
63 let ansi = self.style.to_ansi();
66 let reset = self.style.reset_ansi();
67 format!("{ansi}{value}{reset}")
68 }
69}
70
71#[derive(Debug, Clone)]
77pub struct BarColumn {
78 pub bar: ProgressBar,
80 pub width: Option<usize>,
82}
83
84impl BarColumn {
85 pub fn new() -> Self {
87 Self {
88 bar: ProgressBar::new(),
89 width: None,
90 }
91 }
92
93 pub fn complete_style(mut self, s: Style) -> Self {
95 self.bar = self.bar.complete_style(s);
96 self
97 }
98 pub fn finished_style(mut self, s: Style) -> Self {
100 self.bar = self.bar.remaining_style(s);
101 self
102 }
103 pub fn width(mut self, w: usize) -> Self {
105 self.width = Some(w);
106 self
107 }
108}
109
110impl ProgressColumn for BarColumn {
111 fn render(&self, task: &Task, width: usize, _elapsed: std::time::Duration) -> String {
112 let w = self.width.unwrap_or(width.saturating_sub(2));
113 let mut bar = self.bar.clone();
114 bar.total = task.total;
115 bar.completed = task.completed;
116 bar.width = Some(w);
117 bar.render(w)
118 }
119}
120
121impl Default for BarColumn {
122 fn default() -> Self {
123 Self::new()
124 }
125}
126
127#[derive(Debug, Clone)]
133pub struct SpinnerColumn {
134 pub spinner: Spinner,
135 pub style: Style,
136 pub finished_style: Style,
137 pub finished_text: String,
138}
139
140impl SpinnerColumn {
141 pub fn new() -> Self {
143 Self {
144 spinner: Spinner::default(),
145 style: Style::new(),
146 finished_style: Style::new()
147 .color(crate::color::Color::parse("green").unwrap())
148 .bold(true),
149 finished_text: "✓".to_string(),
150 }
151 }
152
153 pub fn style(mut self, s: Style) -> Self {
155 self.style = s;
156 self
157 }
158 pub fn finished_style(mut self, s: Style) -> Self {
160 self.finished_style = s;
161 self
162 }
163}
164
165impl ProgressColumn for SpinnerColumn {
166 fn render(&self, task: &Task, _width: usize, elapsed: std::time::Duration) -> String {
167 if task.is_finished() {
168 let a = self.finished_style.to_ansi();
169 let r = self.finished_style.reset_ansi();
170 format!("{a}{}{r}", self.finished_text)
171 } else {
172 let frame = self.spinner.frame_at(elapsed);
173 let a = self.style.to_ansi();
174 let r = self.style.reset_ansi();
175 format!("{a}{frame}{r}")
176 }
177 }
178}
179
180impl Default for SpinnerColumn {
181 fn default() -> Self {
182 Self::new()
183 }
184}
185
186#[derive(Debug, Clone)]
192pub struct TimeElapsedColumn {
193 pub style: Style,
194 pub paused_style: Style,
195}
196
197impl TimeElapsedColumn {
198 pub fn new() -> Self {
200 Self {
201 style: Style::new(),
202 paused_style: Style::new().dim(true),
203 }
204 }
205}
206
207impl ProgressColumn for TimeElapsedColumn {
208 fn render(&self, task: &Task, _width: usize, _elapsed: std::time::Duration) -> String {
209 let d = task.elapsed();
210 let s = format_duration_short(&d);
211 let a = self.style.to_ansi();
212 let r = self.style.reset_ansi();
213 format!("{a}{s}{r}")
214 }
215}
216
217impl Default for TimeElapsedColumn {
218 fn default() -> Self {
219 Self::new()
220 }
221}
222
223#[derive(Debug, Clone)]
229pub struct TimeRemainingColumn {
230 pub style: Style,
231 pub elapsed_when_finished: bool,
232}
233
234impl TimeRemainingColumn {
235 pub fn new() -> Self {
236 Self {
237 style: Style::new(),
238 elapsed_when_finished: false,
239 }
240 }
241}
242
243impl ProgressColumn for TimeRemainingColumn {
244 fn render(&self, task: &Task, _width: usize, _elapsed: std::time::Duration) -> String {
245 let text = if task.is_finished() {
246 if self.elapsed_when_finished {
247 format_duration_short(&task.elapsed())
248 } else {
249 String::new()
250 }
251 } else {
252 task.time_remaining()
253 .map(|d| format_duration_short(&d))
254 .unwrap_or_else(|| "?".to_string())
255 };
256
257 let a = self.style.to_ansi();
258 let r = self.style.reset_ansi();
259 format!("{a}{text}{r}")
260 }
261}
262
263impl Default for TimeRemainingColumn {
264 fn default() -> Self {
265 Self::new()
266 }
267}
268
269#[derive(Debug, Clone)]
275pub struct TaskProgressColumn {
276 pub style: Style,
277}
278
279impl TaskProgressColumn {
280 pub fn new() -> Self {
282 Self {
283 style: Style::new(),
284 }
285 }
286
287 pub fn style(mut self, s: Style) -> Self {
289 self.style = s;
290 self
291 }
292}
293
294impl ProgressColumn for TaskProgressColumn {
295 fn render(&self, task: &Task, _width: usize, _elapsed: std::time::Duration) -> String {
296 if task.total.is_some() {
297 let pct = (task.progress() * 100.0) as usize;
298 let s = format!("{pct:>3}%");
299 let a = self.style.to_ansi();
300 let r = self.style.reset_ansi();
301 format!("{a}{s}{r}")
302 } else {
303 String::new()
304 }
305 }
306}
307
308impl Default for TaskProgressColumn {
309 fn default() -> Self {
310 Self::new()
311 }
312}
313
314#[derive(Debug, Clone)]
320pub struct MofNCompleteColumn {
321 pub style: Style,
322 pub separator: String,
323}
324
325impl MofNCompleteColumn {
326 pub fn new() -> Self {
328 Self {
329 style: Style::new(),
330 separator: "/".to_string(),
331 }
332 }
333}
334
335impl ProgressColumn for MofNCompleteColumn {
336 fn render(&self, task: &Task, _width: usize, _elapsed: std::time::Duration) -> String {
337 let completed = task.completed as usize;
338 if let Some(total) = task.total {
339 let total = total as usize;
340 let s = format!("{completed}{}{total}", self.separator);
341 let a = self.style.to_ansi();
342 let r = self.style.reset_ansi();
343 format!("{a}{s}{r}")
344 } else {
345 format!("{completed}")
346 }
347 }
348}
349
350impl Default for MofNCompleteColumn {
351 fn default() -> Self {
352 Self::new()
353 }
354}
355
356fn format_duration_short(d: &std::time::Duration) -> String {
361 let secs = d.as_secs();
362 if secs < 60 {
363 format!("0:{secs:02}")
364 } else if secs < 3600 {
365 format!("{}:{:02}", secs / 60, secs % 60)
366 } else {
367 format!("{}:{:02}:{:02}", secs / 3600, (secs % 3600) / 60, secs % 60)
368 }
369}
370
371pub fn format_size(bytes: f64) -> String {
377 const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB", "PB"];
378 let mut value = bytes;
379 let mut unit_idx = 0;
380 while value >= 1000.0 && unit_idx < UNITS.len() - 1 {
381 value /= 1000.0;
382 unit_idx += 1;
383 }
384 if unit_idx == 0 {
385 format!("{:.0} {}", value, UNITS[unit_idx])
386 } else {
387 format!("{:.1} {}", value, UNITS[unit_idx])
388 }
389}
390
391pub fn format_speed(bytes_per_sec: f64) -> String {
393 format!("{}/s", format_size(bytes_per_sec))
394}
395
396#[derive(Debug, Clone)]
402pub struct FileSizeColumn {
403 pub style: Style,
404}
405
406impl FileSizeColumn {
407 pub fn new() -> Self {
409 Self {
410 style: Style::new(),
411 }
412 }
413
414 pub fn style(mut self, s: Style) -> Self {
416 self.style = s;
417 self
418 }
419}
420
421impl ProgressColumn for FileSizeColumn {
422 fn render(&self, task: &Task, _width: usize, _elapsed: std::time::Duration) -> String {
423 let size = format_size(task.completed);
424 let a = self.style.to_ansi();
425 let r = self.style.reset_ansi();
426 format!("{a}{size}{r}")
427 }
428}
429
430impl Default for FileSizeColumn {
431 fn default() -> Self {
432 Self::new()
433 }
434}
435
436#[derive(Debug, Clone)]
442pub struct TotalFileSizeColumn {
443 pub style: Style,
444}
445
446impl TotalFileSizeColumn {
447 pub fn new() -> Self {
449 Self {
450 style: Style::new(),
451 }
452 }
453
454 pub fn style(mut self, s: Style) -> Self {
456 self.style = s;
457 self
458 }
459}
460
461impl ProgressColumn for TotalFileSizeColumn {
462 fn render(&self, task: &Task, _width: usize, _elapsed: std::time::Duration) -> String {
463 let a = self.style.to_ansi();
464 let r = self.style.reset_ansi();
465 if let Some(total) = task.total {
466 let size = format_size(total);
467 format!("{a}{size}{r}")
468 } else {
469 String::new()
470 }
471 }
472}
473
474impl Default for TotalFileSizeColumn {
475 fn default() -> Self {
476 Self::new()
477 }
478}
479
480#[derive(Debug, Clone)]
486pub struct DownloadColumn {
487 pub style: Style,
488 pub separator: String,
489}
490
491impl DownloadColumn {
492 pub fn new() -> Self {
494 Self {
495 style: Style::new(),
496 separator: "/".to_string(),
497 }
498 }
499
500 pub fn style(mut self, s: Style) -> Self {
502 self.style = s;
503 self
504 }
505 pub fn separator(mut self, sep: impl Into<String>) -> Self {
507 self.separator = sep.into();
508 self
509 }
510}
511
512impl ProgressColumn for DownloadColumn {
513 fn render(&self, task: &Task, _width: usize, _elapsed: std::time::Duration) -> String {
514 let a = self.style.to_ansi();
515 let r = self.style.reset_ansi();
516 let completed = format_size(task.completed);
517 if let Some(total) = task.total {
518 let total = format_size(total);
519 format!("{a}{completed}{}{total}{r}", self.separator)
520 } else {
521 format!("{a}{completed}{r}")
522 }
523 }
524}
525
526impl Default for DownloadColumn {
527 fn default() -> Self {
528 Self::new()
529 }
530}
531
532#[derive(Debug, Clone)]
538pub struct TransferSpeedColumn {
539 pub style: Style,
540}
541
542impl TransferSpeedColumn {
543 pub fn new() -> Self {
545 Self {
546 style: Style::new(),
547 }
548 }
549
550 pub fn style(mut self, s: Style) -> Self {
552 self.style = s;
553 self
554 }
555}
556
557impl ProgressColumn for TransferSpeedColumn {
558 fn render(&self, task: &Task, _width: usize, elapsed: std::time::Duration) -> String {
559 let secs = elapsed.as_secs_f64();
560 let a = self.style.to_ansi();
561 let r = self.style.reset_ansi();
562 if secs > 0.0 && task.completed > 0.0 {
563 let speed = task.completed / secs;
564 let s = format_speed(speed);
565 format!("{a}{s}{r}")
566 } else {
567 format!("{a}0 B/s{r}")
568 }
569 }
570}
571
572impl Default for TransferSpeedColumn {
573 fn default() -> Self {
574 Self::new()
575 }
576}
577
578#[cfg(test)]
583mod tests {
584 use super::*;
585 use crate::progress::Task;
586
587 #[test]
588 fn test_text_column() {
589 let col = TextColumn::new("name");
590 let task = {
591 let mut t = Task::new(1, "test", Some(100.0));
592 t.fields.insert("name".into(), "Alice".into());
593 t
594 };
595 let result = col.render(&task, 20, std::time::Duration::from_secs(5));
596 assert!(result.contains("Alice"));
597 }
598
599 #[test]
600 fn test_spinner_column() {
601 let col = SpinnerColumn::new();
602 let task = Task::new(1, "test", Some(100.0));
603 let result = col.render(&task, 10, std::time::Duration::from_secs(1));
604 assert!(!result.is_empty());
605 }
606
607 #[test]
608 fn test_task_progress_column() {
609 let col = TaskProgressColumn::new();
610 let mut task = Task::new(1, "test", Some(100.0));
611 task.completed = 42.0;
612 let result = col.render(&task, 10, std::time::Duration::new(0, 0));
613 assert!(result.contains("42%"));
614 }
615
616 #[test]
617 fn test_format_size() {
618 assert_eq!(format_size(0.0), "0 B");
619 assert_eq!(format_size(500.0), "500 B");
620 assert_eq!(format_size(1500.0), "1.5 KB");
621 assert_eq!(format_size(2_500_000.0), "2.5 MB");
622 }
623
624 #[test]
625 fn test_format_speed() {
626 assert_eq!(format_speed(0.0), "0 B/s");
627 assert_eq!(format_speed(1500.0), "1.5 KB/s");
628 }
629
630 #[test]
631 fn test_file_size_column() {
632 let col = FileSizeColumn::new();
633 let mut task = Task::new(1, "test", Some(1000.0));
634 task.completed = 500.0;
635 let result = col.render(&task, 10, std::time::Duration::new(0, 0));
636 assert!(result.contains("500 B"));
637 }
638
639 #[test]
640 fn test_total_file_size_column() {
641 let col = TotalFileSizeColumn::new();
642 let task = Task::new(1, "test", Some(2_500_000.0));
643 let result = col.render(&task, 10, std::time::Duration::new(0, 0));
644 assert!(result.contains("2.5 MB"));
645 }
646
647 #[test]
648 fn test_download_column() {
649 let col = DownloadColumn::new();
650 let mut task = Task::new(1, "test", Some(1_500_000.0));
651 task.completed = 500_000.0;
652 let result = col.render(&task, 10, std::time::Duration::new(0, 0));
653 assert!(result.contains("500.0 KB"));
654 assert!(result.contains("1.5 MB"));
655 }
656
657 #[test]
658 fn test_transfer_speed_column() {
659 let col = TransferSpeedColumn::new();
660 let mut task = Task::new(1, "test", Some(1000.0));
661 task.completed = 500.0;
662 let result = col.render(&task, 10, std::time::Duration::from_secs(1));
663 assert!(result.contains("500 B/s"));
664 }
665}