1use std::cell::RefCell;
20use std::collections::{BTreeMap, HashMap, VecDeque};
21use std::sync::Arc;
22
23use crate::console::{Console, ConsoleOptions, Justify};
24use crate::filesize;
25use crate::progress_bar::ProgressBar;
26use crate::protocol::Renderable;
27use crate::pyformat::{self, FormatValue};
28use crate::segment::Segment;
29use crate::spinner::Spinner;
30use crate::style::{Style, StyleType};
31use crate::table::{Cell, ColumnOptions, Table};
32use crate::text::Text;
33
34const MAX_SAMPLES: usize = 1000;
36
37pub use crate::console::GetTime;
39
40use crate::console::monotonic;
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
44pub struct TaskId(pub usize);
45
46pub struct TimeRemainingColumn {
49 compact: bool,
50 elapsed_when_finished: bool,
51 cache: RefCell<HashMap<TaskId, (f64, Text)>>,
52}
53
54impl TimeRemainingColumn {
55 pub fn new(compact: bool, elapsed_when_finished: bool) -> Self {
58 TimeRemainingColumn {
59 compact,
60 elapsed_when_finished,
61 cache: RefCell::new(HashMap::new()),
62 }
63 }
64}
65
66pub struct SpinnerColumn {
69 spinner: Spinner,
70 style: StyleType,
71 finished_text: String,
72}
73
74impl SpinnerColumn {
75 pub fn new(name: &str, finished_text: impl Into<String>) -> Self {
78 SpinnerColumn {
79 spinner: Spinner::new(name),
80 style: StyleType::Name("progress.spinner".to_string()),
81 finished_text: finished_text.into(),
82 }
83 }
84
85 pub fn speed(mut self, speed: f64) -> Self {
87 self.spinner = self.spinner.speed(speed);
88 self
89 }
90
91 pub fn style(mut self, style: impl Into<StyleType>) -> Self {
93 self.style = style.into();
94 self
95 }
96}
97
98pub struct TextColumn {
103 text_format: String,
104 style: StyleType,
105 justify: Justify,
106 markup: bool,
107}
108
109impl TextColumn {
110 pub fn new(text_format: impl Into<String>) -> Self {
113 TextColumn {
114 text_format: text_format.into(),
115 style: StyleType::default(),
116 justify: Justify::Left,
117 markup: true,
118 }
119 }
120
121 pub fn style(mut self, style: impl Into<StyleType>) -> Self {
123 self.style = style.into();
124 self
125 }
126
127 pub fn justify(mut self, justify: Justify) -> Self {
129 self.justify = justify;
130 self
131 }
132
133 pub fn markup(mut self, markup: bool) -> Self {
135 self.markup = markup;
136 self
137 }
138
139 fn render(&self, task: &Task) -> Text {
140 let expanded = pyformat::format(&self.text_format, |name| task.format_field(name));
141 let mut text = if self.markup {
142 Text::from_markup(&expanded).unwrap_or_else(|_| Text::new(expanded.clone()))
143 } else {
144 Text::new(expanded)
145 };
146 text.set_base_style(self.style.clone());
147 text.set_justify(self.justify);
148 text
149 }
150}
151
152pub enum ProgressColumn {
154 Description,
157 Text(String, Style),
159 TextFormat(TextColumn),
161 Renderable(Arc<dyn Renderable + Send + Sync>),
164 Bar,
166 Percentage,
168 TaskProgress { show_speed: bool },
171 MofN,
173 Download,
176 BinaryDownload,
178 TimeElapsed,
180 TimeRemaining(TimeRemainingColumn),
182 TransferSpeed,
184 FileSize,
186 TotalFileSize,
188 Spinner(SpinnerColumn),
190 BarWith(BarColumn),
193 WithTableColumn(Box<ProgressColumn>, ColumnOptions),
196 Custom(CustomColumn),
199}
200
201pub trait CustomProgressColumn: Send + Sync {
205 fn render(&self, task: &Task) -> Cell;
207
208 fn table_column(&self) -> ColumnOptions {
211 ColumnOptions::default()
212 }
213
214 fn max_refresh(&self) -> Option<f64> {
218 None
219 }
220}
221
222pub struct CustomColumn {
225 column: Arc<dyn CustomProgressColumn>,
226 cache: RefCell<HashMap<TaskId, (f64, Cell)>>,
227}
228
229impl CustomColumn {
230 pub fn new(column: Arc<dyn CustomProgressColumn>) -> Self {
232 CustomColumn {
233 column,
234 cache: RefCell::new(HashMap::new()),
235 }
236 }
237
238 pub fn column(&self) -> &Arc<dyn CustomProgressColumn> {
240 &self.column
241 }
242
243 fn call(&self, task: &Task) -> Cell {
245 let now = task.now();
246 let max_refresh = self.column.max_refresh();
247 if let Some(max_refresh) = max_refresh
248 && task.completed == 0.0
249 && let Some((timestamp, cell)) = self.cache.borrow().get(&task.id)
250 && timestamp + max_refresh > now
251 {
252 return cell.clone();
253 }
254 let cell = self.column.render(task);
255 self.cache.borrow_mut().insert(task.id, (now, cell.clone()));
256 cell
257 }
258}
259
260#[derive(Clone, Debug)]
262pub struct BarColumn {
263 bar_width: Option<usize>,
264 style: StyleType,
265 complete_style: StyleType,
266 finished_style: StyleType,
267 pulse_style: StyleType,
268}
269
270impl Default for BarColumn {
271 fn default() -> Self {
272 BarColumn {
273 bar_width: Some(40),
274 style: "bar.back".into(),
275 complete_style: "bar.complete".into(),
276 finished_style: "bar.finished".into(),
277 pulse_style: "bar.pulse".into(),
278 }
279 }
280}
281
282impl BarColumn {
283 pub fn new() -> Self {
285 BarColumn::default()
286 }
287
288 pub fn bar_width(mut self, width: Option<usize>) -> Self {
290 self.bar_width = width;
291 self
292 }
293
294 pub fn style(mut self, style: impl Into<StyleType>) -> Self {
296 self.style = style.into();
297 self
298 }
299
300 pub fn complete_style(mut self, style: impl Into<StyleType>) -> Self {
302 self.complete_style = style.into();
303 self
304 }
305
306 pub fn finished_style(mut self, style: impl Into<StyleType>) -> Self {
308 self.finished_style = style.into();
309 self
310 }
311
312 pub fn pulse_style(mut self, style: impl Into<StyleType>) -> Self {
314 self.pulse_style = style.into();
315 self
316 }
317
318 fn render(&self, task: &Task) -> ProgressBar {
320 let bar = match task.total {
321 Some(total) => ProgressBar::new(total.max(0.0), task.completed.max(0.0)),
322 None => ProgressBar::indeterminate(),
323 };
324 let bar = match self.bar_width {
325 Some(width) => bar.width(width.max(1)),
326 None => bar,
327 };
328 bar.pulse(!task.started())
329 .animation_time(task.now())
330 .style(self.style.clone())
331 .complete_style(self.complete_style.clone())
332 .finished_style(self.finished_style.clone())
333 .pulse_style(self.pulse_style.clone())
334 }
335}
336
337impl ProgressColumn {
338 pub fn time_remaining() -> Self {
340 ProgressColumn::TimeRemaining(TimeRemainingColumn::new(false, false))
341 }
342
343 pub fn spinner() -> Self {
345 ProgressColumn::Spinner(SpinnerColumn::new("dots", " "))
346 }
347
348 pub fn custom(column: impl CustomProgressColumn + 'static) -> Self {
350 ProgressColumn::Custom(CustomColumn::new(Arc::new(column)))
351 }
352
353 pub fn with_table_column(self, options: ColumnOptions) -> Self {
357 let inner = match self {
358 ProgressColumn::WithTableColumn(inner, _) => *inner,
359 column => column,
360 };
361 ProgressColumn::WithTableColumn(Box::new(inner), options)
362 }
363
364 pub fn table_column(&self) -> ColumnOptions {
367 match self {
368 ProgressColumn::WithTableColumn(_, options) => options.clone(),
369 ProgressColumn::Custom(column) => column.column.table_column(),
370 ProgressColumn::Description
371 | ProgressColumn::Text(..)
372 | ProgressColumn::TextFormat(_)
373 | ProgressColumn::Percentage
374 | ProgressColumn::TaskProgress { .. } => ColumnOptions {
375 no_wrap: true,
376 ..ColumnOptions::default()
377 },
378 _ => ColumnOptions::default(),
379 }
380 }
381
382 pub fn table_cell(&self, task: &Task) -> Cell {
386 match self {
387 ProgressColumn::WithTableColumn(inner, _) => inner.table_cell(task),
388 ProgressColumn::Custom(column) => column.call(task),
389 ProgressColumn::Bar => Cell::Renderable(Arc::new(BarColumn::default().render(task))),
390 ProgressColumn::BarWith(column) => Cell::Renderable(Arc::new(column.render(task))),
391 ProgressColumn::Renderable(renderable) => Cell::Renderable(renderable.clone()),
392 column => Cell::Text(column.cell(task)),
393 }
394 }
395
396 fn cell(&self, task: &Task) -> Text {
398 let named = |plain: String, style: &str| Text::styled(plain, style);
399 match self {
400 ProgressColumn::Description => {
403 let markup = format!("[progress.description]{}", task.description);
404 Text::from_markup(&markup)
405 .unwrap_or_else(|_| Text::new(task.description.clone()))
406 .justify(Justify::Left)
407 }
408 ProgressColumn::Text(text, style) => {
409 Text::styled(text.clone(), style.clone()).justify(Justify::Left)
410 }
411 ProgressColumn::TextFormat(column) => column.render(task),
412 ProgressColumn::Bar
413 | ProgressColumn::BarWith(_)
414 | ProgressColumn::Renderable(_)
415 | ProgressColumn::Custom(_)
416 | ProgressColumn::WithTableColumn(..) => {
417 unreachable!("bar, renderable and wrapped columns have no text cell")
418 }
419 ProgressColumn::Percentage => task.percentage_cell().justify(Justify::Left),
420 ProgressColumn::TaskProgress { show_speed } => {
421 if task.total.is_none() && *show_speed {
422 render_speed(
423 task.finished_speed
424 .filter(|s| *s != 0.0)
425 .or_else(|| task.speed()),
426 )
427 } else {
428 task.percentage_cell().justify(Justify::Left)
429 }
430 }
431 ProgressColumn::MofN => named(task.mofn_text(), "progress.download"),
432 ProgressColumn::Download => named(task.download_text(false), "progress.download"),
433 ProgressColumn::BinaryDownload => named(task.download_text(true), "progress.download"),
434 ProgressColumn::TimeElapsed => {
435 let elapsed = if task.finished() {
436 task.finished_time
437 } else {
438 task.elapsed()
439 };
440 let text = match elapsed {
441 None => "-:--:--".to_string(),
442 Some(elapsed) => timedelta(elapsed.max(0.0) as i64),
443 };
444 named(text, "progress.elapsed")
445 }
446 ProgressColumn::TimeRemaining(column) => column.render(task),
447 ProgressColumn::TransferSpeed => {
448 let speed = task
449 .finished_speed
450 .filter(|s| *s != 0.0)
451 .or_else(|| task.speed());
452 let text = match speed {
453 None => "?".to_string(),
454 Some(speed) => format!("{}/s", filesize::decimal_signed(speed as i64)),
455 };
456 named(text, "progress.data.speed")
457 }
458 ProgressColumn::FileSize => named(
461 filesize::decimal_signed(task.completed as i64),
462 "progress.filesize",
463 ),
464 ProgressColumn::TotalFileSize => named(
465 task.total
466 .map_or_else(String::new, |total| filesize::decimal_signed(total as i64)),
467 "progress.filesize.total",
468 ),
469 ProgressColumn::Spinner(column) => {
470 if task.finished() {
471 Text::from_markup(&column.finished_text)
472 .unwrap_or_else(|_| Text::new(column.finished_text.clone()))
473 } else {
474 let mut frame = column.spinner.render(task.now());
477 frame.set_base_style(column.style.clone());
478 frame
479 }
480 }
481 }
482 }
483}
484
485impl TimeRemainingColumn {
486 fn render(&self, task: &Task) -> Text {
487 let now = task.now();
490 if task.completed == 0.0
491 && let Some((timestamp, text)) = self.cache.borrow().get(&task.id)
492 && timestamp + 0.5 > now
493 {
494 return text.clone();
495 }
496 let (task_time, style) = if self.elapsed_when_finished && task.finished() {
497 (task.finished_time, "progress.elapsed")
498 } else {
499 (task.time_remaining(), "progress.remaining")
500 };
501 let text = if task.total.is_none() {
502 Text::styled("", style)
503 } else {
504 match task_time {
505 None => Text::styled(if self.compact { "--:--" } else { "-:--:--" }, style),
506 Some(task_time) => {
507 let whole = task_time as i64;
508 let (minutes, seconds) = (whole.div_euclid(60), whole.rem_euclid(60));
509 let (hours, minutes) = (minutes.div_euclid(60), minutes.rem_euclid(60));
510 let formatted = if self.compact && hours == 0 {
511 format!("{minutes:02}:{seconds:02}")
512 } else {
513 format!("{hours}:{minutes:02}:{seconds:02}")
514 };
515 Text::styled(formatted, style)
516 }
517 }
518 };
519 self.cache.borrow_mut().insert(task.id, (now, text.clone()));
520 text
521 }
522}
523
524fn render_speed(speed: Option<f64>) -> Text {
527 let Some(speed) = speed else {
528 return Text::styled("", "progress.percentage");
529 };
530 let (unit, suffix) = filesize::pick_unit_and_suffix_signed(
531 speed as i64,
532 &["", "×10³", "×10⁶", "×10⁹", "×10¹²"],
533 1000,
534 );
535 let data_speed = speed / unit as f64;
536 Text::styled(
537 format!("{data_speed:.1}{suffix} it/s"),
538 "progress.percentage",
539 )
540}
541
542fn timedelta(total_seconds: i64) -> String {
545 let days = total_seconds / 86_400;
546 let rest = total_seconds % 86_400;
547 let clock = format!("{}:{:02}:{:02}", rest / 3600, rest % 3600 / 60, rest % 60);
548 match days {
549 0 => clock,
550 1 => format!("1 day, {clock}"),
551 days => format!("{days} days, {clock}"),
552 }
553}
554
555fn grouped(value: f64, precision: usize) -> String {
557 let formatted = format!("{value:.precision$}");
558 let (sign, digits) = match formatted.strip_prefix('-') {
559 Some(rest) => ("-", rest),
560 None => ("", formatted.as_str()),
561 };
562 let (integer, fraction) = match digits.split_once('.') {
563 Some((integer, fraction)) => (integer, Some(fraction)),
564 None => (digits, None),
565 };
566 let mut grouped = String::new();
567 for (index, digit) in integer.chars().enumerate() {
568 if index > 0 && (integer.len() - index) % 3 == 0 {
569 grouped.push(',');
570 }
571 grouped.push(digit);
572 }
573 match fraction {
574 Some(fraction) => format!("{sign}{grouped}.{fraction}"),
575 None => format!("{sign}{grouped}"),
576 }
577}
578
579pub struct Task {
582 id: TaskId,
583 description: String,
584 total: Option<f64>,
585 completed: f64,
586 visible: bool,
587 start_time: Option<f64>,
588 stop_time: Option<f64>,
589 finished_time: Option<f64>,
590 finished_speed: Option<f64>,
591 samples: VecDeque<(f64, f64)>,
593 fields: BTreeMap<String, FormatValue>,
595 get_time: GetTime,
596}
597
598impl Task {
599 pub fn fields(&self) -> &BTreeMap<String, FormatValue> {
601 &self.fields
602 }
603
604 fn format_field(&self, name: &str) -> Option<FormatValue> {
608 let attribute = name.strip_prefix("task.")?;
609 if let Some(key) = attribute
610 .strip_prefix("fields[")
611 .and_then(|rest| rest.strip_suffix(']'))
612 {
613 return self.fields.get(key).cloned();
614 }
615 Some(match attribute {
616 "id" => FormatValue::Int(self.id.0 as i64),
617 "description" => FormatValue::Str(self.description.clone()),
618 "total" => self.total.map_or(FormatValue::None, whole_number),
621 "completed" => whole_number(self.completed),
622 "visible" => FormatValue::Bool(self.visible),
623 "started" => FormatValue::Bool(self.started()),
624 "finished" => FormatValue::Bool(self.finished()),
625 "percentage" => FormatValue::Float(self.percentage()),
626 "remaining" => self.remaining().into(),
627 "elapsed" => self.elapsed().into(),
628 "speed" => self.speed().into(),
629 "time_remaining" => self.time_remaining().into(),
630 "start_time" => self.start_time.into(),
631 "stop_time" => self.stop_time.into(),
632 "finished_time" => self.finished_time.into(),
633 "finished_speed" => self.finished_speed.into(),
634 _ => return None,
635 })
636 }
637 fn now(&self) -> f64 {
638 (self.get_time)()
639 }
640
641 pub fn get_time(&self) -> f64 {
643 self.now()
644 }
645
646 pub fn id(&self) -> TaskId {
648 self.id
649 }
650
651 pub fn description(&self) -> &str {
653 &self.description
654 }
655
656 pub fn total(&self) -> Option<f64> {
658 self.total
659 }
660
661 pub fn completed(&self) -> f64 {
663 self.completed
664 }
665
666 pub fn visible(&self) -> bool {
668 self.visible
669 }
670
671 pub fn started(&self) -> bool {
673 self.start_time.is_some()
674 }
675
676 pub fn remaining(&self) -> Option<f64> {
678 self.total.map(|total| total - self.completed)
679 }
680
681 pub fn elapsed(&self) -> Option<f64> {
683 let start = self.start_time?;
684 Some(self.stop_time.unwrap_or_else(|| self.now()) - start)
685 }
686
687 pub fn finished(&self) -> bool {
689 self.finished_time.is_some()
690 }
691
692 pub fn finished_time(&self) -> Option<f64> {
694 self.finished_time
695 }
696
697 pub fn percentage(&self) -> f64 {
699 match self.total {
700 Some(total) if total != 0.0 => (self.completed / total * 100.0).clamp(0.0, 100.0),
701 _ => 0.0,
702 }
703 }
704
705 pub fn speed(&self) -> Option<f64> {
707 self.start_time?;
708 let (first, _) = *self.samples.front()?;
709 let (last, _) = *self.samples.back()?;
710 let total_time = last - first;
711 if total_time == 0.0 {
712 return None;
713 }
714 let total_completed: f64 = self.samples.iter().skip(1).map(|(_, done)| done).sum();
715 Some(total_completed / total_time)
716 }
717
718 pub fn time_remaining(&self) -> Option<f64> {
720 if self.finished() {
721 return Some(0.0);
722 }
723 let speed = self.speed().filter(|speed| *speed != 0.0)?;
724 let remaining = self.remaining()?;
725 Some((remaining / speed).ceil())
726 }
727
728 fn clear_progress(&mut self) {
730 self.samples.clear();
731 self.finished_time = None;
732 self.finished_speed = None;
733 }
734
735 fn percentage_cell(&self) -> Text {
738 if self.total.is_none() {
739 return Text::new("");
740 }
741 let mut text = Text::new(format!("{:>3.0}%", self.percentage()));
742 let len = text.plain().len();
743 text.stylize("progress.percentage", 0, len);
744 text
745 }
746
747 fn mofn_text(&self) -> String {
750 let completed = self.completed as i64;
751 let total = self
752 .total
753 .map_or_else(|| "?".to_string(), |total| (total as i64).to_string());
754 let total_width = total.chars().count();
755 format!("{completed:>total_width$}/{total}")
756 }
757
758 fn download_text(&self, binary: bool) -> String {
761 const DECIMAL: &[&str] = &["bytes", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
762 const BINARY: &[&str] = &[
763 "bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB",
764 ];
765 let completed = self.completed as i64;
767 let base_size = self.total.map_or(completed, |total| total as i64);
768 let (unit, suffix) = if binary {
769 filesize::pick_unit_and_suffix_signed(base_size, BINARY, 1024)
770 } else {
771 filesize::pick_unit_and_suffix_signed(base_size, DECIMAL, 1000)
772 };
773 let precision = if unit == 1 { 0 } else { 1 };
774 let completed_str = grouped(completed as f64 / unit as f64, precision);
775 let total_str = self.total.map_or_else(
776 || "?".to_string(),
777 |total| grouped((total as i64) as f64 / unit as f64, precision),
778 );
779 format!("{completed_str}/{total_str} {suffix}")
780 }
781}
782
783#[derive(Debug, Clone, Default)]
786pub struct TaskUpdate {
787 pub total: Option<f64>,
788 pub completed: Option<f64>,
789 pub advance: Option<f64>,
790 pub description: Option<String>,
791 pub visible: Option<bool>,
792 pub fields: Vec<(String, FormatValue)>,
794 pub refresh: bool,
796}
797
798impl TaskUpdate {
799 pub fn total(mut self, total: f64) -> Self {
800 self.total = Some(total);
801 self
802 }
803
804 pub fn completed(mut self, completed: f64) -> Self {
805 self.completed = Some(completed);
806 self
807 }
808
809 pub fn advance(mut self, advance: f64) -> Self {
810 self.advance = Some(advance);
811 self
812 }
813
814 pub fn description(mut self, description: impl Into<String>) -> Self {
815 self.description = Some(description.into());
816 self
817 }
818
819 pub fn visible(mut self, visible: bool) -> Self {
820 self.visible = Some(visible);
821 self
822 }
823
824 pub fn refresh(mut self, refresh: bool) -> Self {
827 self.refresh = refresh;
828 self
829 }
830
831 pub fn field(mut self, name: impl Into<String>, value: impl Into<FormatValue>) -> Self {
833 self.fields.push((name.into(), value.into()));
834 self
835 }
836}
837
838pub struct Progress {
840 tasks: Vec<Task>,
841 next_id: usize,
842 columns: Vec<ProgressColumn>,
843 get_time: GetTime,
844 speed_estimate_period: f64,
845 expand: bool,
846 transient: bool,
847 disable: bool,
848 redirect_stdout: bool,
849 redirect_stderr: bool,
850}
851
852impl Default for Progress {
853 fn default() -> Self {
854 Progress {
855 tasks: Vec::new(),
856 next_id: 0,
857 columns: Progress::default_columns(),
858 get_time: Arc::new(monotonic),
859 speed_estimate_period: 30.0,
860 expand: false,
861 transient: false,
862 disable: false,
863 redirect_stdout: true,
864 redirect_stderr: true,
865 }
866 }
867}
868
869impl Progress {
870 pub fn new() -> Self {
871 Progress::default()
872 }
873
874 pub fn default_columns() -> Vec<ProgressColumn> {
877 vec![
878 ProgressColumn::Description,
879 ProgressColumn::Bar,
880 ProgressColumn::Percentage,
881 ProgressColumn::time_remaining(),
882 ]
883 }
884
885 pub fn columns(mut self, columns: Vec<ProgressColumn>) -> Self {
887 self.columns = columns;
888 self
889 }
890
891 pub fn clock(mut self, clock: impl Fn() -> f64 + Send + Sync + 'static) -> Self {
894 self.get_time = Arc::new(clock);
895 for task in &mut self.tasks {
896 task.get_time = self.get_time.clone();
897 }
898 self
899 }
900
901 pub fn expand(mut self, expand: bool) -> Self {
903 self.expand = expand;
904 self
905 }
906
907 pub fn transient(mut self, transient: bool) -> Self {
909 self.transient = transient;
910 self
911 }
912
913 pub fn redirect_stdout(mut self, redirect: bool) -> Self {
916 self.redirect_stdout = redirect;
917 self
918 }
919
920 pub fn redirect_stderr(mut self, redirect: bool) -> Self {
922 self.redirect_stderr = redirect;
923 self
924 }
925
926 pub fn disable(mut self, disable: bool) -> Self {
929 self.disable = disable;
930 self
931 }
932
933 pub fn speed_estimate_period(mut self, seconds: f64) -> Self {
935 self.speed_estimate_period = seconds;
936 self
937 }
938
939 fn now(&self) -> f64 {
940 (self.get_time)()
941 }
942
943 fn task_mut(&mut self, id: TaskId) -> Option<&mut Task> {
944 self.tasks.iter_mut().find(|task| task.id == id)
945 }
946
947 pub fn add_task(
950 &mut self,
951 description: impl Into<String>,
952 total: impl Into<Option<f64>>,
953 completed: f64,
954 ) -> TaskId {
955 let id = self.push_task(description.into(), total.into(), completed);
956 self.start_task(id);
957 id
958 }
959
960 pub fn add_unstarted_task(
963 &mut self,
964 description: impl Into<String>,
965 total: impl Into<Option<f64>>,
966 completed: f64,
967 ) -> TaskId {
968 self.push_task(description.into(), total.into(), completed)
969 }
970
971 pub fn add_task_with<K: Into<String>, V: Into<FormatValue>>(
974 &mut self,
975 description: impl Into<String>,
976 total: impl Into<Option<f64>>,
977 completed: f64,
978 start: bool,
979 fields: impl IntoIterator<Item = (K, V)>,
980 ) -> TaskId {
981 let id = self.push_task(description.into(), total.into(), completed);
982 if let Some(task) = self.task_mut(id) {
983 task.fields = fields
984 .into_iter()
985 .map(|(name, value)| (name.into(), value.into()))
986 .collect();
987 }
988 if start {
989 self.start_task(id);
990 }
991 id
992 }
993
994 fn push_task(&mut self, description: String, total: Option<f64>, completed: f64) -> TaskId {
995 let id = TaskId(self.next_id);
996 self.next_id += 1;
997 self.tasks.push(Task {
998 id,
999 description,
1000 total,
1001 completed,
1002 visible: true,
1003 start_time: None,
1004 stop_time: None,
1005 finished_time: None,
1006 finished_speed: None,
1007 samples: VecDeque::new(),
1008 fields: BTreeMap::new(),
1009 get_time: self.get_time.clone(),
1010 });
1011 id
1012 }
1013
1014 pub fn task(&self, id: TaskId) -> Option<&Task> {
1016 self.tasks.iter().find(|task| task.id == id)
1017 }
1018
1019 pub fn tasks(&self) -> &[Task] {
1021 &self.tasks
1022 }
1023
1024 pub fn finished(&self) -> bool {
1026 self.tasks.iter().all(Task::finished)
1027 }
1028
1029 pub fn start_task(&mut self, id: TaskId) {
1031 let now = self.now();
1032 if let Some(task) = self.task_mut(id) {
1033 task.start_time.get_or_insert(now);
1034 }
1035 }
1036
1037 pub fn stop_task(&mut self, id: TaskId) {
1039 let now = self.now();
1040 if let Some(task) = self.task_mut(id) {
1041 task.start_time.get_or_insert(now);
1042 task.stop_time = Some(now);
1043 }
1044 }
1045
1046 pub fn update(&mut self, id: TaskId, update: TaskUpdate) {
1050 let now = self.now();
1051 let period = self.speed_estimate_period;
1052 let Some(task) = self.task_mut(id) else {
1053 return;
1054 };
1055 let completed_start = task.completed;
1056 if let Some(total) = update.total
1057 && Some(total) != task.total
1058 {
1059 task.total = Some(total);
1060 task.clear_progress();
1061 }
1062 if let Some(advance) = update.advance {
1063 task.completed += advance;
1064 }
1065 if let Some(completed) = update.completed {
1066 task.completed = completed;
1067 }
1068 if let Some(description) = update.description {
1069 task.description = description;
1070 }
1071 if let Some(visible) = update.visible {
1072 task.visible = visible;
1073 }
1074 task.fields.extend(update.fields);
1075 let update_completed = task.completed - completed_start;
1076 let old_sample_time = now - period;
1077 while task
1078 .samples
1079 .front()
1080 .is_some_and(|(time, _)| *time < old_sample_time)
1081 {
1082 task.samples.pop_front();
1083 }
1084 if update_completed > 0.0 {
1085 task.samples.push_back((now, update_completed));
1086 if task.samples.len() > MAX_SAMPLES {
1087 task.samples.pop_front();
1088 }
1089 }
1090 if task.total.is_some_and(|total| task.completed >= total) && task.finished_time.is_none() {
1091 task.finished_time = task.elapsed();
1092 }
1093 }
1094
1095 pub fn advance(&mut self, id: TaskId, amount: f64) {
1098 let now = self.now();
1099 let period = self.speed_estimate_period;
1100 let Some(task) = self.task_mut(id) else {
1101 return;
1102 };
1103 let completed_start = task.completed;
1104 task.completed += amount;
1105 let update_completed = task.completed - completed_start;
1106 let old_sample_time = now - period;
1107 while task
1108 .samples
1109 .front()
1110 .is_some_and(|(time, _)| *time < old_sample_time)
1111 {
1112 task.samples.pop_front();
1113 }
1114 while task.samples.len() > MAX_SAMPLES {
1115 task.samples.pop_front();
1116 }
1117 task.samples.push_back((now, update_completed));
1118 if task.samples.len() > MAX_SAMPLES {
1119 task.samples.pop_front();
1120 }
1121 if task.total.is_some_and(|total| task.completed >= total) && task.finished_time.is_none() {
1122 task.finished_time = task.elapsed();
1123 task.finished_speed = task.speed();
1124 }
1125 }
1126
1127 pub fn reset(&mut self, id: TaskId, start: bool, total: Option<f64>, completed: f64) {
1131 let now = self.now();
1132 let Some(task) = self.task_mut(id) else {
1133 return;
1134 };
1135 task.clear_progress();
1136 task.start_time = start.then_some(now);
1137 if let Some(total) = total {
1138 task.total = Some(total);
1139 }
1140 task.completed = completed;
1141 task.finished_time = None;
1142 }
1143
1144 pub fn remove_task(&mut self, id: TaskId) {
1146 self.tasks.retain(|task| task.id != id);
1147 }
1148}
1149
1150impl Progress {
1151 pub fn make_tasks_table(&self) -> Table {
1156 let mut table = Table::grid().padding(0, 1, 0, 1).expand(self.expand);
1157 for column in &self.columns {
1158 table.add_column_with(Text::new(""), column.table_column());
1159 }
1160 for task in self.tasks.iter().filter(|task| task.visible) {
1161 let cells = self
1164 .columns
1165 .iter()
1166 .map(|column| column.table_cell(task))
1167 .collect();
1168 table.add_row_cells(cells);
1169 }
1170 table
1171 }
1172}
1173
1174impl Renderable for Progress {
1175 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
1176 self.make_tasks_table().rich_render(console, options)
1177 }
1178}
1179
1180impl Progress {
1181 pub fn start<W: std::io::Write + Send + 'static>(
1186 self,
1187 console: Console,
1188 writer: W,
1189 refresh_per_second: f64,
1190 ) -> LiveProgress<W> {
1191 if self.disable {
1193 return LiveProgress {
1194 progress: Arc::new(std::sync::Mutex::new(self)),
1195 live: None,
1196 disabled: Some(std::sync::Mutex::new((console, writer))),
1197 interactive: true,
1198 holder: Arc::new(std::sync::Mutex::new(None)),
1199 handle: crate::live::LiveHandle::detached(),
1200 };
1201 }
1202 let options = crate::live::LiveOptions {
1203 transient: self.transient,
1204 redirect_stdout: self.redirect_stdout,
1205 redirect_stderr: self.redirect_stderr,
1206 ..crate::live::LiveOptions::default()
1207 };
1208 let interactive = console.is_terminal() && !console.is_dumb_terminal();
1210 let shared = Arc::new(std::sync::Mutex::new(self));
1211 let view = shared.clone();
1212 let live = crate::live::Live::spawn_get_renderable(
1213 move || {
1215 let progress = view.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
1216 Box::new(progress.make_tasks_table()) as Box<dyn Renderable>
1217 },
1218 console,
1219 writer,
1220 refresh_per_second,
1221 options,
1222 );
1223 let holder: crate::live::Holder = Arc::new(std::sync::Mutex::new(None));
1224 LiveProgress {
1225 progress: shared,
1226 handle: live.handle().with_holder(holder.clone()),
1227 live: Some(live),
1228 disabled: None,
1229 interactive,
1230 holder,
1231 }
1232 }
1233}
1234
1235pub struct LiveProgress<W: std::io::Write + Send + 'static> {
1238 progress: Arc<std::sync::Mutex<Progress>>,
1239 live: Option<crate::live::AutoLive<W>>,
1240 disabled: Option<std::sync::Mutex<(Console, W)>>,
1243 interactive: bool,
1245 holder: crate::live::Holder,
1249 handle: crate::live::LiveHandle<W>,
1251}
1252
1253struct HolderGuard<'a>(&'a crate::live::Holder);
1256
1257impl Drop for HolderGuard<'_> {
1258 fn drop(&mut self) {
1259 *self
1260 .0
1261 .lock()
1262 .unwrap_or_else(|poisoned| poisoned.into_inner()) = None;
1263 }
1264}
1265
1266impl<W: std::io::Write + Send + 'static> LiveProgress<W> {
1267 pub fn with<R>(&self, f: impl FnOnce(&mut Progress) -> R) -> R {
1274 let current = std::thread::current().id();
1275 assert!(
1276 !self.held_by(current),
1277 "LiveProgress::with re-entered from inside a `with` closure; \
1278 use the `&mut Progress` it was given instead"
1279 );
1280 let mut progress = match self.progress.lock() {
1281 Ok(progress) => progress,
1282 Err(poisoned) => poisoned.into_inner(),
1283 };
1284 *self
1285 .holder
1286 .lock()
1287 .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(current);
1288 let _holder = HolderGuard(&self.holder);
1289 f(&mut progress)
1290 }
1291
1292 fn held_by(&self, thread: std::thread::ThreadId) -> bool {
1294 *self
1295 .holder
1296 .lock()
1297 .unwrap_or_else(|poisoned| poisoned.into_inner())
1298 == Some(thread)
1299 }
1300
1301 pub fn add_task(
1303 &self,
1304 description: impl Into<String>,
1305 total: impl Into<Option<f64>>,
1306 completed: f64,
1307 ) -> TaskId {
1308 let id = self.with(|progress| progress.add_task(description, total, completed));
1309 self.refresh();
1310 id
1311 }
1312
1313 pub fn reset(&self, id: TaskId, start: bool, total: Option<f64>, completed: f64) {
1315 self.with(|progress| progress.reset(id, start, total, completed));
1316 self.refresh();
1317 }
1318
1319 pub fn advance(&self, id: TaskId, amount: f64) {
1321 self.with(|progress| progress.advance(id, amount));
1322 }
1323
1324 pub fn update(&self, id: TaskId, update: TaskUpdate) {
1327 let refresh = update.refresh;
1328 self.with(|progress| progress.update(id, update));
1329 if refresh {
1330 self.refresh();
1331 }
1332 }
1333
1334 pub fn refresh(&self) {
1341 if let Some(live) = &self.live {
1342 if self.held_by(std::thread::current().id()) {
1343 live.refresh();
1344 } else {
1345 live.refresh_wait();
1346 }
1347 }
1348 }
1349
1350 pub fn print(&self, renderable: Box<dyn Renderable + Send>) {
1355 if self.live.is_some() {
1356 self.handle.print(renderable);
1357 } else {
1358 self.print_disabled(|console| console.print(renderable.as_ref()));
1359 }
1360 }
1361
1362 fn print_disabled(&self, print: impl FnOnce(&Console)) {
1364 if let Some(disabled) = &self.disabled {
1365 let mut guard = disabled
1366 .lock()
1367 .unwrap_or_else(|poisoned| poisoned.into_inner());
1368 let (console, writer) = &mut *guard;
1369 let output = console.capture(print);
1370 let _ = writer.write_all(output.as_bytes());
1371 }
1372 }
1373
1374 pub fn print_str(&self, content: impl Into<String>) {
1376 if self.live.is_some() {
1377 self.handle.print_str(content);
1378 } else {
1379 let content = content.into();
1380 self.print_disabled(|console| console.print_str(&content));
1381 }
1382 }
1383
1384 #[track_caller]
1390 pub fn log(
1391 &self,
1392 objects: &[crate::console::LogObject<'_>],
1393 options: &crate::console::LogOptions<'_>,
1394 ) -> crate::errors::Result<()> {
1395 let caller = std::panic::Location::caller();
1396 self.log_at(objects, options, caller.file(), caller.line())
1397 }
1398
1399 pub fn log_at(
1402 &self,
1403 objects: &[crate::console::LogObject<'_>],
1404 options: &crate::console::LogOptions<'_>,
1405 file: &str,
1406 line: u32,
1407 ) -> crate::errors::Result<()> {
1408 if self.live.is_some() {
1409 return self.handle.log_at(objects, options, file, line);
1410 }
1411 let mut result = Ok(());
1412 self.print_disabled(|console| result = console.log_at(objects, options, file, line));
1413 result
1414 }
1415
1416 pub fn stdout(
1420 &self,
1421 ) -> crate::file_proxy::FileProxy<crate::live::LiveHandle<W>, std::io::Stdout> {
1422 self.handle
1423 .file_proxy(crate::file_proxy::ProxyStream::Stdout, std::io::stdout())
1424 }
1425
1426 pub fn stderr(
1428 &self,
1429 ) -> crate::file_proxy::FileProxy<crate::live::LiveHandle<W>, std::io::Stderr> {
1430 self.handle
1431 .file_proxy(crate::file_proxy::ProxyStream::Stderr, std::io::stderr())
1432 }
1433
1434 pub fn file_proxy<F: std::io::Write>(
1437 &self,
1438 stream: crate::file_proxy::ProxyStream,
1439 file: F,
1440 ) -> crate::file_proxy::FileProxy<crate::live::LiveHandle<W>, F> {
1441 self.handle.file_proxy(stream, file)
1442 }
1443
1444 pub fn track<I: IntoIterator>(
1448 &self,
1449 iter: I,
1450 total: Option<f64>,
1451 description: impl Into<String>,
1452 ) -> Track<'_, I::IntoIter, W> {
1453 self.track_with(iter, total, 0.0, None, description, 0.1)
1454 }
1455
1456 pub fn track_with<I: IntoIterator>(
1465 &self,
1466 iter: I,
1467 total: Option<f64>,
1468 completed: f64,
1469 task: Option<TaskId>,
1470 description: impl Into<String>,
1471 update_period: f64,
1472 ) -> Track<'_, I::IntoIter, W> {
1473 let iter = iter.into_iter();
1474 let total = total.or_else(|| exact_length(&iter));
1475 let task = match task {
1476 Some(task) => {
1477 let mut update = TaskUpdate::default().completed(completed);
1478 if let Some(total) = total {
1479 update = update.total(total);
1480 }
1481 self.update(task, update);
1482 task
1483 }
1484 None => self.add_task(description, total, completed),
1485 };
1486 Track {
1487 iter,
1488 progress: self,
1489 state: TrackState::start(self.progress.clone(), task, update_period),
1490 }
1491 }
1492
1493 pub fn wrap_read<R: std::io::Read>(
1498 &self,
1499 reader: R,
1500 total: Option<u64>,
1501 task: Option<TaskId>,
1502 description: impl Into<String>,
1503 ) -> std::io::Result<ProgressReader<'_, R, W>> {
1504 let total = total.map(|total| total as f64).or_else(|| {
1505 task.and_then(|task| self.with(|progress| progress.task(task).and_then(Task::total)))
1506 });
1507 let Some(total) = total else {
1508 return Err(std::io::Error::new(
1509 std::io::ErrorKind::InvalidInput,
1510 "unable to get the total number of bytes, please specify 'total'",
1511 ));
1512 };
1513 let task = self.task_for(task, total, description);
1514 Ok(ProgressReader {
1515 reader,
1516 progress: self,
1517 task,
1518 })
1519 }
1520
1521 pub fn open(
1524 &self,
1525 path: impl AsRef<std::path::Path>,
1526 total: Option<u64>,
1527 task: Option<TaskId>,
1528 description: impl Into<String>,
1529 ) -> std::io::Result<ProgressReader<'_, std::fs::File, W>> {
1530 let file = std::fs::File::open(path)?;
1531 let total = match total {
1532 Some(total) => total,
1533 None => file.metadata()?.len(),
1534 };
1535 let task = self.task_for(task, total as f64, description);
1536 Ok(ProgressReader {
1537 reader: file,
1538 progress: self,
1539 task,
1540 })
1541 }
1542
1543 fn task_for(&self, task: Option<TaskId>, total: f64, description: impl Into<String>) -> TaskId {
1545 match task {
1546 Some(task) => {
1547 self.update(task, TaskUpdate::default().total(total));
1548 task
1549 }
1550 None => self.add_task(description, total, 0.0),
1551 }
1552 }
1553
1554 pub fn stop(mut self) -> (Progress, W) {
1557 let writer = match self.live.take() {
1558 Some(live) => {
1559 let mut writer = live.stop();
1560 if !self.interactive {
1562 let _ = writer.write_all(b"\n");
1563 }
1564 writer
1565 }
1566 None => {
1567 self.disabled
1568 .take()
1569 .expect("a disabled display keeps its writer")
1570 .into_inner()
1571 .unwrap_or_else(|poisoned| poisoned.into_inner())
1572 .1
1573 }
1574 };
1575 let progress = match Arc::try_unwrap(std::mem::replace(
1576 &mut self.progress,
1577 Arc::new(std::sync::Mutex::new(Progress::new())),
1578 )) {
1579 Ok(mutex) => mutex
1580 .into_inner()
1581 .unwrap_or_else(|poisoned| poisoned.into_inner()),
1582 Err(_) => unreachable!("progress still shared after the live display stopped"),
1584 };
1585 (progress, writer)
1586 }
1587}
1588
1589pub struct ProgressReader<'a, R, W: std::io::Write + Send + 'static> {
1592 reader: R,
1593 progress: &'a LiveProgress<W>,
1594 task: TaskId,
1595}
1596
1597impl<R, W: std::io::Write + Send + 'static> ProgressReader<'_, R, W> {
1598 pub fn task(&self) -> TaskId {
1600 self.task
1601 }
1602
1603 pub fn into_inner(self) -> R {
1605 self.reader
1606 }
1607}
1608
1609impl<R: std::io::Read, W: std::io::Write + Send + 'static> std::io::Read
1610 for ProgressReader<'_, R, W>
1611{
1612 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1613 let count = self.reader.read(buf)?;
1614 self.progress.advance(self.task, count as f64);
1615 Ok(count)
1616 }
1617}
1618
1619impl<R: std::io::BufRead, W: std::io::Write + Send + 'static> std::io::BufRead
1620 for ProgressReader<'_, R, W>
1621{
1622 fn fill_buf(&mut self) -> std::io::Result<&[u8]> {
1623 self.reader.fill_buf()
1624 }
1625
1626 fn consume(&mut self, amount: usize) {
1627 self.reader.consume(amount);
1628 self.progress.advance(self.task, amount as f64);
1629 }
1630}
1631
1632fn exact_length<I: Iterator>(iter: &I) -> Option<f64> {
1635 match iter.size_hint() {
1636 (lower, Some(upper)) if lower == upper && lower > 0 => Some(lower as f64),
1637 _ => None,
1638 }
1639}
1640
1641struct TrackState {
1644 task: TaskId,
1645 completed: Arc<std::sync::atomic::AtomicU64>,
1647 pending: bool,
1650 stop: Option<std::sync::mpsc::Sender<()>>,
1651 thread: Option<std::thread::JoinHandle<()>>,
1652}
1653
1654impl TrackState {
1655 fn start(progress: Arc<std::sync::Mutex<Progress>>, task: TaskId, update_period: f64) -> Self {
1656 use std::sync::atomic::Ordering;
1657 let completed = Arc::new(std::sync::atomic::AtomicU64::new(0));
1658 let (stop, stopped) = std::sync::mpsc::channel::<()>();
1659 let count = completed.clone();
1660 let period = std::time::Duration::from_secs_f64(update_period.max(0.0));
1661 let thread = std::thread::spawn(move || {
1662 let mut last = 0;
1663 while let Err(std::sync::mpsc::RecvTimeoutError::Timeout) = stopped.recv_timeout(period)
1665 {
1666 let now = count.load(Ordering::SeqCst);
1667 if now != last {
1668 progress
1669 .lock()
1670 .unwrap_or_else(|poisoned| poisoned.into_inner())
1671 .advance(task, (now - last) as f64);
1672 last = now;
1673 }
1674 }
1675 });
1676 TrackState {
1677 task,
1678 completed,
1679 pending: false,
1680 stop: Some(stop),
1681 thread: Some(thread),
1682 }
1683 }
1684
1685 fn next_requested(&mut self) {
1687 if std::mem::take(&mut self.pending) {
1688 self.completed
1689 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1690 }
1691 }
1692
1693 fn finish<W: std::io::Write + Send + 'static>(&mut self, live: &LiveProgress<W>) {
1696 let Some(thread) = self.thread.take() else {
1697 return;
1698 };
1699 self.next_requested();
1700 drop(self.stop.take());
1701 let _ = thread.join();
1702 let completed = self.completed.load(std::sync::atomic::Ordering::SeqCst);
1703 live.update(
1704 self.task,
1705 TaskUpdate::default()
1706 .completed(completed as f64)
1707 .refresh(true),
1708 );
1709 }
1710}
1711
1712pub struct Track<'a, I: Iterator, W: std::io::Write + Send + 'static> {
1714 iter: I,
1715 progress: &'a LiveProgress<W>,
1716 state: TrackState,
1717}
1718
1719impl<I: Iterator, W: std::io::Write + Send + 'static> Track<'_, I, W> {
1720 pub fn task(&self) -> TaskId {
1722 self.state.task
1723 }
1724}
1725
1726impl<I: Iterator, W: std::io::Write + Send + 'static> Iterator for Track<'_, I, W> {
1727 type Item = I::Item;
1728
1729 fn next(&mut self) -> Option<I::Item> {
1730 self.state.next_requested();
1731 let item = self.iter.next();
1732 if item.is_some() {
1733 self.state.pending = true;
1734 } else {
1735 self.state.finish(self.progress);
1736 }
1737 item
1738 }
1739
1740 fn size_hint(&self) -> (usize, Option<usize>) {
1741 self.iter.size_hint()
1742 }
1743}
1744
1745impl<I: Iterator, W: std::io::Write + Send + 'static> Drop for Track<'_, I, W> {
1746 fn drop(&mut self) {
1749 self.state.pending = false;
1751 self.state.finish(self.progress);
1752 }
1753}
1754
1755pub fn track<I: IntoIterator>(iter: I, description: &str) -> TrackStdout<I::IntoIter> {
1760 let mut columns = Vec::new();
1761 if !description.is_empty() {
1762 columns.push(ProgressColumn::Description);
1763 }
1764 columns.extend([
1765 ProgressColumn::Bar,
1766 ProgressColumn::TaskProgress { show_speed: true },
1767 ProgressColumn::TimeRemaining(TimeRemainingColumn::new(false, true)),
1768 ]);
1769 let iter = iter.into_iter();
1770 let total = exact_length(&iter);
1771 let live = Progress::new()
1772 .columns(columns)
1773 .start(Console::new(), std::io::stdout(), 10.0);
1774 let task = live.add_task(description, total, 0.0);
1775 TrackStdout {
1776 iter,
1777 state: TrackState::start(live.progress.clone(), task, 0.1),
1778 live: Some(live),
1779 }
1780}
1781
1782pub struct TrackStdout<I: Iterator> {
1784 iter: I,
1785 live: Option<LiveProgress<std::io::Stdout>>,
1786 state: TrackState,
1787}
1788
1789impl<I: Iterator> Iterator for TrackStdout<I> {
1790 type Item = I::Item;
1791
1792 fn next(&mut self) -> Option<I::Item> {
1793 self.live.as_ref()?;
1794 self.state.next_requested();
1795 match self.iter.next() {
1796 Some(item) => {
1797 self.state.pending = true;
1798 Some(item)
1799 }
1800 None => {
1801 if let Some(live) = self.live.take() {
1802 self.state.finish(&live);
1803 live.stop();
1804 }
1805 None
1806 }
1807 }
1808 }
1809}
1810
1811impl<I: Iterator> Drop for TrackStdout<I> {
1812 fn drop(&mut self) {
1813 if let Some(live) = self.live.take() {
1814 self.state.pending = false;
1815 self.state.finish(&live);
1816 live.stop();
1817 }
1818 }
1819}
1820
1821fn whole_number(value: f64) -> FormatValue {
1823 if value.fract() == 0.0 && value.abs() < 9_007_199_254_740_992.0 {
1824 FormatValue::Int(value as i64)
1825 } else {
1826 FormatValue::Float(value)
1827 }
1828}
1829
1830#[cfg(test)]
1831mod tests {
1832 use super::*;
1833 use crate::color::ColorSystem;
1834
1835 fn render(progress: &Progress) -> String {
1836 Console::builder()
1837 .force_terminal(true)
1838 .color_system(Some(ColorSystem::Truecolor))
1839 .width(50)
1840 .no_color(false)
1841 .build()
1842 .render_to_string(progress)
1843 }
1844
1845 #[test]
1846 fn three_tasks_match_upstream() {
1847 let mut progress = Progress::new().columns(vec![
1849 ProgressColumn::Description,
1850 ProgressColumn::Bar,
1851 ProgressColumn::Percentage,
1852 ]);
1853 progress.add_task("Downloading", 100.0, 50.0);
1854 progress.add_task("Processing", 100.0, 100.0);
1855 progress.add_task("Waiting", 100.0, 0.0);
1856 let expected = concat!(
1857 "Downloading \x1b[38;2;249;38;114m━━━━━━━━━━━━━━━━\x1b[0m",
1858 "\x1b[38;2;249;38;114m╸\x1b[0m\x1b[38;5;237m━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 50%\x1b[0m\n",
1859 "Processing \x1b[38;2;114;156;31m",
1860 "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m100%\x1b[0m\n",
1861 "Waiting \x1b[38;5;237m",
1862 "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 0%\x1b[0m",
1863 );
1864 assert_eq!(render(&progress), expected);
1865 }
1866
1867 #[test]
1868 fn download_text_matches_upstream() {
1869 let dl = |completed: f64, total: f64| {
1871 let mut progress = Progress::new();
1872 let id = progress.add_task("", total, completed);
1873 progress.task(id).unwrap().download_text(false)
1874 };
1875 assert_eq!(dl(500.0, 1000.0), "0.5/1.0 kB");
1876 assert_eq!(dl(500.0, 999.0), "500/999 bytes");
1877 assert_eq!(dl(1_500_000.0, 3_000_000.0), "1.5/3.0 MB");
1878 assert_eq!(dl(0.0, 1024.0), "0.0/1.0 kB");
1879 assert_eq!(dl(2_500_000_000.0, 10_000_000_000.0), "2.5/10.0 GB");
1880 assert_eq!(dl(250.0, 250.0), "250/250 bytes");
1881 }
1882
1883 #[test]
1884 fn download_column_in_grid_matches_upstream() {
1885 let mut progress = Progress::new().columns(vec![
1887 ProgressColumn::Description,
1888 ProgressColumn::Bar,
1889 ProgressColumn::Download,
1890 ]);
1891 progress.add_task("File", 1000.0, 500.0);
1892 let expected = concat!(
1893 "File \x1b[38;2;249;38;114m━━━━━━━━━━━━━━━━━\x1b[0m\x1b[38;5;237m╺\x1b[0m",
1894 "\x1b[38;5;237m━━━━━━━━━━━━━━━━\x1b[0m \x1b[32m0.5/1.0 kB\x1b[0m",
1895 );
1896 assert_eq!(render(&progress), expected);
1897 }
1898
1899 #[test]
1900 fn custom_columns_with_mofn_match_upstream() {
1901 let mut progress = Progress::new().columns(vec![
1904 ProgressColumn::Description,
1905 ProgressColumn::Bar,
1906 ProgressColumn::MofN,
1907 ]);
1908 progress.add_task("A", 5.0, 3.0);
1909 progress.add_task("B", 100.0, 50.0);
1910 let console = Console::builder()
1911 .force_terminal(true)
1912 .color_system(Some(ColorSystem::Truecolor))
1913 .width(40)
1914 .no_color(false)
1915 .build();
1916 let expected = concat!(
1917 "A \x1b[38;2;249;38;114m━━━━━━━━━━━━━━━━━━\x1b[0m\x1b[38;5;237m╺\x1b[0m",
1918 "\x1b[38;5;237m━━━━━━━━━━━\x1b[0m \x1b[32m3/5 \x1b[0m\n",
1919 "B \x1b[38;2;249;38;114m━━━━━━━━━━━━━━━\x1b[0m\x1b[38;5;237m╺\x1b[0m",
1920 "\x1b[38;5;237m━━━━━━━━━━━━━━\x1b[0m \x1b[32m 50/100\x1b[0m",
1921 );
1922 assert_eq!(console.render_to_string(&progress), expected);
1923 }
1924
1925 fn live(columns: Vec<ProgressColumn>) -> LiveProgress<Vec<u8>> {
1926 let console = Console::builder()
1927 .force_terminal(true)
1928 .color_system(Some(ColorSystem::Truecolor))
1929 .width(40)
1930 .build();
1931 Progress::new()
1932 .columns(columns)
1933 .clock(|| 0.0)
1934 .start(console, Vec::new(), 1e-9)
1935 }
1936
1937 fn within_deadline<R: Send + 'static>(f: impl FnOnce() -> R + Send + 'static) -> R {
1940 let (done, wait) = std::sync::mpsc::channel();
1941 std::thread::spawn(move || {
1942 let _ = done.send(std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)));
1943 });
1944 match wait.recv_timeout(std::time::Duration::from_secs(10)) {
1945 Ok(Ok(value)) => value,
1946 Ok(Err(payload)) => std::panic::resume_unwind(payload),
1947 Err(_) => panic!("deadlocked: did not finish within 10s"),
1948 }
1949 }
1950
1951 #[test]
1952 fn refresh_inside_with_does_not_deadlock() {
1953 let output = within_deadline(|| {
1956 let live = live(vec![ProgressColumn::Description, ProgressColumn::MofN]);
1957 live.with(|progress| {
1958 let task = progress.add_task("inside", Some(2.0), 1.0);
1959 live.refresh();
1960 task
1961 });
1962 live.refresh();
1963 String::from_utf8(live.stop().1).unwrap()
1964 });
1965 assert!(
1966 output.ends_with("inside \x1b[32m1/2\x1b[0m\n\x1b[?25h"),
1967 "{output:?}"
1968 );
1969 }
1970
1971 #[test]
1972 fn nested_with_panics_instead_of_deadlocking() {
1973 let result = within_deadline(|| {
1974 let live = live(vec![ProgressColumn::Description]);
1975 let nested = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1976 live.with(|_| live.add_task("nested", None, 0.0))
1977 }));
1978 let task = live.add_task("after", None, 0.0);
1980 live.stop();
1981 (nested.is_err(), task)
1982 });
1983 assert!(result.0, "a nested `with` must be refused, not deadlock");
1984 }
1985
1986 #[test]
1987 fn the_track_thread_advances_by_the_items_counted() {
1988 let live = live(vec![ProgressColumn::Description, ProgressColumn::MofN]);
1989 let tracked = live.track_with(0..4, None, 0.0, None, "n", 0.001);
1990 let task = tracked.task();
1991 let mut seen = 0.0;
1992 for item in tracked {
1993 if item == 2 {
1994 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
1996 while seen < 2.0 && std::time::Instant::now() < deadline {
1997 std::thread::sleep(std::time::Duration::from_millis(1));
1998 seen = live.with(|progress| progress.task(task).unwrap().completed());
1999 }
2000 }
2001 }
2002 assert_eq!(seen, 2.0);
2003 let (progress, _) = live.stop();
2004 assert_eq!(progress.task(task).unwrap().completed(), 4.0);
2005 }
2006
2007 #[test]
2008 fn breaking_out_of_track_counts_only_finished_items() {
2009 let live = live(vec![ProgressColumn::Description, ProgressColumn::MofN]);
2010 let tracked = live.track_with(0..10, None, 0.0, None, "n", 1e9);
2011 let task = tracked.task();
2012 for item in tracked {
2013 if item == 3 {
2014 break;
2015 }
2016 }
2017 let (progress, _) = live.stop();
2018 assert_eq!(progress.task(task).unwrap().completed(), 3.0);
2019 }
2020
2021 #[test]
2022 fn track_counts_each_item_after_its_loop_body() {
2023 let live = live(vec![ProgressColumn::Description, ProgressColumn::MofN]);
2024 let mut seen = Vec::new();
2025 let tracked = live.track_with(vec!['a', 'b', 'c'], None, 0.0, None, "letters", 1e9);
2028 let task = tracked.task();
2029 for item in tracked {
2030 let completed = live.with(|progress| progress.task(task).unwrap().completed());
2031 seen.push((item, completed));
2032 }
2033 assert_eq!(seen, vec![('a', 0.0), ('b', 0.0), ('c', 0.0)]);
2034 let (progress, bytes) = live.stop();
2035 let task = progress.task(task).unwrap();
2036 assert_eq!((task.total(), task.completed()), (Some(3.0), 3.0));
2037 let output = String::from_utf8(bytes).unwrap();
2038 assert!(
2039 output.ends_with("letters \x1b[32m3/3\x1b[0m\n\x1b[?25h"),
2040 "{output:?}"
2041 );
2042 }
2043
2044 #[test]
2045 fn track_leaves_an_iterator_of_unknown_length_indeterminate() {
2046 let live = live(vec![ProgressColumn::MofN]);
2047 let task = {
2048 let mut tracked = live.track((0..10).filter(|n| n % 3 == 0), None, "");
2049 let task = tracked.task();
2050 assert_eq!(tracked.by_ref().count(), 4);
2051 task
2052 };
2053 let with_total = {
2054 let mut tracked = live.track(0..2, Some(5.0), "");
2055 tracked.by_ref().for_each(drop);
2056 tracked.task()
2057 };
2058 let (progress, _) = live.stop();
2059 assert_eq!(progress.task(task).unwrap().total(), None);
2060 assert_eq!(progress.task(task).unwrap().completed(), 4.0);
2061 assert_eq!(progress.task(with_total).unwrap().total(), Some(5.0));
2062 }
2063
2064 fn quiet_console() -> Console {
2065 Console::builder().force_terminal(false).width(40).build()
2066 }
2067
2068 #[test]
2069 fn wrap_read_advances_by_the_bytes_read() {
2070 use std::io::Read;
2071 let live = Progress::new()
2072 .disable(true)
2073 .start(quiet_console(), Vec::new(), 1.0);
2074 let mut reader = live
2075 .wrap_read(&b"hello world"[..], Some(11), None, "Reading...")
2076 .expect("total given");
2077 let task = reader.task();
2078 let mut buf = [0u8; 4];
2079 reader.read_exact(&mut buf).unwrap();
2080 assert_eq!(live.with(|p| p.task(task).unwrap().completed()), 4.0);
2081 let mut rest = Vec::new();
2082 reader.read_to_end(&mut rest).unwrap();
2083 assert!(live.with(|p| p.task(task).unwrap().finished()));
2084 let (_, out) = live.stop();
2085 assert!(out.is_empty(), "a disabled display writes nothing");
2086 }
2087
2088 #[test]
2089 fn wrap_read_needs_a_total() {
2090 let live = Progress::new()
2091 .disable(true)
2092 .start(quiet_console(), Vec::new(), 1.0);
2093 let err = live
2094 .wrap_read(&b""[..], None, None, "x")
2095 .err()
2096 .expect("no total");
2097 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
2098 let task = live.add_task("sized", 5.0, 0.0);
2099 assert!(live.wrap_read(&b""[..], None, Some(task), "x").is_ok());
2100 }
2101
2102 #[test]
2103 fn open_takes_the_file_size_as_total() {
2104 use std::io::Read;
2105 let path = std::env::temp_dir().join(format!("rs-rich-open-{}", std::process::id()));
2106 std::fs::write(&path, b"0123456789").unwrap();
2107 let live = Progress::new()
2108 .disable(true)
2109 .start(quiet_console(), Vec::new(), 1.0);
2110 let mut reader = live.open(&path, None, None, "Reading...").unwrap();
2111 let task = reader.task();
2112 assert_eq!(live.with(|p| p.task(task).unwrap().total()), Some(10.0));
2113 std::io::copy(&mut reader, &mut std::io::sink()).unwrap();
2114 assert_eq!(live.with(|p| p.task(task).unwrap().completed()), 10.0);
2115 let _ = reader.read(&mut [0u8; 1]);
2116 drop(reader);
2117 std::fs::remove_file(path).unwrap();
2118 }
2119}