1use crate::progress::{ProgressBar, Task};
6use crate::spinner::Spinner;
7use crate::style::Style;
8
9pub trait ProgressColumn: std::fmt::Debug {
15 fn render(&self, task: &Task, width: usize, elapsed: std::time::Duration) -> String;
17}
18
19#[derive(Debug, Clone)]
26pub struct TextColumn {
27 pub key: String,
29 pub format: String,
31 pub style: Style,
33}
34
35impl TextColumn {
36 pub fn new(key: impl Into<String>) -> Self {
38 Self { key: key.into(), format: "{:>11}".to_string(), style: Style::new() }
39 }
40
41 pub fn format(mut self, fmt: impl Into<String>) -> Self { self.format = fmt.into(); self }
43 pub fn style(mut self, s: Style) -> Self { self.style = s; self }
45}
46
47impl ProgressColumn for TextColumn {
48 fn render(&self, task: &Task, _width: usize, _elapsed: std::time::Duration) -> String {
49 let value = task.fields.get(&self.key).map(|s| s.as_str()).unwrap_or("?");
50 let ansi = self.style.to_ansi();
53 let reset = self.style.reset_ansi();
54 format!("{ansi}{value}{reset}")
55 }
56}
57
58#[derive(Debug, Clone)]
64pub struct BarColumn {
65 pub bar: ProgressBar,
67 pub width: Option<usize>,
69}
70
71impl BarColumn {
72 pub fn new() -> Self {
74 Self { bar: ProgressBar::new(), width: None }
75 }
76
77 pub fn complete_style(mut self, s: Style) -> Self { self.bar = self.bar.complete_style(s); self }
79 pub fn finished_style(mut self, s: Style) -> Self { self.bar = self.bar.remaining_style(s); self }
81 pub fn width(mut self, w: usize) -> Self { self.width = Some(w); self }
83}
84
85impl ProgressColumn for BarColumn {
86 fn render(&self, task: &Task, width: usize, _elapsed: std::time::Duration) -> String {
87 let w = self.width.unwrap_or(width.saturating_sub(2));
88 let mut bar = self.bar.clone();
89 bar.total = task.total;
90 bar.completed = task.completed;
91 bar.width = Some(w);
92 bar.render(w)
93 }
94}
95
96impl Default for BarColumn {
97 fn default() -> Self { Self::new() }
98}
99
100#[derive(Debug, Clone)]
106pub struct SpinnerColumn {
107 pub spinner: Spinner,
108 pub style: Style,
109 pub finished_style: Style,
110 pub finished_text: String,
111}
112
113impl SpinnerColumn {
114 pub fn new() -> Self {
116 Self {
117 spinner: Spinner::default(),
118 style: Style::new(),
119 finished_style: Style::new().color(crate::color::Color::parse("green").unwrap()).bold(true),
120 finished_text: "✓".to_string(),
121 }
122 }
123
124 pub fn style(mut self, s: Style) -> Self { self.style = s; self }
126 pub fn finished_style(mut self, s: Style) -> Self { self.finished_style = s; self }
128}
129
130impl ProgressColumn for SpinnerColumn {
131 fn render(&self, task: &Task, _width: usize, elapsed: std::time::Duration) -> String {
132 if task.is_finished() {
133 let a = self.finished_style.to_ansi();
134 let r = self.finished_style.reset_ansi();
135 format!("{a}{}{r}", self.finished_text)
136 } else {
137 let frame = self.spinner.frame_at(elapsed);
138 let a = self.style.to_ansi();
139 let r = self.style.reset_ansi();
140 format!("{a}{frame}{r}")
141 }
142 }
143}
144
145impl Default for SpinnerColumn {
146 fn default() -> Self { Self::new() }
147}
148
149#[derive(Debug, Clone)]
155pub struct TimeElapsedColumn {
156 pub style: Style,
157 pub paused_style: Style,
158}
159
160impl TimeElapsedColumn {
161 pub fn new() -> Self {
163 Self { style: Style::new(), paused_style: Style::new().dim(true) }
164 }
165}
166
167impl ProgressColumn for TimeElapsedColumn {
168 fn render(&self, task: &Task, _width: usize, _elapsed: std::time::Duration) -> String {
169 let d = task.elapsed();
170 let s = format_duration_short(&d);
171 let a = self.style.to_ansi();
172 let r = self.style.reset_ansi();
173 format!("{a}{s}{r}")
174 }
175}
176
177impl Default for TimeElapsedColumn {
178 fn default() -> Self { Self::new() }
179}
180
181#[derive(Debug, Clone)]
187pub struct TimeRemainingColumn {
188 pub style: Style,
189 pub elapsed_when_finished: bool,
190}
191
192impl TimeRemainingColumn {
193 pub fn new() -> Self {
194 Self { style: Style::new(), elapsed_when_finished: false }
195 }
196}
197
198impl ProgressColumn for TimeRemainingColumn {
199 fn render(&self, task: &Task, _width: usize, _elapsed: std::time::Duration) -> String {
200 let text = if task.is_finished() {
201 if self.elapsed_when_finished {
202 format_duration_short(&task.elapsed())
203 } else {
204 String::new()
205 }
206 } else {
207 task.time_remaining()
208 .map(|d| format_duration_short(&d))
209 .unwrap_or_else(|| "?".to_string())
210 };
211
212 let a = self.style.to_ansi();
213 let r = self.style.reset_ansi();
214 format!("{a}{text}{r}")
215 }
216}
217
218impl Default for TimeRemainingColumn {
219 fn default() -> Self { Self::new() }
220}
221
222#[derive(Debug, Clone)]
228pub struct TaskProgressColumn {
229 pub style: Style,
230}
231
232impl TaskProgressColumn {
233 pub fn new() -> Self { Self { style: Style::new() } }
235
236 pub fn style(mut self, s: Style) -> Self { self.style = s; self }
238}
239
240impl ProgressColumn for TaskProgressColumn {
241 fn render(&self, task: &Task, _width: usize, _elapsed: std::time::Duration) -> String {
242 if task.total.is_some() {
243 let pct = (task.progress() * 100.0) as usize;
244 let s = format!("{pct:>3}%");
245 let a = self.style.to_ansi();
246 let r = self.style.reset_ansi();
247 format!("{a}{s}{r}")
248 } else {
249 String::new()
250 }
251 }
252}
253
254impl Default for TaskProgressColumn {
255 fn default() -> Self { Self::new() }
256}
257
258#[derive(Debug, Clone)]
264pub struct MofNCompleteColumn {
265 pub style: Style,
266 pub separator: String,
267}
268
269impl MofNCompleteColumn {
270 pub fn new() -> Self {
272 Self { style: Style::new(), separator: "/".to_string() }
273 }
274}
275
276impl ProgressColumn for MofNCompleteColumn {
277 fn render(&self, task: &Task, _width: usize, _elapsed: std::time::Duration) -> String {
278 let completed = task.completed as usize;
279 if let Some(total) = task.total {
280 let total = total as usize;
281 let s = format!("{completed}{}{total}", self.separator);
282 let a = self.style.to_ansi();
283 let r = self.style.reset_ansi();
284 format!("{a}{s}{r}")
285 } else {
286 format!("{completed}")
287 }
288 }
289}
290
291impl Default for MofNCompleteColumn {
292 fn default() -> Self { Self::new() }
293}
294
295fn format_duration_short(d: &std::time::Duration) -> String {
300 let secs = d.as_secs();
301 if secs < 60 {
302 format!("0:{secs:02}")
303 } else if secs < 3600 {
304 format!("{}:{:02}", secs / 60, secs % 60)
305 } else {
306 format!("{}:{:02}:{:02}", secs / 3600, (secs % 3600) / 60, secs % 60)
307 }
308}
309
310pub fn format_size(bytes: f64) -> String {
316 const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB", "PB"];
317 let mut value = bytes;
318 let mut unit_idx = 0;
319 while value >= 1000.0 && unit_idx < UNITS.len() - 1 {
320 value /= 1000.0;
321 unit_idx += 1;
322 }
323 if unit_idx == 0 {
324 format!("{:.0} {}", value, UNITS[unit_idx])
325 } else {
326 format!("{:.1} {}", value, UNITS[unit_idx])
327 }
328}
329
330pub fn format_speed(bytes_per_sec: f64) -> String {
332 format!("{}/s", format_size(bytes_per_sec))
333}
334
335#[derive(Debug, Clone)]
341pub struct FileSizeColumn {
342 pub style: Style,
343}
344
345impl FileSizeColumn {
346 pub fn new() -> Self {
348 Self { style: Style::new() }
349 }
350
351 pub fn style(mut self, s: Style) -> Self { self.style = s; self }
353}
354
355impl ProgressColumn for FileSizeColumn {
356 fn render(&self, task: &Task, _width: usize, _elapsed: std::time::Duration) -> String {
357 let size = format_size(task.completed);
358 let a = self.style.to_ansi();
359 let r = self.style.reset_ansi();
360 format!("{a}{size}{r}")
361 }
362}
363
364impl Default for FileSizeColumn {
365 fn default() -> Self { Self::new() }
366}
367
368#[derive(Debug, Clone)]
374pub struct TotalFileSizeColumn {
375 pub style: Style,
376}
377
378impl TotalFileSizeColumn {
379 pub fn new() -> Self {
381 Self { style: Style::new() }
382 }
383
384 pub fn style(mut self, s: Style) -> Self { self.style = s; self }
386}
387
388impl ProgressColumn for TotalFileSizeColumn {
389 fn render(&self, task: &Task, _width: usize, _elapsed: std::time::Duration) -> String {
390 let a = self.style.to_ansi();
391 let r = self.style.reset_ansi();
392 if let Some(total) = task.total {
393 let size = format_size(total);
394 format!("{a}{size}{r}")
395 } else {
396 String::new()
397 }
398 }
399}
400
401impl Default for TotalFileSizeColumn {
402 fn default() -> Self { Self::new() }
403}
404
405#[derive(Debug, Clone)]
411pub struct DownloadColumn {
412 pub style: Style,
413 pub separator: String,
414}
415
416impl DownloadColumn {
417 pub fn new() -> Self {
419 Self { style: Style::new(), separator: "/".to_string() }
420 }
421
422 pub fn style(mut self, s: Style) -> Self { self.style = s; self }
424 pub fn separator(mut self, sep: impl Into<String>) -> Self { self.separator = sep.into(); self }
426}
427
428impl ProgressColumn for DownloadColumn {
429 fn render(&self, task: &Task, _width: usize, _elapsed: std::time::Duration) -> String {
430 let a = self.style.to_ansi();
431 let r = self.style.reset_ansi();
432 let completed = format_size(task.completed);
433 if let Some(total) = task.total {
434 let total = format_size(total);
435 format!("{a}{completed}{}{total}{r}", self.separator)
436 } else {
437 format!("{a}{completed}{r}")
438 }
439 }
440}
441
442impl Default for DownloadColumn {
443 fn default() -> Self { Self::new() }
444}
445
446#[derive(Debug, Clone)]
452pub struct TransferSpeedColumn {
453 pub style: Style,
454}
455
456impl TransferSpeedColumn {
457 pub fn new() -> Self {
459 Self { style: Style::new() }
460 }
461
462 pub fn style(mut self, s: Style) -> Self { self.style = s; self }
464}
465
466impl ProgressColumn for TransferSpeedColumn {
467 fn render(&self, task: &Task, _width: usize, elapsed: std::time::Duration) -> String {
468 let secs = elapsed.as_secs_f64();
469 let a = self.style.to_ansi();
470 let r = self.style.reset_ansi();
471 if secs > 0.0 && task.completed > 0.0 {
472 let speed = task.completed / secs;
473 let s = format_speed(speed);
474 format!("{a}{s}{r}")
475 } else {
476 format!("{a}0 B/s{r}")
477 }
478 }
479}
480
481impl Default for TransferSpeedColumn {
482 fn default() -> Self { Self::new() }
483}
484
485#[cfg(test)]
490mod tests {
491 use super::*;
492 use crate::progress::Task;
493
494 #[test]
495 fn test_text_column() {
496 let col = TextColumn::new("name");
497 let task = {
498 let mut t = Task::new(1, "test", Some(100.0));
499 t.fields.insert("name".into(), "Alice".into());
500 t
501 };
502 let result = col.render(&task, 20, std::time::Duration::from_secs(5));
503 assert!(result.contains("Alice"));
504 }
505
506 #[test]
507 fn test_spinner_column() {
508 let col = SpinnerColumn::new();
509 let task = Task::new(1, "test", Some(100.0));
510 let result = col.render(&task, 10, std::time::Duration::from_secs(1));
511 assert!(!result.is_empty());
512 }
513
514 #[test]
515 fn test_task_progress_column() {
516 let col = TaskProgressColumn::new();
517 let mut task = Task::new(1, "test", Some(100.0));
518 task.completed = 42.0;
519 let result = col.render(&task, 10, std::time::Duration::new(0, 0));
520 assert!(result.contains("42%"));
521 }
522
523 #[test]
524 fn test_format_size() {
525 assert_eq!(format_size(0.0), "0 B");
526 assert_eq!(format_size(500.0), "500 B");
527 assert_eq!(format_size(1500.0), "1.5 KB");
528 assert_eq!(format_size(2_500_000.0), "2.5 MB");
529 }
530
531 #[test]
532 fn test_format_speed() {
533 assert_eq!(format_speed(0.0), "0 B/s");
534 assert_eq!(format_speed(1500.0), "1.5 KB/s");
535 }
536
537 #[test]
538 fn test_file_size_column() {
539 let col = FileSizeColumn::new();
540 let mut task = Task::new(1, "test", Some(1000.0));
541 task.completed = 500.0;
542 let result = col.render(&task, 10, std::time::Duration::new(0, 0));
543 assert!(result.contains("500 B"));
544 }
545
546 #[test]
547 fn test_total_file_size_column() {
548 let col = TotalFileSizeColumn::new();
549 let task = Task::new(1, "test", Some(2_500_000.0));
550 let result = col.render(&task, 10, std::time::Duration::new(0, 0));
551 assert!(result.contains("2.5 MB"));
552 }
553
554 #[test]
555 fn test_download_column() {
556 let col = DownloadColumn::new();
557 let mut task = Task::new(1, "test", Some(1_500_000.0));
558 task.completed = 500_000.0;
559 let result = col.render(&task, 10, std::time::Duration::new(0, 0));
560 assert!(result.contains("500.0 KB"));
561 assert!(result.contains("1.5 MB"));
562 }
563
564 #[test]
565 fn test_transfer_speed_column() {
566 let col = TransferSpeedColumn::new();
567 let mut task = Task::new(1, "test", Some(1000.0));
568 task.completed = 500.0;
569 let result = col.render(&task, 10, std::time::Duration::from_secs(1));
570 assert!(result.contains("500 B/s"));
571 }
572}