1use crate::cells;
7use crate::console::{Console, ConsoleOptions};
8use crate::filesize::{self, SizeUnit, binary, binary_speed, decimal, decimal_speed};
9use crate::renderables::Renderable;
10use crate::segment::Segment;
11use crate::style::Style;
12use crate::text::Text;
13use std::time::{Duration, Instant};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
17pub enum BarStyle {
18 Ascii,
20 #[default]
22 Block,
23 Line,
25 Dots,
27 Gradient,
29}
30
31impl BarStyle {
32 #[must_use]
34 pub const fn completed_char(&self) -> &'static str {
35 match self {
36 Self::Ascii => "#",
37 Self::Block => "\u{2588}", Self::Line => "\u{2501}", Self::Dots => "\u{25CF}", Self::Gradient => "\u{2588}", }
42 }
43
44 #[must_use]
46 pub const fn remaining_char(&self) -> &'static str {
47 match self {
48 Self::Ascii => "-",
49 Self::Block => "\u{2591}", Self::Line => "\u{2501}", Self::Dots => "\u{25CB}", Self::Gradient => "\u{2591}", }
54 }
55
56 #[must_use]
58 pub const fn pulse_char(&self) -> &'static str {
59 match self {
60 Self::Ascii => ">",
61 Self::Block => "\u{2593}", Self::Line => "\u{257A}", Self::Dots => "\u{25CF}", Self::Gradient => "\u{2593}", }
66 }
67}
68
69#[derive(Debug, Clone)]
71pub struct Spinner {
72 frames: Vec<&'static str>,
74 frame_index: usize,
76 style: Style,
78}
79
80impl Default for Spinner {
81 fn default() -> Self {
82 Self::dots()
83 }
84}
85
86impl Spinner {
87 #[must_use]
89 pub fn dots() -> Self {
90 Self {
91 frames: vec!["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
92 frame_index: 0,
93 style: Style::new(),
94 }
95 }
96
97 #[must_use]
99 pub fn line() -> Self {
100 Self {
101 frames: vec!["⎺", "⎻", "⎼", "⎽", "⎼", "⎻"],
102 frame_index: 0,
103 style: Style::new(),
104 }
105 }
106
107 #[must_use]
109 pub fn simple() -> Self {
110 Self {
111 frames: vec!["|", "/", "-", "\\"],
112 frame_index: 0,
113 style: Style::new(),
114 }
115 }
116
117 #[must_use]
119 pub fn bounce() -> Self {
120 Self {
121 frames: vec!["⠁", "⠂", "⠄", "⠂"],
122 frame_index: 0,
123 style: Style::new(),
124 }
125 }
126
127 #[must_use]
129 pub fn growing() -> Self {
130 Self {
131 frames: vec!["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"],
132 frame_index: 0,
133 style: Style::new(),
134 }
135 }
136
137 #[must_use]
139 pub fn moon() -> Self {
140 Self {
141 frames: vec!["🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"],
142 frame_index: 0,
143 style: Style::new(),
144 }
145 }
146
147 #[must_use]
149 pub fn clock() -> Self {
150 Self {
151 frames: vec![
152 "🕐", "🕑", "🕒", "🕓", "🕔", "🕕", "🕖", "🕗", "🕘", "🕙", "🕚", "🕛",
153 ],
154 frame_index: 0,
155 style: Style::new(),
156 }
157 }
158
159 #[must_use]
161 pub fn custom(frames: Vec<&'static str>) -> Self {
162 Self {
163 frames,
164 frame_index: 0,
165 style: Style::new(),
166 }
167 }
168
169 #[must_use]
171 pub fn style(mut self, style: Style) -> Self {
172 self.style = style;
173 self
174 }
175
176 pub fn next_frame(&mut self) -> &'static str {
178 if self.frames.is_empty() {
179 return " ";
180 }
181 let frame = self.frames[self.frame_index];
182 self.frame_index = (self.frame_index + 1) % self.frames.len();
183 frame
184 }
185
186 #[must_use]
188 pub fn current_frame(&self) -> &'static str {
189 if self.frames.is_empty() {
190 return " ";
191 }
192 self.frames[self.frame_index]
193 }
194
195 #[must_use]
197 pub fn render(&self) -> Segment<'static> {
198 Segment::new(self.current_frame(), Some(self.style.clone()))
199 }
200}
201
202#[derive(Debug, Clone)]
204pub struct ProgressBar {
205 completed: f64,
207 total: Option<u64>,
209 current: u64,
211 width: usize,
213 bar_style: BarStyle,
215 completed_style: Style,
217 remaining_style: Style,
219 pulse_style: Style,
221 show_percentage: bool,
223 show_eta: bool,
225 show_elapsed: bool,
227 show_speed: bool,
229 description: Option<Text>,
231 start_time: Option<Instant>,
233 show_brackets: bool,
235 finished_message: Option<String>,
237 is_finished: bool,
239 total_bytes: Option<u64>,
241 transferred_bytes: u64,
243 show_file_size: bool,
245 show_transfer_speed: bool,
247 use_binary_units: bool,
249}
250
251impl Default for ProgressBar {
252 fn default() -> Self {
253 Self {
254 completed: 0.0,
255 total: None,
256 current: 0,
257 width: 40,
258 bar_style: BarStyle::default(),
259 completed_style: Style::new().color_str("green").unwrap_or_default(),
260 remaining_style: Style::new().color_str("bright_black").unwrap_or_default(),
261 pulse_style: Style::new().color_str("cyan").unwrap_or_default(),
262 show_percentage: true,
263 show_eta: false,
264 show_elapsed: false,
265 show_speed: false,
266 description: None,
267 start_time: None,
268 show_brackets: true,
269 finished_message: None,
270 is_finished: false,
271 total_bytes: None,
272 transferred_bytes: 0,
273 show_file_size: false,
274 show_transfer_speed: false,
275 use_binary_units: false,
276 }
277 }
278}
279
280impl ProgressBar {
281 #[must_use]
283 pub fn new() -> Self {
284 Self::default()
285 }
286
287 #[must_use]
289 pub fn with_total(total: u64) -> Self {
290 Self {
291 total: Some(total),
292 show_eta: true,
293 start_time: Some(Instant::now()),
294 ..Self::default()
295 }
296 }
297
298 #[must_use]
300 pub fn width(mut self, width: usize) -> Self {
301 self.width = width;
302 self
303 }
304
305 #[must_use]
307 pub fn bar_style(mut self, style: BarStyle) -> Self {
308 self.bar_style = style;
309 self
310 }
311
312 #[must_use]
314 pub fn completed_style(mut self, style: Style) -> Self {
315 self.completed_style = style;
316 self
317 }
318
319 #[must_use]
321 pub fn remaining_style(mut self, style: Style) -> Self {
322 self.remaining_style = style;
323 self
324 }
325
326 #[must_use]
328 pub fn pulse_style(mut self, style: Style) -> Self {
329 self.pulse_style = style;
330 self
331 }
332
333 #[must_use]
335 pub fn show_percentage(mut self, show: bool) -> Self {
336 self.show_percentage = show;
337 self
338 }
339
340 #[must_use]
342 pub fn show_eta(mut self, show: bool) -> Self {
343 self.show_eta = show;
344 if show && self.start_time.is_none() {
345 self.start_time = Some(Instant::now());
346 }
347 self
348 }
349
350 #[must_use]
352 pub fn show_elapsed(mut self, show: bool) -> Self {
353 self.show_elapsed = show;
354 if show && self.start_time.is_none() {
355 self.start_time = Some(Instant::now());
356 }
357 self
358 }
359
360 #[must_use]
362 pub fn show_speed(mut self, show: bool) -> Self {
363 self.show_speed = show;
364 if show && self.start_time.is_none() {
365 self.start_time = Some(Instant::now());
366 }
367 self
368 }
369
370 #[must_use]
376 pub fn description(mut self, desc: impl Into<Text>) -> Self {
377 self.description = Some(desc.into());
378 self
379 }
380
381 #[must_use]
383 pub fn show_brackets(mut self, show: bool) -> Self {
384 self.show_brackets = show;
385 self
386 }
387
388 #[must_use]
392 pub fn finished_message(mut self, msg: impl Into<String>) -> Self {
393 self.finished_message = Some(msg.into());
394 self
395 }
396
397 pub fn set_progress(&mut self, progress: f64) {
399 self.completed = progress.clamp(0.0, 1.0);
400 if self.completed >= 1.0 {
401 self.is_finished = true;
402 }
403 }
404
405 pub fn update(&mut self, current: u64) {
407 self.current = current;
408 if let Some(total) = self.total
409 && total > 0
410 {
411 #[allow(clippy::cast_precision_loss)]
412 {
413 self.completed = (current as f64) / (total as f64);
414 }
415 self.completed = self.completed.clamp(0.0, 1.0);
416 }
417 if self.completed >= 1.0 {
418 self.is_finished = true;
419 }
420 }
421
422 pub fn advance(&mut self, delta: u64) {
424 self.update(self.current + delta);
425 }
426
427 pub fn finish(&mut self) {
429 self.completed = 1.0;
430 self.is_finished = true;
431 }
432
433 #[must_use]
435 pub fn progress(&self) -> f64 {
436 self.completed
437 }
438
439 #[must_use]
441 pub fn is_finished(&self) -> bool {
442 self.is_finished
443 }
444
445 #[must_use]
447 pub fn elapsed(&self) -> Option<Duration> {
448 self.start_time.map(|start| start.elapsed())
449 }
450
451 #[must_use]
453 pub fn eta(&self) -> Option<Duration> {
454 if self.completed <= 0.0 || self.completed >= 1.0 {
455 return None;
456 }
457
458 let elapsed = self.elapsed()?;
459 let elapsed_secs = elapsed.as_secs_f64();
460 if elapsed_secs < 0.1 {
461 return None; }
463
464 let remaining_ratio = (1.0 - self.completed) / self.completed;
465 let eta_secs = elapsed_secs * remaining_ratio;
466
467 Some(Duration::from_secs_f64(eta_secs))
468 }
469
470 #[must_use]
472 pub fn speed(&self) -> Option<f64> {
473 let elapsed = self.elapsed()?;
474 let elapsed_secs = elapsed.as_secs_f64();
475 if elapsed_secs < 0.1 {
476 return None;
477 }
478
479 #[allow(clippy::cast_precision_loss)]
480 Some((self.current as f64) / elapsed_secs)
481 }
482
483 #[must_use]
500 pub fn for_download(total_bytes: u64) -> Self {
501 Self {
502 total_bytes: Some(total_bytes),
503 total: Some(total_bytes),
504 show_file_size: true,
505 show_transfer_speed: true,
506 show_percentage: true,
507 show_eta: true,
508 start_time: Some(Instant::now()),
509 ..Self::default()
510 }
511 }
512
513 #[must_use]
515 pub fn total_bytes(mut self, bytes: u64) -> Self {
516 self.total_bytes = Some(bytes);
517 self.total = Some(bytes);
518 self
519 }
520
521 #[must_use]
523 pub fn show_file_size(mut self, show: bool) -> Self {
524 self.show_file_size = show;
525 self
526 }
527
528 #[must_use]
530 pub fn show_transfer_speed(mut self, show: bool) -> Self {
531 self.show_transfer_speed = show;
532 if show && self.start_time.is_none() {
533 self.start_time = Some(Instant::now());
534 }
535 self
536 }
537
538 #[must_use]
542 pub fn use_binary_units(mut self, use_binary: bool) -> Self {
543 self.use_binary_units = use_binary;
544 self
545 }
546
547 pub fn update_bytes(&mut self, bytes: u64) {
549 self.transferred_bytes = bytes;
550 self.current = bytes;
551 if let Some(total) = self.total_bytes
552 && total > 0
553 {
554 #[allow(clippy::cast_precision_loss)]
555 {
556 self.completed = (bytes as f64) / (total as f64);
557 }
558 self.completed = self.completed.clamp(0.0, 1.0);
559 }
560 if self.completed >= 1.0 {
561 self.is_finished = true;
562 }
563 }
564
565 pub fn advance_bytes(&mut self, delta: u64) {
567 self.update_bytes(self.transferred_bytes + delta);
568 }
569
570 #[must_use]
572 pub fn transferred_bytes(&self) -> u64 {
573 self.transferred_bytes
574 }
575
576 #[must_use]
578 pub fn total_bytes_value(&self) -> Option<u64> {
579 self.total_bytes
580 }
581
582 #[must_use]
584 pub fn transfer_speed(&self) -> Option<f64> {
585 let elapsed = self.elapsed()?;
586 let elapsed_secs = elapsed.as_secs_f64();
587 if elapsed_secs < 0.1 {
588 return None;
589 }
590
591 #[allow(clippy::cast_precision_loss)]
592 Some((self.transferred_bytes as f64) / elapsed_secs)
593 }
594
595 #[must_use]
597 pub fn format_file_size(&self) -> String {
598 if self.use_binary_units {
599 binary(self.transferred_bytes)
600 } else {
601 decimal(self.transferred_bytes)
602 }
603 }
604
605 #[must_use]
607 pub fn format_total_size(&self) -> Option<String> {
608 self.total_bytes.map(|total| {
609 if self.use_binary_units {
610 binary(total)
611 } else {
612 decimal(total)
613 }
614 })
615 }
616
617 #[must_use]
619 pub fn format_transfer_speed(&self) -> Option<String> {
620 self.transfer_speed().map(|speed| {
621 if self.use_binary_units {
622 binary_speed(speed)
623 } else {
624 decimal_speed(speed)
625 }
626 })
627 }
628
629 #[must_use]
631 fn format_duration(duration: Duration) -> String {
632 let total_secs = duration.as_secs();
633 if total_secs < 60 {
634 format!("{total_secs}s")
635 } else if total_secs < 3600 {
636 let mins = total_secs / 60;
637 let secs = total_secs % 60;
638 format!("{mins}:{secs:02}")
639 } else {
640 let hours = total_secs / 3600;
641 let mins = (total_secs % 3600) / 60;
642 let secs = total_secs % 60;
643 format!("{hours}:{mins:02}:{secs:02}")
644 }
645 }
646
647 #[must_use]
649 pub fn render(&self, available_width: usize) -> Vec<Segment<'static>> {
650 let mut segments = Vec::new();
651
652 if self.is_finished
654 && let Some(ref msg) = self.finished_message
655 {
656 let style = Style::new().color_str("green").unwrap_or_default();
657 segments.push(Segment::new(format!("✓ {msg}"), Some(style)));
658 segments.push(Segment::line());
659 return segments;
660 }
661
662 let mut used_width = 0;
664 if let Some(ref desc) = self.description {
665 let mut desc_text = desc.clone();
666 desc_text.append(" ");
667 let desc_width = desc_text.cell_len();
668 segments.extend(
669 desc_text
670 .render("")
671 .into_iter()
672 .map(super::super::segment::Segment::into_owned),
673 );
674 used_width += desc_width;
675 }
676
677 let mut suffix_parts: Vec<String> = Vec::new();
679
680 if self.show_percentage {
681 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
682 let pct = (self.completed * 100.0) as u32;
683 suffix_parts.push(format!("{pct:3}%"));
684 }
685
686 if self.show_elapsed
687 && let Some(elapsed) = self.elapsed()
688 {
689 suffix_parts.push(Self::format_duration(elapsed));
690 }
691
692 if self.show_eta
693 && !self.is_finished
694 && let Some(eta) = self.eta()
695 {
696 suffix_parts.push(format!("ETA {}", Self::format_duration(eta)));
697 }
698
699 if self.show_speed
700 && let Some(speed) = self.speed()
701 {
702 if speed >= 1.0 {
703 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
704 let speed_int = speed as u64;
705 suffix_parts.push(format!("{speed_int}/s"));
706 } else {
707 suffix_parts.push(format!("{speed:.2}/s"));
708 }
709 }
710
711 if self.show_file_size {
713 let current_size = self.format_file_size();
714 if let Some(total_size) = self.format_total_size() {
715 suffix_parts.push(format!("{current_size}/{total_size}"));
716 } else {
717 suffix_parts.push(current_size);
718 }
719 }
720
721 if self.show_transfer_speed
723 && let Some(speed_str) = self.format_transfer_speed()
724 {
725 suffix_parts.push(speed_str);
726 }
727
728 let suffix = if suffix_parts.is_empty() {
729 String::new()
730 } else {
731 format!(" {}", suffix_parts.join(" "))
732 };
733 let suffix_width = cells::cell_len(&suffix);
734
735 let bracket_width = if self.show_brackets { 2 } else { 0 };
736 let bar_width = available_width
737 .saturating_sub(used_width)
738 .saturating_sub(suffix_width)
739 .saturating_sub(bracket_width)
740 .min(self.width);
741
742 if bar_width < 3 {
743 if self.show_percentage {
745 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
746 let pct = (self.completed * 100.0) as u32;
747 segments.push(Segment::new(format!("{pct}%"), None));
748 }
749 segments.push(Segment::line());
750 return segments;
751 }
752
753 if self.show_brackets {
755 segments.push(Segment::new("[", None));
756 }
757
758 #[allow(
759 clippy::cast_possible_truncation,
760 clippy::cast_sign_loss,
761 clippy::cast_precision_loss
762 )]
763 let completed_width = ((self.completed * bar_width as f64).floor() as usize).min(bar_width);
764 let remaining_width = bar_width.saturating_sub(completed_width);
765
766 if completed_width > 0 {
768 let completed_chars = self.bar_style.completed_char().repeat(completed_width);
769 segments.push(Segment::new(
770 completed_chars,
771 Some(self.completed_style.clone()),
772 ));
773 }
774
775 let show_pulse = remaining_width > 0 && self.completed > 0.0 && self.completed < 1.0;
779
780 if show_pulse {
781 let remaining_after_pulse = remaining_width.saturating_sub(1);
783 segments.push(Segment::new(
784 self.bar_style.pulse_char(),
785 Some(self.pulse_style.clone()),
786 ));
787
788 if remaining_after_pulse > 0 {
789 let remaining_chars = self
790 .bar_style
791 .remaining_char()
792 .repeat(remaining_after_pulse);
793 segments.push(Segment::new(
794 remaining_chars,
795 Some(self.remaining_style.clone()),
796 ));
797 }
798 } else if remaining_width > 0 {
799 let remaining_chars = self.bar_style.remaining_char().repeat(remaining_width);
800 segments.push(Segment::new(
801 remaining_chars,
802 Some(self.remaining_style.clone()),
803 ));
804 }
805
806 if self.show_brackets {
807 segments.push(Segment::new("]", None));
808 }
809
810 if !suffix.is_empty() {
812 segments.push(Segment::new(suffix, None));
813 }
814
815 segments.push(Segment::line());
816 segments
817 }
818
819 #[must_use]
821 pub fn render_plain(&self, width: usize) -> String {
822 self.render(width)
823 .into_iter()
824 .map(|seg| seg.text.into_owned())
825 .collect()
826 }
827}
828
829impl Renderable for ProgressBar {
830 fn render<'a>(&'a self, _console: &Console, options: &ConsoleOptions) -> Vec<Segment<'a>> {
831 self.render(options.max_width).into_iter().collect()
832 }
833}
834
835#[must_use]
837pub fn ascii_bar() -> ProgressBar {
838 ProgressBar::new().bar_style(BarStyle::Ascii)
839}
840
841#[must_use]
843pub fn line_bar() -> ProgressBar {
844 ProgressBar::new().bar_style(BarStyle::Line)
845}
846
847#[must_use]
849pub fn dots_bar() -> ProgressBar {
850 ProgressBar::new().bar_style(BarStyle::Dots)
851}
852
853#[must_use]
855pub fn gradient_bar() -> ProgressBar {
856 ProgressBar::new().bar_style(BarStyle::Gradient)
857}
858
859#[derive(Debug, Clone)]
865pub struct FileSizeColumn {
866 size: u64,
867 unit: SizeUnit,
868 precision: usize,
869 style: Style,
870}
871
872impl FileSizeColumn {
873 #[must_use]
874 pub fn new(size: u64) -> Self {
875 Self {
876 size,
877 unit: SizeUnit::Decimal,
878 precision: 1,
879 style: Style::new().color_str("green").unwrap_or_default(),
880 }
881 }
882
883 #[must_use]
884 pub fn unit(mut self, unit: SizeUnit) -> Self {
885 self.unit = unit;
886 self
887 }
888
889 #[must_use]
890 pub fn precision(mut self, precision: usize) -> Self {
891 self.precision = precision;
892 self
893 }
894
895 #[must_use]
896 pub fn style(mut self, style: Style) -> Self {
897 self.style = style;
898 self
899 }
900
901 pub fn set_size(&mut self, size: u64) {
902 self.size = size;
903 }
904
905 #[must_use]
906 pub fn size(&self) -> u64 {
907 self.size
908 }
909
910 #[must_use]
911 pub fn render_plain(&self) -> String {
912 #[allow(clippy::cast_possible_wrap)]
913 filesize::format_size(self.size as i64, self.unit, self.precision)
914 }
915
916 #[must_use]
917 pub fn render(&self) -> Vec<Segment<'static>> {
918 vec![Segment::new(self.render_plain(), Some(self.style.clone()))]
919 }
920}
921
922impl Default for FileSizeColumn {
923 fn default() -> Self {
924 Self::new(0)
925 }
926}
927
928#[derive(Debug, Clone)]
930pub struct TotalFileSizeColumn {
931 inner: FileSizeColumn,
932}
933
934impl TotalFileSizeColumn {
935 #[must_use]
936 pub fn new(size: u64) -> Self {
937 Self {
938 inner: FileSizeColumn::new(size),
939 }
940 }
941
942 #[must_use]
943 pub fn unit(mut self, unit: SizeUnit) -> Self {
944 self.inner = self.inner.unit(unit);
945 self
946 }
947
948 #[must_use]
949 pub fn precision(mut self, precision: usize) -> Self {
950 self.inner = self.inner.precision(precision);
951 self
952 }
953
954 #[must_use]
955 pub fn style(mut self, style: Style) -> Self {
956 self.inner = self.inner.style(style);
957 self
958 }
959
960 #[must_use]
961 pub fn render_plain(&self) -> String {
962 self.inner.render_plain()
963 }
964
965 #[must_use]
966 pub fn render(&self) -> Vec<Segment<'static>> {
967 self.inner.render()
968 }
969}
970
971impl Default for TotalFileSizeColumn {
972 fn default() -> Self {
973 Self::new(0)
974 }
975}
976
977#[derive(Debug, Clone)]
979pub struct DownloadColumn {
980 current: u64,
981 total: u64,
982 unit: SizeUnit,
983 precision: usize,
984 current_style: Style,
985 separator_style: Style,
986 total_style: Style,
987}
988
989impl DownloadColumn {
990 #[must_use]
991 pub fn new(current: u64, total: u64) -> Self {
992 let green_style = Style::new().color_str("green").unwrap_or_default();
993 Self {
994 current,
995 total,
996 unit: SizeUnit::Decimal,
997 precision: 1,
998 current_style: green_style.clone(),
999 separator_style: Style::new(),
1000 total_style: green_style,
1001 }
1002 }
1003
1004 #[must_use]
1005 pub fn unit(mut self, unit: SizeUnit) -> Self {
1006 self.unit = unit;
1007 self
1008 }
1009
1010 #[must_use]
1011 pub fn precision(mut self, precision: usize) -> Self {
1012 self.precision = precision;
1013 self
1014 }
1015
1016 #[must_use]
1017 pub fn current_style(mut self, style: Style) -> Self {
1018 self.current_style = style;
1019 self
1020 }
1021
1022 #[must_use]
1023 pub fn total_style(mut self, style: Style) -> Self {
1024 self.total_style = style;
1025 self
1026 }
1027
1028 pub fn set_current(&mut self, current: u64) {
1029 self.current = current;
1030 }
1031
1032 pub fn set_total(&mut self, total: u64) {
1033 self.total = total;
1034 }
1035
1036 #[must_use]
1037 pub fn current(&self) -> u64 {
1038 self.current
1039 }
1040
1041 #[must_use]
1042 pub fn total(&self) -> u64 {
1043 self.total
1044 }
1045
1046 #[must_use]
1047 pub fn render_plain(&self) -> String {
1048 #[allow(clippy::cast_possible_wrap)]
1049 let current_str = filesize::format_size(self.current as i64, self.unit, self.precision);
1050 #[allow(clippy::cast_possible_wrap)]
1051 let total_str = filesize::format_size(self.total as i64, self.unit, self.precision);
1052 let parts: Vec<&str> = total_str.rsplitn(2, ' ').collect();
1053 if parts.len() == 2 {
1054 let unit_str = parts[0];
1055 let total_value = parts[1];
1056 let current_parts: Vec<&str> = current_str.rsplitn(2, ' ').collect();
1057 let current_value = if current_parts.len() == 2 {
1058 current_parts[1]
1059 } else {
1060 ¤t_str
1061 };
1062 format!("{current_value}/{total_value} {unit_str}")
1063 } else {
1064 format!("{current_str}/{total_str}")
1065 }
1066 }
1067
1068 #[must_use]
1069 pub fn render(&self) -> Vec<Segment<'static>> {
1070 #[allow(clippy::cast_possible_wrap)]
1071 let current_str = filesize::format_size(self.current as i64, self.unit, self.precision);
1072 #[allow(clippy::cast_possible_wrap)]
1073 let total_str = filesize::format_size(self.total as i64, self.unit, self.precision);
1074 let parts: Vec<&str> = total_str.rsplitn(2, ' ').collect();
1075 if parts.len() == 2 {
1076 let unit_str = parts[0];
1077 let total_value = parts[1];
1078 let current_parts: Vec<&str> = current_str.rsplitn(2, ' ').collect();
1079 let current_value = if current_parts.len() == 2 {
1080 current_parts[1]
1081 } else {
1082 ¤t_str
1083 };
1084 vec![
1085 Segment::new(current_value.to_string(), Some(self.current_style.clone())),
1086 Segment::new("/", Some(self.separator_style.clone())),
1087 Segment::new(
1088 format!("{total_value} {unit_str}"),
1089 Some(self.total_style.clone()),
1090 ),
1091 ]
1092 } else {
1093 vec![
1094 Segment::new(current_str, Some(self.current_style.clone())),
1095 Segment::new("/", Some(self.separator_style.clone())),
1096 Segment::new(total_str, Some(self.total_style.clone())),
1097 ]
1098 }
1099 }
1100}
1101
1102impl Default for DownloadColumn {
1103 fn default() -> Self {
1104 Self::new(0, 0)
1105 }
1106}
1107
1108#[derive(Debug, Clone)]
1110pub struct TransferSpeedColumn {
1111 speed: f64,
1112 unit: SizeUnit,
1113 precision: usize,
1114 style: Style,
1115}
1116
1117impl TransferSpeedColumn {
1118 #[must_use]
1119 pub fn new(speed: f64) -> Self {
1120 Self {
1121 speed,
1122 unit: SizeUnit::Decimal,
1123 precision: 1,
1124 style: Style::new().color_str("red").unwrap_or_default(),
1125 }
1126 }
1127
1128 #[must_use]
1129 pub fn from_transfer(bytes: u64, duration: Duration) -> Self {
1130 let secs = duration.as_secs_f64();
1131 #[allow(clippy::cast_precision_loss)]
1132 let speed = if secs > 0.0 { bytes as f64 / secs } else { 0.0 };
1133 Self::new(speed)
1134 }
1135
1136 #[must_use]
1137 pub fn unit(mut self, unit: SizeUnit) -> Self {
1138 self.unit = unit;
1139 self
1140 }
1141
1142 #[must_use]
1143 pub fn precision(mut self, precision: usize) -> Self {
1144 self.precision = precision;
1145 self
1146 }
1147
1148 #[must_use]
1149 pub fn style(mut self, style: Style) -> Self {
1150 self.style = style;
1151 self
1152 }
1153
1154 pub fn set_speed(&mut self, speed: f64) {
1155 self.speed = speed;
1156 }
1157
1158 pub fn update_from_transfer(&mut self, bytes: u64, duration: Duration) {
1159 let secs = duration.as_secs_f64();
1160 #[allow(clippy::cast_precision_loss)]
1161 {
1162 self.speed = if secs > 0.0 { bytes as f64 / secs } else { 0.0 };
1163 }
1164 }
1165
1166 #[must_use]
1167 pub fn speed(&self) -> f64 {
1168 self.speed
1169 }
1170
1171 #[must_use]
1172 pub fn render_plain(&self) -> String {
1173 filesize::format_speed(self.speed, self.unit, self.precision)
1174 }
1175
1176 #[must_use]
1177 pub fn render(&self) -> Vec<Segment<'static>> {
1178 vec![Segment::new(self.render_plain(), Some(self.style.clone()))]
1179 }
1180}
1181
1182impl Default for TransferSpeedColumn {
1183 fn default() -> Self {
1184 Self::new(0.0)
1185 }
1186}
1187
1188#[cfg(test)]
1189mod tests {
1190 use super::*;
1191 use crate::style::Attributes;
1192
1193 #[test]
1194 fn test_progress_bar_new() {
1195 let bar = ProgressBar::new();
1196 assert!((bar.progress() - 0.0).abs() < f64::EPSILON);
1197 assert!(!bar.is_finished());
1198 }
1199
1200 #[test]
1201 fn test_progress_bar_with_total() {
1202 let mut bar = ProgressBar::with_total(100);
1203 bar.update(50);
1204 assert!((bar.progress() - 0.5).abs() < f64::EPSILON);
1205 }
1206
1207 #[test]
1208 fn test_progress_bar_set_progress() {
1209 let mut bar = ProgressBar::new();
1210 bar.set_progress(0.75);
1211 assert!((bar.progress() - 0.75).abs() < f64::EPSILON);
1212 }
1213
1214 #[test]
1215 fn test_progress_bar_advance() {
1216 let mut bar = ProgressBar::with_total(10);
1217 bar.advance(3);
1218 assert!((bar.progress() - 0.3).abs() < f64::EPSILON);
1219 bar.advance(2);
1220 assert!((bar.progress() - 0.5).abs() < f64::EPSILON);
1221 }
1222
1223 #[test]
1224 fn test_progress_bar_finish() {
1225 let mut bar = ProgressBar::new();
1226 bar.finish();
1227 assert!(bar.is_finished());
1228 assert!((bar.progress() - 1.0).abs() < f64::EPSILON);
1229 }
1230
1231 #[test]
1232 fn test_progress_bar_render() {
1233 let mut bar = ProgressBar::new().width(20).show_brackets(true);
1234 bar.set_progress(0.5);
1235 let segments = bar.render(80);
1236 assert!(!segments.is_empty());
1237 let text: String = segments.iter().map(|s| s.text.as_ref()).collect();
1238 assert!(text.contains('['));
1239 assert!(text.contains(']'));
1240 assert!(text.contains('%'));
1241 }
1242
1243 #[test]
1244 fn test_progress_bar_render_plain() {
1245 let mut bar = ProgressBar::new().width(10).show_brackets(false);
1246 bar.set_progress(0.5);
1247 let plain = bar.render_plain(40);
1248 assert!(!plain.is_empty());
1249 }
1250
1251 #[test]
1252 fn test_progress_bar_styles() {
1253 for style in [
1254 BarStyle::Ascii,
1255 BarStyle::Block,
1256 BarStyle::Line,
1257 BarStyle::Dots,
1258 ] {
1259 let mut bar = ProgressBar::new().bar_style(style).width(10);
1260 bar.set_progress(0.5);
1261 let segments = bar.render(40);
1262 assert!(!segments.is_empty());
1263 }
1264 }
1265
1266 #[test]
1267 fn test_progress_bar_with_description() {
1268 let mut bar = ProgressBar::new().description("Downloading").width(20);
1269 bar.set_progress(0.5);
1270 let plain = bar.render_plain(80);
1271 assert!(plain.contains("Downloading"));
1272 }
1273
1274 #[test]
1275 fn test_progress_bar_description_preserves_spans() {
1276 let mut desc = Text::new("Download");
1277 desc.stylize(0, 8, Style::new().bold());
1278 let bar = ProgressBar::new().description(desc).width(20);
1279 let segments = bar.render(80);
1280 let has_bold = segments.iter().any(|seg| {
1281 seg.text.contains("Download")
1282 && seg
1283 .style
1284 .as_ref()
1285 .is_some_and(|style| style.attributes.contains(Attributes::BOLD))
1286 });
1287 assert!(has_bold, "description should preserve span styles");
1288 }
1289
1290 #[test]
1291 fn test_progress_bar_finished_message() {
1292 let mut bar = ProgressBar::new().finished_message("Done!").width(20);
1293 bar.finish();
1294 let plain = bar.render_plain(80);
1295 assert!(plain.contains("Done!"));
1296 assert!(plain.contains('✓'));
1297 }
1298
1299 #[test]
1300 fn test_spinner_next_frame() {
1301 let mut spinner = Spinner::simple();
1302 assert_eq!(spinner.next_frame(), "|");
1303 assert_eq!(spinner.next_frame(), "/");
1304 assert_eq!(spinner.next_frame(), "-");
1305 assert_eq!(spinner.next_frame(), "\\");
1306 assert_eq!(spinner.next_frame(), "|"); }
1308
1309 #[test]
1310 fn test_spinner_current_frame() {
1311 let spinner = Spinner::simple();
1312 assert_eq!(spinner.current_frame(), "|");
1313 assert_eq!(spinner.current_frame(), "|"); }
1315
1316 #[test]
1317 fn test_spinner_render() {
1318 let spinner = Spinner::dots();
1319 let segment = spinner.render();
1320 assert!(!segment.text.is_empty());
1321 }
1322
1323 #[test]
1324 fn test_bar_style_chars() {
1325 assert_eq!(BarStyle::Ascii.completed_char(), "#");
1326 assert_eq!(BarStyle::Ascii.remaining_char(), "-");
1327 assert_eq!(BarStyle::Block.completed_char(), "\u{2588}");
1328 assert_eq!(BarStyle::Block.remaining_char(), "\u{2591}");
1329 }
1330
1331 #[test]
1332 fn test_ascii_bar() {
1333 let mut bar = ascii_bar();
1334 bar.set_progress(0.5);
1335 let plain = bar.render_plain(40);
1336 assert!(plain.contains('#'));
1337 assert!(plain.contains('-'));
1338 }
1339
1340 #[test]
1341 fn test_format_duration() {
1342 assert_eq!(ProgressBar::format_duration(Duration::from_secs(30)), "30s");
1343 assert_eq!(
1344 ProgressBar::format_duration(Duration::from_secs(90)),
1345 "1:30"
1346 );
1347 assert_eq!(
1348 ProgressBar::format_duration(Duration::from_secs(3661)),
1349 "1:01:01"
1350 );
1351 }
1352
1353 #[test]
1354 fn test_progress_clamp() {
1355 let mut bar = ProgressBar::new();
1356 bar.set_progress(-0.5);
1357 assert!((bar.progress() - 0.0).abs() < f64::EPSILON);
1358 bar.set_progress(1.5);
1359 assert!((bar.progress() - 1.0).abs() < f64::EPSILON);
1360 }
1361
1362 #[test]
1363 fn test_update_clamps_progress() {
1364 let mut bar = ProgressBar::with_total(10);
1365 bar.update(15);
1366 assert!((bar.progress() - 1.0).abs() < f64::EPSILON);
1367 assert!(bar.is_finished());
1368 }
1369
1370 #[test]
1375 fn test_for_download() {
1376 let bar = ProgressBar::for_download(1_000_000);
1377 assert_eq!(bar.total_bytes_value(), Some(1_000_000));
1378 assert!(bar.show_file_size);
1379 assert!(bar.show_transfer_speed);
1380 }
1381
1382 #[test]
1383 fn test_update_bytes() {
1384 let mut bar = ProgressBar::for_download(1_000_000);
1385 bar.update_bytes(500_000);
1386 assert_eq!(bar.transferred_bytes(), 500_000);
1387 assert!((bar.progress() - 0.5).abs() < f64::EPSILON);
1388 }
1389
1390 #[test]
1391 fn test_advance_bytes() {
1392 let mut bar = ProgressBar::for_download(1_000_000);
1393 bar.advance_bytes(250_000);
1394 bar.advance_bytes(250_000);
1395 assert_eq!(bar.transferred_bytes(), 500_000);
1396 assert!((bar.progress() - 0.5).abs() < f64::EPSILON);
1397 }
1398
1399 #[test]
1400 fn test_format_file_size_decimal() {
1401 let mut bar = ProgressBar::for_download(10_000_000);
1402 bar.update_bytes(1_500_000);
1403 assert_eq!(bar.format_file_size(), "1.5 MB");
1404 assert_eq!(bar.format_total_size(), Some("10.0 MB".to_string()));
1405 }
1406
1407 #[test]
1408 fn test_format_file_size_binary() {
1409 let mut bar = ProgressBar::for_download(10_485_760) .use_binary_units(true);
1411 bar.update_bytes(1_572_864); assert_eq!(bar.format_file_size(), "1.5 MiB");
1413 assert_eq!(bar.format_total_size(), Some("10.0 MiB".to_string()));
1414 }
1415
1416 #[test]
1417 fn test_render_with_file_size() {
1418 let mut bar = ProgressBar::for_download(10_000_000)
1419 .width(20)
1420 .show_percentage(false)
1421 .show_eta(false);
1422 bar.update_bytes(5_000_000);
1423 let plain = bar.render_plain(100);
1424 assert!(plain.contains("MB") || plain.contains("bytes"));
1426 }
1427
1428 #[test]
1429 fn test_total_bytes_builder() {
1430 let bar = ProgressBar::new()
1431 .total_bytes(2_000_000)
1432 .show_file_size(true)
1433 .show_transfer_speed(true);
1434 assert_eq!(bar.total_bytes_value(), Some(2_000_000));
1435 assert!(bar.show_file_size);
1436 assert!(bar.show_transfer_speed);
1437 }
1438
1439 #[test]
1440 fn test_use_binary_units() {
1441 let bar = ProgressBar::for_download(1024).use_binary_units(true);
1442 assert!(bar.use_binary_units);
1443
1444 let bar_decimal = ProgressBar::for_download(1000).use_binary_units(false);
1445 assert!(!bar_decimal.use_binary_units);
1446 }
1447
1448 #[test]
1449 fn test_download_finishes_at_100() {
1450 let mut bar = ProgressBar::for_download(1_000_000);
1451 bar.update_bytes(1_000_000);
1452 assert!(bar.is_finished());
1453 assert!((bar.progress() - 1.0).abs() < f64::EPSILON);
1454 }
1455
1456 #[test]
1461 fn test_file_size_column_decimal() {
1462 let column = FileSizeColumn::new(1_500_000);
1463 assert_eq!(column.render_plain(), "1.5 MB");
1464 assert_eq!(column.size(), 1_500_000);
1465 }
1466
1467 #[test]
1468 fn test_file_size_column_binary() {
1469 let column = FileSizeColumn::new(1_048_576).unit(SizeUnit::Binary);
1470 assert_eq!(column.render_plain(), "1.0 MiB");
1471 }
1472
1473 #[test]
1474 fn test_file_size_column_precision() {
1475 let column = FileSizeColumn::new(1_234_567).precision(2);
1476 assert_eq!(column.render_plain(), "1.23 MB");
1477 }
1478
1479 #[test]
1480 fn test_file_size_column_set_size() {
1481 let mut column = FileSizeColumn::new(1000);
1482 column.set_size(2_000_000);
1483 assert_eq!(column.size(), 2_000_000);
1484 assert_eq!(column.render_plain(), "2.0 MB");
1485 }
1486
1487 #[test]
1488 fn test_total_file_size_column() {
1489 let column = TotalFileSizeColumn::new(10_000_000);
1490 assert_eq!(column.render_plain(), "10.0 MB");
1491 }
1492
1493 #[test]
1494 fn test_download_column() {
1495 let column = DownloadColumn::new(1_500_000, 10_000_000);
1496 assert_eq!(column.render_plain(), "1.5/10.0 MB");
1497 assert_eq!(column.current(), 1_500_000);
1498 assert_eq!(column.total(), 10_000_000);
1499 }
1500
1501 #[test]
1502 fn test_download_column_binary() {
1503 let column = DownloadColumn::new(1_048_576, 10_485_760).unit(SizeUnit::Binary);
1504 assert_eq!(column.render_plain(), "1.0/10.0 MiB");
1505 }
1506
1507 #[test]
1508 fn test_download_column_update() {
1509 let mut column = DownloadColumn::new(0, 1000);
1510 column.set_current(500);
1511 assert_eq!(column.current(), 500);
1512 column.set_total(2000);
1513 assert_eq!(column.total(), 2000);
1514 }
1515
1516 #[test]
1517 fn test_transfer_speed_column() {
1518 let column = TransferSpeedColumn::new(1_500_000.0);
1519 assert_eq!(column.render_plain(), "1.5 MB/s");
1520 assert!((column.speed() - 1_500_000.0).abs() < f64::EPSILON);
1521 }
1522
1523 #[test]
1524 fn test_transfer_speed_column_binary() {
1525 let column = TransferSpeedColumn::new(1_048_576.0).unit(SizeUnit::Binary);
1526 assert_eq!(column.render_plain(), "1.0 MiB/s");
1527 }
1528
1529 #[test]
1530 fn test_transfer_speed_from_transfer() {
1531 let column = TransferSpeedColumn::from_transfer(1_000_000, Duration::from_secs(1));
1532 assert!((column.speed() - 1_000_000.0).abs() < f64::EPSILON);
1533 }
1534
1535 #[test]
1536 fn test_transfer_speed_update() {
1537 let mut column = TransferSpeedColumn::new(0.0);
1538 column.set_speed(5_000_000.0);
1539 assert!((column.speed() - 5_000_000.0).abs() < f64::EPSILON);
1540 }
1541
1542 #[test]
1543 fn test_column_default_impls() {
1544 assert_eq!(FileSizeColumn::default().size(), 0);
1545 assert_eq!(TotalFileSizeColumn::default().render_plain(), "0 bytes");
1546 assert_eq!(DownloadColumn::default().current(), 0);
1547 assert!((TransferSpeedColumn::default().speed() - 0.0).abs() < f64::EPSILON);
1548 }
1549}