1use crate::progress::Task;
2use crate::progress::spinner::{Spinner, SpinnerStyle};
4use crate::style::{Color, Style};
5use crate::text::Span;
6use std::fmt::Debug;
7use std::time::Duration;
8
9pub trait ProgressColumn: Send + Sync + Debug {
11 fn render(&self, task: &Task) -> Vec<Span>;
13
14 fn render_with_width(&self, task: &Task, _available_width: Option<usize>) -> Vec<Span> {
20 self.render(task)
21 }
22
23 fn is_expandable(&self) -> bool {
25 false
26 }
27
28 fn min_width(&self) -> usize {
30 0
31 }
32}
33
34#[derive(Debug)]
36pub struct TextColumn {
37 text: String,
38 style: Style,
39}
40
41impl TextColumn {
42 pub fn new(text: &str) -> Self {
43 Self {
44 text: text.to_string(),
45 style: Style::new(),
46 }
47 }
48
49 pub fn styled(text: &str, style: Style) -> Self {
50 Self {
51 text: text.to_string(),
52 style,
53 }
54 }
55}
56
57impl ProgressColumn for TextColumn {
58 fn render(&self, task: &Task) -> Vec<Span> {
59 let text = if self.text == "[progress.description]" {
61 &task.description
62 } else {
63 &self.text
64 };
65
66 vec![Span::styled(text.clone(), self.style)]
67 }
68}
69
70#[derive(Debug)]
82pub struct BarColumn {
83 pub bar_width: usize,
85 pub complete_char: char,
87 pub incomplete_char: char,
89 pub edge_char: Option<char>,
91 pub complete_style: Style,
93 pub finished_style: Option<Style>,
95 pub incomplete_style: Style,
97 pub pulse_style: Style,
99 pub expand: bool,
101 pub use_sub_blocks: bool,
103}
104
105impl Default for BarColumn {
106 fn default() -> Self {
107 Self::new(40)
108 }
109}
110
111impl BarColumn {
112 pub fn new(bar_width: usize) -> Self {
113 Self {
114 bar_width,
115 complete_char: '━', incomplete_char: '─', edge_char: Some('╸'), complete_style: Style::new().foreground(Color::Magenta),
119 finished_style: Some(Style::new().foreground(Color::Green)),
120 incomplete_style: Style::new().foreground(Color::Ansi256(237)), pulse_style: Style::new().foreground(Color::Cyan),
122 expand: false,
123 use_sub_blocks: false,
124 }
125 }
126
127 pub fn complete_char(mut self, c: char) -> Self {
129 self.complete_char = c;
130 self
131 }
132
133 pub fn incomplete_char(mut self, c: char) -> Self {
135 self.incomplete_char = c;
136 self
137 }
138
139 pub fn edge_char(mut self, c: Option<char>) -> Self {
141 self.edge_char = c;
142 self
143 }
144
145 pub fn complete_style(mut self, style: Style) -> Self {
147 self.complete_style = style;
148 self
149 }
150
151 pub fn finished_style(mut self, style: Option<Style>) -> Self {
153 self.finished_style = style;
154 self
155 }
156
157 pub fn expand(mut self, expand: bool) -> Self {
162 self.expand = expand;
163 self
164 }
165
166 pub fn use_sub_blocks(mut self, enabled: bool) -> Self {
171 self.use_sub_blocks = enabled;
172 self
173 }
174
175 fn sub_block_char(eighths: usize) -> char {
178 match eighths {
179 0 => ' ', 1 => '▏', 2 => '▎', 3 => '▍', 4 => '▌', 5 => '▋', 6 => '▊', 7 => '▉', _ => '█', }
189 }
190
191 fn render_bar(&self, task: &Task, width: usize) -> Vec<Span> {
193 if task.total.is_none() && !task.finished {
195 return self.render_pulse_with_width(task, width);
196 }
197
198 let total = task.total.unwrap_or(100) as f64;
199 let completed = task.completed as f64;
200 let percentage = (completed / total).clamp(0.0, 1.0);
201
202 let style = if task.finished {
203 self.finished_style.unwrap_or(self.complete_style)
204 } else {
205 self.complete_style
206 };
207
208 if self.use_sub_blocks && !task.finished && percentage < 1.0 {
210 return self.render_sub_blocks(percentage, width, style);
211 }
212
213 let has_edge = self.edge_char.is_some() && !task.finished && percentage < 1.0;
215 let effective_width = if has_edge {
216 width.saturating_sub(1)
217 } else {
218 width
219 };
220
221 let filled_width = (effective_width as f64 * percentage).round() as usize;
222 let empty_width = effective_width.saturating_sub(filled_width);
223
224 let mut spans = Vec::new();
225
226 if filled_width > 0 {
228 spans.push(Span::styled(
229 self.complete_char.to_string().repeat(filled_width),
230 style,
231 ));
232 }
233
234 if has_edge && filled_width < width {
236 if let Some(edge) = self.edge_char {
237 spans.push(Span::styled(edge.to_string(), style));
238 }
239 }
240
241 if empty_width > 0 {
243 spans.push(Span::styled(
244 self.incomplete_char.to_string().repeat(empty_width),
245 self.incomplete_style,
246 ));
247 }
248
249 spans
250 }
251
252 fn render_sub_blocks(&self, percentage: f64, width: usize, style: Style) -> Vec<Span> {
254 let exact_filled = width as f64 * percentage;
256 let full_blocks = exact_filled as usize;
257 let fraction = exact_filled - full_blocks as f64;
258 let eighths = (fraction * 8.0).round() as usize;
259
260 let mut spans = Vec::new();
261
262 if full_blocks > 0 {
264 spans.push(Span::styled("█".repeat(full_blocks), style));
265 }
266
267 if eighths > 0 && full_blocks < width {
269 spans.push(Span::styled(
270 Self::sub_block_char(eighths).to_string(),
271 style,
272 ));
273 }
274
275 let used_width = full_blocks + if eighths > 0 { 1 } else { 0 };
277 let empty_width = width.saturating_sub(used_width);
278 if empty_width > 0 {
279 spans.push(Span::styled(" ".repeat(empty_width), self.incomplete_style));
280 }
281
282 spans
283 }
284
285 fn render_pulse_with_width(&self, task: &Task, width: usize) -> Vec<Span> {
287 let pulse_width = 6.min(width / 3);
288
289 let elapsed_ms = task.elapsed().as_millis() as usize;
290 let cycle_duration_ms = 1500;
291 let position_in_cycle = elapsed_ms % cycle_duration_ms;
292
293 let half_cycle = cycle_duration_ms / 2;
294 let normalized_pos = if position_in_cycle < half_cycle {
295 position_in_cycle as f64 / half_cycle as f64
296 } else {
297 1.0 - ((position_in_cycle - half_cycle) as f64 / half_cycle as f64)
298 };
299
300 let pulse_start =
301 ((width.saturating_sub(pulse_width)) as f64 * normalized_pos).round() as usize;
302 let pulse_end = pulse_start + pulse_width;
303
304 let mut spans = Vec::new();
305
306 if pulse_start > 0 {
307 spans.push(Span::styled(
308 self.incomplete_char.to_string().repeat(pulse_start),
309 self.incomplete_style,
310 ));
311 }
312
313 spans.push(Span::styled(
314 self.complete_char.to_string().repeat(pulse_width),
315 self.pulse_style,
316 ));
317
318 let after_pulse = width.saturating_sub(pulse_end);
319 if after_pulse > 0 {
320 spans.push(Span::styled(
321 self.incomplete_char.to_string().repeat(after_pulse),
322 self.incomplete_style,
323 ));
324 }
325
326 spans
327 }
328}
329
330impl ProgressColumn for BarColumn {
331 fn render(&self, task: &Task) -> Vec<Span> {
332 self.render_bar(task, self.bar_width)
333 }
334
335 fn render_with_width(&self, task: &Task, available_width: Option<usize>) -> Vec<Span> {
336 let width = if self.expand {
337 available_width
338 .unwrap_or(self.bar_width)
339 .max(self.bar_width)
340 } else {
341 self.bar_width
342 };
343 self.render_bar(task, width)
344 }
345
346 fn is_expandable(&self) -> bool {
347 self.expand
348 }
349
350 fn min_width(&self) -> usize {
351 self.bar_width
352 }
353}
354
355#[derive(Debug)]
357pub struct PercentageColumn(pub Style);
358
359impl Default for PercentageColumn {
360 fn default() -> Self {
361 Self::new()
362 }
363}
364
365impl PercentageColumn {
366 pub fn new() -> Self {
367 Self(Style::new().foreground(Color::Cyan))
368 }
369}
370
371impl ProgressColumn for PercentageColumn {
372 fn render(&self, task: &Task) -> Vec<Span> {
373 let percentage = task.percentage() * 100.0;
374 vec![Span::styled(format!("{:>3.0}%", percentage), self.0)]
375 }
376}
377
378#[derive(Debug)]
380pub struct SpinnerColumn {
381 spinner: Spinner, }
383
384impl Default for SpinnerColumn {
385 fn default() -> Self {
386 Self::new()
387 }
388}
389
390impl SpinnerColumn {
391 pub fn new() -> Self {
392 Self {
393 spinner: Spinner::new("").style(SpinnerStyle::Dots),
394 }
395 }
396
397 pub fn with_style(mut self, style: SpinnerStyle) -> Self {
405 self.spinner = Spinner::new("").style(style);
406 self
407 }
408}
409
410impl ProgressColumn for SpinnerColumn {
411 fn render(&self, task: &Task) -> Vec<Span> {
412 let style = self.spinner.get_style();
423 let interval = style.interval_ms();
424 let frames = style.frames();
425 let elapsed_ms = task.elapsed().as_millis() as u64;
426 let idx = ((elapsed_ms / interval) as usize) % frames.len();
427
428 vec![Span::styled(
429 frames[idx].to_string(),
430 Style::new().foreground(Color::Green),
431 )]
432 }
433}
434
435#[derive(Debug)]
437pub struct TransferSpeedColumn;
438
439impl ProgressColumn for TransferSpeedColumn {
440 fn render(&self, task: &Task) -> Vec<Span> {
441 let speed = task.speed();
442 let speed_str = if speed >= 1_000_000.0 {
443 format!("{:.1} MB/s", speed / 1_000_000.0)
444 } else if speed >= 1_000.0 {
445 format!("{:.1} KB/s", speed / 1_000.0)
446 } else {
447 format!("{:.0} B/s", speed)
448 };
449 vec![Span::styled(speed_str, Style::new().foreground(Color::Red))]
450 }
451}
452
453#[derive(Debug)]
455pub struct TimeRemainingColumn;
456
457impl ProgressColumn for TimeRemainingColumn {
458 fn render(&self, task: &Task) -> Vec<Span> {
459 let eta = match task.eta() {
460 Some(d) => format_duration(d),
461 None => "-:--:--".to_string(),
462 };
463 vec![Span::styled(eta, Style::new().foreground(Color::Cyan))]
464 }
465}
466
467fn format_duration(d: Duration) -> String {
468 let secs = d.as_secs();
469 if secs >= 3600 {
470 format!(
471 "{:02}:{:02}:{:02}",
472 secs / 3600,
473 (secs % 3600) / 60,
474 secs % 60
475 )
476 } else {
477 format!("{:02}:{:02}", secs / 60, secs % 60)
478 }
479}
480
481#[derive(Debug)]
482pub struct MofNColumn {
483 separator: String,
484}
485
486impl Default for MofNColumn {
487 fn default() -> Self {
488 Self::new()
489 }
490}
491
492impl MofNColumn {
493 pub fn new() -> Self {
494 Self {
495 separator: "/".to_string(),
496 }
497 }
498}
499
500impl ProgressColumn for MofNColumn {
501 fn render(&self, task: &Task) -> Vec<Span> {
502 let completed = task.completed;
503 let total = task.total.unwrap_or(0);
504 vec![Span::styled(
505 format!("{}{}{}", completed, self.separator, total),
506 Style::new().foreground(Color::Green),
507 )]
508 }
509}
510
511#[derive(Debug)]
512pub struct ElapsedColumn;
513
514impl ProgressColumn for ElapsedColumn {
515 fn render(&self, task: &Task) -> Vec<Span> {
516 let elapsed = task.elapsed();
517 vec![Span::styled(
518 format_duration(elapsed),
519 Style::new().foreground(Color::Cyan),
520 )]
521 }
522}
523
524fn format_bytes(bytes: u64) -> String {
526 const KB: f64 = 1024.0;
527 const MB: f64 = 1024.0 * 1024.0;
528 const GB: f64 = 1024.0 * 1024.0 * 1024.0;
529
530 let bytes_f = bytes as f64;
531 if bytes_f >= GB {
532 format!("{:.1} GB", bytes_f / GB)
533 } else if bytes_f >= MB {
534 format!("{:.1} MB", bytes_f / MB)
535 } else if bytes_f >= KB {
536 format!("{:.1} KB", bytes_f / KB)
537 } else {
538 format!("{} B", bytes)
539 }
540}
541
542#[derive(Debug)]
546pub struct FileSizeColumn {
547 style: Style,
548}
549
550impl Default for FileSizeColumn {
551 fn default() -> Self {
552 Self::new()
553 }
554}
555
556impl FileSizeColumn {
557 pub fn new() -> Self {
558 Self {
559 style: Style::new().foreground(Color::Green),
560 }
561 }
562
563 pub fn style(mut self, style: Style) -> Self {
565 self.style = style;
566 self
567 }
568}
569
570impl ProgressColumn for FileSizeColumn {
571 fn render(&self, task: &Task) -> Vec<Span> {
572 let size_str = format_bytes(task.completed);
573 vec![Span::styled(size_str, self.style)]
574 }
575}
576
577#[derive(Debug)]
581pub struct TotalFileSizeColumn {
582 separator: String,
583 completed_style: Style,
584 total_style: Style,
585}
586
587impl Default for TotalFileSizeColumn {
588 fn default() -> Self {
589 Self::new()
590 }
591}
592
593impl TotalFileSizeColumn {
594 pub fn new() -> Self {
595 Self {
596 separator: " / ".to_string(),
597 completed_style: Style::new().foreground(Color::Green),
598 total_style: Style::new().foreground(Color::Blue),
599 }
600 }
601
602 pub fn separator(mut self, sep: &str) -> Self {
604 self.separator = sep.to_string();
605 self
606 }
607
608 pub fn completed_style(mut self, style: Style) -> Self {
610 self.completed_style = style;
611 self
612 }
613
614 pub fn total_style(mut self, style: Style) -> Self {
616 self.total_style = style;
617 self
618 }
619}
620
621impl ProgressColumn for TotalFileSizeColumn {
622 fn render(&self, task: &Task) -> Vec<Span> {
623 let completed_str = format_bytes(task.completed);
624 let total_str = match task.total {
625 Some(t) => format_bytes(t),
626 None => "?".to_string(),
627 };
628
629 vec![
630 Span::styled(completed_str, self.completed_style),
631 Span::styled(self.separator.clone(), Style::new()),
632 Span::styled(total_str, self.total_style),
633 ]
634 }
635}
636
637#[derive(Debug)]
641pub struct DownloadColumn {
642 size_style: Style,
643 speed_style: Style,
644}
645
646impl Default for DownloadColumn {
647 fn default() -> Self {
648 Self::new()
649 }
650}
651
652impl DownloadColumn {
653 pub fn new() -> Self {
654 Self {
655 size_style: Style::new().foreground(Color::Green),
656 speed_style: Style::new().foreground(Color::Red),
657 }
658 }
659
660 pub fn size_style(mut self, style: Style) -> Self {
662 self.size_style = style;
663 self
664 }
665
666 pub fn speed_style(mut self, style: Style) -> Self {
668 self.speed_style = style;
669 self
670 }
671}
672
673impl ProgressColumn for DownloadColumn {
674 fn render(&self, task: &Task) -> Vec<Span> {
675 let size_str = format_bytes(task.completed);
676 let speed = task.speed();
677 let speed_str = if speed >= 1_000_000.0 {
678 format!("{:.1} MB/s", speed / 1_000_000.0)
679 } else if speed >= 1_000.0 {
680 format!("{:.1} KB/s", speed / 1_000.0)
681 } else {
682 format!("{:.0} B/s", speed)
683 };
684
685 vec![
686 Span::styled(size_str, self.size_style),
687 Span::raw(" @ "),
688 Span::styled(speed_str, self.speed_style),
689 ]
690 }
691}