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 if task.completed == 0.0 {
249 if let Some((timestamp, cell)) = self.cache.borrow().get(&task.id) {
250 if timestamp + max_refresh > now {
251 return cell.clone();
252 }
253 }
254 }
255 }
256 let cell = self.column.render(task);
257 self.cache.borrow_mut().insert(task.id, (now, cell.clone()));
258 cell
259 }
260}
261
262#[derive(Clone, Debug)]
264pub struct BarColumn {
265 bar_width: Option<usize>,
266 style: StyleType,
267 complete_style: StyleType,
268 finished_style: StyleType,
269 pulse_style: StyleType,
270}
271
272impl Default for BarColumn {
273 fn default() -> Self {
274 BarColumn {
275 bar_width: Some(40),
276 style: "bar.back".into(),
277 complete_style: "bar.complete".into(),
278 finished_style: "bar.finished".into(),
279 pulse_style: "bar.pulse".into(),
280 }
281 }
282}
283
284impl BarColumn {
285 pub fn new() -> Self {
287 BarColumn::default()
288 }
289
290 pub fn bar_width(mut self, width: Option<usize>) -> Self {
292 self.bar_width = width;
293 self
294 }
295
296 pub fn style(mut self, style: impl Into<StyleType>) -> Self {
298 self.style = style.into();
299 self
300 }
301
302 pub fn complete_style(mut self, style: impl Into<StyleType>) -> Self {
304 self.complete_style = style.into();
305 self
306 }
307
308 pub fn finished_style(mut self, style: impl Into<StyleType>) -> Self {
310 self.finished_style = style.into();
311 self
312 }
313
314 pub fn pulse_style(mut self, style: impl Into<StyleType>) -> Self {
316 self.pulse_style = style.into();
317 self
318 }
319
320 fn render(&self, task: &Task) -> ProgressBar {
322 let bar = match task.total {
323 Some(total) => ProgressBar::new(total.max(0.0), task.completed.max(0.0)),
324 None => ProgressBar::indeterminate(),
325 };
326 let bar = match self.bar_width {
327 Some(width) => bar.width(width.max(1)),
328 None => bar,
329 };
330 bar.pulse(!task.started())
331 .animation_time(task.now())
332 .style(self.style.clone())
333 .complete_style(self.complete_style.clone())
334 .finished_style(self.finished_style.clone())
335 .pulse_style(self.pulse_style.clone())
336 }
337}
338
339impl ProgressColumn {
340 pub fn time_remaining() -> Self {
342 ProgressColumn::TimeRemaining(TimeRemainingColumn::new(false, false))
343 }
344
345 pub fn spinner() -> Self {
347 ProgressColumn::Spinner(SpinnerColumn::new("dots", " "))
348 }
349
350 pub fn custom(column: impl CustomProgressColumn + 'static) -> Self {
352 ProgressColumn::Custom(CustomColumn::new(Arc::new(column)))
353 }
354
355 pub fn with_table_column(self, options: ColumnOptions) -> Self {
359 let inner = match self {
360 ProgressColumn::WithTableColumn(inner, _) => *inner,
361 column => column,
362 };
363 ProgressColumn::WithTableColumn(Box::new(inner), options)
364 }
365
366 pub fn table_column(&self) -> ColumnOptions {
369 match self {
370 ProgressColumn::WithTableColumn(_, options) => options.clone(),
371 ProgressColumn::Custom(column) => column.column.table_column(),
372 ProgressColumn::Description
373 | ProgressColumn::Text(..)
374 | ProgressColumn::TextFormat(_)
375 | ProgressColumn::Percentage
376 | ProgressColumn::TaskProgress { .. } => ColumnOptions {
377 no_wrap: true,
378 ..ColumnOptions::default()
379 },
380 _ => ColumnOptions::default(),
381 }
382 }
383
384 pub fn table_cell(&self, task: &Task) -> Cell {
388 match self {
389 ProgressColumn::WithTableColumn(inner, _) => inner.table_cell(task),
390 ProgressColumn::Custom(column) => column.call(task),
391 ProgressColumn::Bar => Cell::Renderable(Arc::new(BarColumn::default().render(task))),
392 ProgressColumn::BarWith(column) => Cell::Renderable(Arc::new(column.render(task))),
393 ProgressColumn::Renderable(renderable) => Cell::Renderable(renderable.clone()),
394 column => Cell::Text(column.cell(task)),
395 }
396 }
397
398 fn cell(&self, task: &Task) -> Text {
400 let named = |plain: String, style: &str| Text::styled(plain, style);
401 match self {
402 ProgressColumn::Description => {
405 let markup = format!("[progress.description]{}", task.description);
406 Text::from_markup(&markup)
407 .unwrap_or_else(|_| Text::new(task.description.clone()))
408 .justify(Justify::Left)
409 }
410 ProgressColumn::Text(text, style) => {
411 Text::styled(text.clone(), style.clone()).justify(Justify::Left)
412 }
413 ProgressColumn::TextFormat(column) => column.render(task),
414 ProgressColumn::Bar
415 | ProgressColumn::BarWith(_)
416 | ProgressColumn::Renderable(_)
417 | ProgressColumn::Custom(_)
418 | ProgressColumn::WithTableColumn(..) => {
419 unreachable!("bar, renderable and wrapped columns have no text cell")
420 }
421 ProgressColumn::Percentage => task.percentage_cell().justify(Justify::Left),
422 ProgressColumn::TaskProgress { show_speed } => {
423 if task.total.is_none() && *show_speed {
424 render_speed(
425 task.finished_speed
426 .filter(|s| *s != 0.0)
427 .or_else(|| task.speed()),
428 )
429 } else {
430 task.percentage_cell().justify(Justify::Left)
431 }
432 }
433 ProgressColumn::MofN => named(task.mofn_text(), "progress.download"),
434 ProgressColumn::Download => named(task.download_text(false), "progress.download"),
435 ProgressColumn::BinaryDownload => named(task.download_text(true), "progress.download"),
436 ProgressColumn::TimeElapsed => {
437 let elapsed = if task.finished() {
438 task.finished_time
439 } else {
440 task.elapsed()
441 };
442 let text = match elapsed {
443 None => "-:--:--".to_string(),
444 Some(elapsed) => timedelta(elapsed.max(0.0) as i64),
445 };
446 named(text, "progress.elapsed")
447 }
448 ProgressColumn::TimeRemaining(column) => column.render(task),
449 ProgressColumn::TransferSpeed => {
450 let speed = task
451 .finished_speed
452 .filter(|s| *s != 0.0)
453 .or_else(|| task.speed());
454 let text = match speed {
455 None => "?".to_string(),
456 Some(speed) => format!("{}/s", filesize::decimal_signed(speed as i64)),
457 };
458 named(text, "progress.data.speed")
459 }
460 ProgressColumn::FileSize => named(
463 filesize::decimal_signed(task.completed as i64),
464 "progress.filesize",
465 ),
466 ProgressColumn::TotalFileSize => named(
467 task.total
468 .map_or_else(String::new, |total| filesize::decimal_signed(total as i64)),
469 "progress.filesize.total",
470 ),
471 ProgressColumn::Spinner(column) => {
472 if task.finished() {
473 Text::from_markup(&column.finished_text)
474 .unwrap_or_else(|_| Text::new(column.finished_text.clone()))
475 } else {
476 let mut frame = column.spinner.render(task.now());
479 frame.set_base_style(column.style.clone());
480 frame
481 }
482 }
483 }
484 }
485}
486
487impl TimeRemainingColumn {
488 fn render(&self, task: &Task) -> Text {
489 let now = task.now();
492 if task.completed == 0.0 {
493 if let Some((timestamp, text)) = self.cache.borrow().get(&task.id) {
494 if timestamp + 0.5 > now {
495 return text.clone();
496 }
497 }
498 }
499 let (task_time, style) = if self.elapsed_when_finished && task.finished() {
500 (task.finished_time, "progress.elapsed")
501 } else {
502 (task.time_remaining(), "progress.remaining")
503 };
504 let text = if task.total.is_none() {
505 Text::styled("", style)
506 } else {
507 match task_time {
508 None => Text::styled(if self.compact { "--:--" } else { "-:--:--" }, style),
509 Some(task_time) => {
510 let whole = task_time as i64;
511 let (minutes, seconds) = (whole.div_euclid(60), whole.rem_euclid(60));
512 let (hours, minutes) = (minutes.div_euclid(60), minutes.rem_euclid(60));
513 let formatted = if self.compact && hours == 0 {
514 format!("{minutes:02}:{seconds:02}")
515 } else {
516 format!("{hours}:{minutes:02}:{seconds:02}")
517 };
518 Text::styled(formatted, style)
519 }
520 }
521 };
522 self.cache.borrow_mut().insert(task.id, (now, text.clone()));
523 text
524 }
525}
526
527fn render_speed(speed: Option<f64>) -> Text {
530 let Some(speed) = speed else {
531 return Text::styled("", "progress.percentage");
532 };
533 let (unit, suffix) = filesize::pick_unit_and_suffix_signed(
534 speed as i64,
535 &["", "×10³", "×10⁶", "×10⁹", "×10¹²"],
536 1000,
537 );
538 let data_speed = speed / unit as f64;
539 Text::styled(
540 format!("{data_speed:.1}{suffix} it/s"),
541 "progress.percentage",
542 )
543}
544
545fn timedelta(total_seconds: i64) -> String {
548 let days = total_seconds / 86_400;
549 let rest = total_seconds % 86_400;
550 let clock = format!("{}:{:02}:{:02}", rest / 3600, rest % 3600 / 60, rest % 60);
551 match days {
552 0 => clock,
553 1 => format!("1 day, {clock}"),
554 days => format!("{days} days, {clock}"),
555 }
556}
557
558fn grouped(value: f64, precision: usize) -> String {
560 let formatted = format!("{value:.precision$}");
561 let (sign, digits) = match formatted.strip_prefix('-') {
562 Some(rest) => ("-", rest),
563 None => ("", formatted.as_str()),
564 };
565 let (integer, fraction) = match digits.split_once('.') {
566 Some((integer, fraction)) => (integer, Some(fraction)),
567 None => (digits, None),
568 };
569 let mut grouped = String::new();
570 for (index, digit) in integer.chars().enumerate() {
571 if index > 0 && (integer.len() - index) % 3 == 0 {
572 grouped.push(',');
573 }
574 grouped.push(digit);
575 }
576 match fraction {
577 Some(fraction) => format!("{sign}{grouped}.{fraction}"),
578 None => format!("{sign}{grouped}"),
579 }
580}
581
582pub struct Task {
585 id: TaskId,
586 description: String,
587 total: Option<f64>,
588 completed: f64,
589 visible: bool,
590 start_time: Option<f64>,
591 stop_time: Option<f64>,
592 finished_time: Option<f64>,
593 finished_speed: Option<f64>,
594 samples: VecDeque<(f64, f64)>,
596 fields: BTreeMap<String, FormatValue>,
598 get_time: GetTime,
599}
600
601impl Task {
602 pub fn fields(&self) -> &BTreeMap<String, FormatValue> {
604 &self.fields
605 }
606
607 fn format_field(&self, name: &str) -> Option<FormatValue> {
611 let attribute = name.strip_prefix("task.")?;
612 if let Some(key) = attribute
613 .strip_prefix("fields[")
614 .and_then(|rest| rest.strip_suffix(']'))
615 {
616 return self.fields.get(key).cloned();
617 }
618 Some(match attribute {
619 "id" => FormatValue::Int(self.id.0 as i64),
620 "description" => FormatValue::Str(self.description.clone()),
621 "total" => self.total.map_or(FormatValue::None, whole_number),
624 "completed" => whole_number(self.completed),
625 "visible" => FormatValue::Bool(self.visible),
626 "started" => FormatValue::Bool(self.started()),
627 "finished" => FormatValue::Bool(self.finished()),
628 "percentage" => FormatValue::Float(self.percentage()),
629 "remaining" => self.remaining().into(),
630 "elapsed" => self.elapsed().into(),
631 "speed" => self.speed().into(),
632 "time_remaining" => self.time_remaining().into(),
633 "start_time" => self.start_time.into(),
634 "stop_time" => self.stop_time.into(),
635 "finished_time" => self.finished_time.into(),
636 "finished_speed" => self.finished_speed.into(),
637 _ => return None,
638 })
639 }
640 fn now(&self) -> f64 {
641 (self.get_time)()
642 }
643
644 pub fn get_time(&self) -> f64 {
646 self.now()
647 }
648
649 pub fn id(&self) -> TaskId {
651 self.id
652 }
653
654 pub fn description(&self) -> &str {
656 &self.description
657 }
658
659 pub fn total(&self) -> Option<f64> {
661 self.total
662 }
663
664 pub fn completed(&self) -> f64 {
666 self.completed
667 }
668
669 pub fn visible(&self) -> bool {
671 self.visible
672 }
673
674 pub fn started(&self) -> bool {
676 self.start_time.is_some()
677 }
678
679 pub fn remaining(&self) -> Option<f64> {
681 self.total.map(|total| total - self.completed)
682 }
683
684 pub fn elapsed(&self) -> Option<f64> {
686 let start = self.start_time?;
687 Some(self.stop_time.unwrap_or_else(|| self.now()) - start)
688 }
689
690 pub fn finished(&self) -> bool {
692 self.finished_time.is_some()
693 }
694
695 pub fn finished_time(&self) -> Option<f64> {
697 self.finished_time
698 }
699
700 pub fn percentage(&self) -> f64 {
702 match self.total {
703 Some(total) if total != 0.0 => (self.completed / total * 100.0).clamp(0.0, 100.0),
704 _ => 0.0,
705 }
706 }
707
708 pub fn speed(&self) -> Option<f64> {
710 self.start_time?;
711 let (first, _) = *self.samples.front()?;
712 let (last, _) = *self.samples.back()?;
713 let total_time = last - first;
714 if total_time == 0.0 {
715 return None;
716 }
717 let total_completed: f64 = self.samples.iter().skip(1).map(|(_, done)| done).sum();
718 Some(total_completed / total_time)
719 }
720
721 pub fn time_remaining(&self) -> Option<f64> {
723 if self.finished() {
724 return Some(0.0);
725 }
726 let speed = self.speed().filter(|speed| *speed != 0.0)?;
727 let remaining = self.remaining()?;
728 Some((remaining / speed).ceil())
729 }
730
731 fn clear_progress(&mut self) {
733 self.samples.clear();
734 self.finished_time = None;
735 self.finished_speed = None;
736 }
737
738 fn percentage_cell(&self) -> Text {
741 if self.total.is_none() {
742 return Text::new("");
743 }
744 let mut text = Text::new(format!("{:>3.0}%", self.percentage()));
745 let len = text.plain().len();
746 text.stylize("progress.percentage", 0, len);
747 text
748 }
749
750 fn mofn_text(&self) -> String {
753 let completed = self.completed as i64;
754 let total = self
755 .total
756 .map_or_else(|| "?".to_string(), |total| (total as i64).to_string());
757 let total_width = total.chars().count();
758 format!("{completed:>total_width$}/{total}")
759 }
760
761 fn download_text(&self, binary: bool) -> String {
764 const DECIMAL: &[&str] = &["bytes", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
765 const BINARY: &[&str] = &[
766 "bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB",
767 ];
768 let completed = self.completed as i64;
770 let base_size = self.total.map_or(completed, |total| total as i64);
771 let (unit, suffix) = if binary {
772 filesize::pick_unit_and_suffix_signed(base_size, BINARY, 1024)
773 } else {
774 filesize::pick_unit_and_suffix_signed(base_size, DECIMAL, 1000)
775 };
776 let precision = if unit == 1 { 0 } else { 1 };
777 let completed_str = grouped(completed as f64 / unit as f64, precision);
778 let total_str = self.total.map_or_else(
779 || "?".to_string(),
780 |total| grouped((total as i64) as f64 / unit as f64, precision),
781 );
782 format!("{completed_str}/{total_str} {suffix}")
783 }
784}
785
786#[derive(Debug, Clone, Default)]
789pub struct TaskUpdate {
790 pub total: Option<f64>,
791 pub completed: Option<f64>,
792 pub advance: Option<f64>,
793 pub description: Option<String>,
794 pub visible: Option<bool>,
795 pub fields: Vec<(String, FormatValue)>,
797 pub refresh: bool,
799}
800
801impl TaskUpdate {
802 pub fn total(mut self, total: f64) -> Self {
803 self.total = Some(total);
804 self
805 }
806
807 pub fn completed(mut self, completed: f64) -> Self {
808 self.completed = Some(completed);
809 self
810 }
811
812 pub fn advance(mut self, advance: f64) -> Self {
813 self.advance = Some(advance);
814 self
815 }
816
817 pub fn description(mut self, description: impl Into<String>) -> Self {
818 self.description = Some(description.into());
819 self
820 }
821
822 pub fn visible(mut self, visible: bool) -> Self {
823 self.visible = Some(visible);
824 self
825 }
826
827 pub fn refresh(mut self, refresh: bool) -> Self {
830 self.refresh = refresh;
831 self
832 }
833
834 pub fn field(mut self, name: impl Into<String>, value: impl Into<FormatValue>) -> Self {
836 self.fields.push((name.into(), value.into()));
837 self
838 }
839}
840
841pub struct Progress {
843 tasks: Vec<Task>,
844 next_id: usize,
845 columns: Vec<ProgressColumn>,
846 get_time: GetTime,
847 speed_estimate_period: f64,
848 expand: bool,
849 transient: bool,
850 disable: bool,
851 redirect_stdout: bool,
852 redirect_stderr: bool,
853}
854
855impl Default for Progress {
856 fn default() -> Self {
857 Progress {
858 tasks: Vec::new(),
859 next_id: 0,
860 columns: Progress::default_columns(),
861 get_time: Arc::new(monotonic),
862 speed_estimate_period: 30.0,
863 expand: false,
864 transient: false,
865 disable: false,
866 redirect_stdout: true,
867 redirect_stderr: true,
868 }
869 }
870}
871
872impl Progress {
873 pub fn new() -> Self {
874 Progress::default()
875 }
876
877 pub fn default_columns() -> Vec<ProgressColumn> {
880 vec![
881 ProgressColumn::Description,
882 ProgressColumn::Bar,
883 ProgressColumn::Percentage,
884 ProgressColumn::time_remaining(),
885 ]
886 }
887
888 pub fn columns(mut self, columns: Vec<ProgressColumn>) -> Self {
890 self.columns = columns;
891 self
892 }
893
894 pub fn clock(mut self, clock: impl Fn() -> f64 + Send + Sync + 'static) -> Self {
897 self.get_time = Arc::new(clock);
898 for task in &mut self.tasks {
899 task.get_time = self.get_time.clone();
900 }
901 self
902 }
903
904 pub fn expand(mut self, expand: bool) -> Self {
906 self.expand = expand;
907 self
908 }
909
910 pub fn transient(mut self, transient: bool) -> Self {
912 self.transient = transient;
913 self
914 }
915
916 pub fn redirect_stdout(mut self, redirect: bool) -> Self {
919 self.redirect_stdout = redirect;
920 self
921 }
922
923 pub fn redirect_stderr(mut self, redirect: bool) -> Self {
925 self.redirect_stderr = redirect;
926 self
927 }
928
929 pub fn disable(mut self, disable: bool) -> Self {
932 self.disable = disable;
933 self
934 }
935
936 pub fn speed_estimate_period(mut self, seconds: f64) -> Self {
938 self.speed_estimate_period = seconds;
939 self
940 }
941
942 fn now(&self) -> f64 {
943 (self.get_time)()
944 }
945
946 fn task_mut(&mut self, id: TaskId) -> Option<&mut Task> {
947 self.tasks.iter_mut().find(|task| task.id == id)
948 }
949
950 pub fn add_task(
953 &mut self,
954 description: impl Into<String>,
955 total: impl Into<Option<f64>>,
956 completed: f64,
957 ) -> TaskId {
958 let id = self.push_task(description.into(), total.into(), completed);
959 self.start_task(id);
960 id
961 }
962
963 pub fn add_unstarted_task(
966 &mut self,
967 description: impl Into<String>,
968 total: impl Into<Option<f64>>,
969 completed: f64,
970 ) -> TaskId {
971 self.push_task(description.into(), total.into(), completed)
972 }
973
974 pub fn add_task_with<K: Into<String>, V: Into<FormatValue>>(
977 &mut self,
978 description: impl Into<String>,
979 total: impl Into<Option<f64>>,
980 completed: f64,
981 start: bool,
982 fields: impl IntoIterator<Item = (K, V)>,
983 ) -> TaskId {
984 let id = self.push_task(description.into(), total.into(), completed);
985 if let Some(task) = self.task_mut(id) {
986 task.fields = fields
987 .into_iter()
988 .map(|(name, value)| (name.into(), value.into()))
989 .collect();
990 }
991 if start {
992 self.start_task(id);
993 }
994 id
995 }
996
997 fn push_task(&mut self, description: String, total: Option<f64>, completed: f64) -> TaskId {
998 let id = TaskId(self.next_id);
999 self.next_id += 1;
1000 self.tasks.push(Task {
1001 id,
1002 description,
1003 total,
1004 completed,
1005 visible: true,
1006 start_time: None,
1007 stop_time: None,
1008 finished_time: None,
1009 finished_speed: None,
1010 samples: VecDeque::new(),
1011 fields: BTreeMap::new(),
1012 get_time: self.get_time.clone(),
1013 });
1014 id
1015 }
1016
1017 pub fn task(&self, id: TaskId) -> Option<&Task> {
1019 self.tasks.iter().find(|task| task.id == id)
1020 }
1021
1022 pub fn tasks(&self) -> &[Task] {
1024 &self.tasks
1025 }
1026
1027 pub fn finished(&self) -> bool {
1029 self.tasks.iter().all(Task::finished)
1030 }
1031
1032 pub fn start_task(&mut self, id: TaskId) {
1034 let now = self.now();
1035 if let Some(task) = self.task_mut(id) {
1036 task.start_time.get_or_insert(now);
1037 }
1038 }
1039
1040 pub fn stop_task(&mut self, id: TaskId) {
1042 let now = self.now();
1043 if let Some(task) = self.task_mut(id) {
1044 task.start_time.get_or_insert(now);
1045 task.stop_time = Some(now);
1046 }
1047 }
1048
1049 pub fn update(&mut self, id: TaskId, update: TaskUpdate) {
1053 let now = self.now();
1054 let period = self.speed_estimate_period;
1055 let Some(task) = self.task_mut(id) else {
1056 return;
1057 };
1058 let completed_start = task.completed;
1059 if let Some(total) = update.total {
1060 if Some(total) != task.total {
1061 task.total = Some(total);
1062 task.clear_progress();
1063 }
1064 }
1065 if let Some(advance) = update.advance {
1066 task.completed += advance;
1067 }
1068 if let Some(completed) = update.completed {
1069 task.completed = completed;
1070 }
1071 if let Some(description) = update.description {
1072 task.description = description;
1073 }
1074 if let Some(visible) = update.visible {
1075 task.visible = visible;
1076 }
1077 task.fields.extend(update.fields);
1078 let update_completed = task.completed - completed_start;
1079 let old_sample_time = now - period;
1080 while task
1081 .samples
1082 .front()
1083 .is_some_and(|(time, _)| *time < old_sample_time)
1084 {
1085 task.samples.pop_front();
1086 }
1087 if update_completed > 0.0 {
1088 task.samples.push_back((now, update_completed));
1089 if task.samples.len() > MAX_SAMPLES {
1090 task.samples.pop_front();
1091 }
1092 }
1093 if task.total.is_some_and(|total| task.completed >= total) && task.finished_time.is_none() {
1094 task.finished_time = task.elapsed();
1095 }
1096 }
1097
1098 pub fn advance(&mut self, id: TaskId, amount: f64) {
1101 let now = self.now();
1102 let period = self.speed_estimate_period;
1103 let Some(task) = self.task_mut(id) else {
1104 return;
1105 };
1106 let completed_start = task.completed;
1107 task.completed += amount;
1108 let update_completed = task.completed - completed_start;
1109 let old_sample_time = now - period;
1110 while task
1111 .samples
1112 .front()
1113 .is_some_and(|(time, _)| *time < old_sample_time)
1114 {
1115 task.samples.pop_front();
1116 }
1117 while task.samples.len() > MAX_SAMPLES {
1118 task.samples.pop_front();
1119 }
1120 task.samples.push_back((now, update_completed));
1121 if task.samples.len() > MAX_SAMPLES {
1122 task.samples.pop_front();
1123 }
1124 if task.total.is_some_and(|total| task.completed >= total) && task.finished_time.is_none() {
1125 task.finished_time = task.elapsed();
1126 task.finished_speed = task.speed();
1127 }
1128 }
1129
1130 pub fn reset(&mut self, id: TaskId, start: bool, total: Option<f64>, completed: f64) {
1134 let now = self.now();
1135 let Some(task) = self.task_mut(id) else {
1136 return;
1137 };
1138 task.clear_progress();
1139 task.start_time = start.then_some(now);
1140 if let Some(total) = total {
1141 task.total = Some(total);
1142 }
1143 task.completed = completed;
1144 task.finished_time = None;
1145 }
1146
1147 pub fn remove_task(&mut self, id: TaskId) {
1149 self.tasks.retain(|task| task.id != id);
1150 }
1151}
1152
1153impl Progress {
1154 pub fn make_tasks_table(&self) -> Table {
1159 let mut table = Table::grid().padding(0, 1, 0, 1).expand(self.expand);
1160 for column in &self.columns {
1161 table.add_column_with(Text::new(""), column.table_column());
1162 }
1163 for task in self.tasks.iter().filter(|task| task.visible) {
1164 let cells = self
1167 .columns
1168 .iter()
1169 .map(|column| column.table_cell(task))
1170 .collect();
1171 table.add_row_cells(cells);
1172 }
1173 table
1174 }
1175}
1176
1177impl Renderable for Progress {
1178 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
1179 self.make_tasks_table().rich_render(console, options)
1180 }
1181}
1182
1183impl Progress {
1184 pub fn start<W: std::io::Write + Send + 'static>(
1189 self,
1190 console: Console,
1191 writer: W,
1192 refresh_per_second: f64,
1193 ) -> LiveProgress<W> {
1194 if self.disable {
1196 return LiveProgress {
1197 progress: Arc::new(std::sync::Mutex::new(self)),
1198 live: None,
1199 disabled: Some(std::sync::Mutex::new((console, writer))),
1200 interactive: true,
1201 holder: Arc::new(std::sync::Mutex::new(None)),
1202 handle: crate::live::LiveHandle::detached(),
1203 };
1204 }
1205 let options = crate::live::LiveOptions {
1206 transient: self.transient,
1207 redirect_stdout: self.redirect_stdout,
1208 redirect_stderr: self.redirect_stderr,
1209 ..crate::live::LiveOptions::default()
1210 };
1211 let interactive = console.is_terminal() && !console.is_dumb_terminal();
1213 let shared = Arc::new(std::sync::Mutex::new(self));
1214 let view = shared.clone();
1215 let live = crate::live::Live::spawn_get_renderable(
1216 move || {
1218 let progress = view.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
1219 Box::new(progress.make_tasks_table()) as Box<dyn Renderable>
1220 },
1221 console,
1222 writer,
1223 refresh_per_second,
1224 options,
1225 );
1226 let holder: crate::live::Holder = Arc::new(std::sync::Mutex::new(None));
1227 LiveProgress {
1228 progress: shared,
1229 handle: live.handle().with_holder(holder.clone()),
1230 live: Some(live),
1231 disabled: None,
1232 interactive,
1233 holder,
1234 }
1235 }
1236}
1237
1238pub struct LiveProgress<W: std::io::Write + Send + 'static> {
1241 progress: Arc<std::sync::Mutex<Progress>>,
1242 live: Option<crate::live::AutoLive<W>>,
1243 disabled: Option<std::sync::Mutex<(Console, W)>>,
1246 interactive: bool,
1248 holder: crate::live::Holder,
1252 handle: crate::live::LiveHandle<W>,
1254}
1255
1256struct HolderGuard<'a>(&'a crate::live::Holder);
1259
1260impl Drop for HolderGuard<'_> {
1261 fn drop(&mut self) {
1262 *self
1263 .0
1264 .lock()
1265 .unwrap_or_else(|poisoned| poisoned.into_inner()) = None;
1266 }
1267}
1268
1269impl<W: std::io::Write + Send + 'static> LiveProgress<W> {
1270 pub fn with<R>(&self, f: impl FnOnce(&mut Progress) -> R) -> R {
1277 let current = std::thread::current().id();
1278 assert!(
1279 !self.held_by(current),
1280 "LiveProgress::with re-entered from inside a `with` closure; \
1281 use the `&mut Progress` it was given instead"
1282 );
1283 let mut progress = match self.progress.lock() {
1284 Ok(progress) => progress,
1285 Err(poisoned) => poisoned.into_inner(),
1286 };
1287 *self
1288 .holder
1289 .lock()
1290 .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(current);
1291 let _holder = HolderGuard(&self.holder);
1292 f(&mut progress)
1293 }
1294
1295 fn held_by(&self, thread: std::thread::ThreadId) -> bool {
1297 *self
1298 .holder
1299 .lock()
1300 .unwrap_or_else(|poisoned| poisoned.into_inner())
1301 == Some(thread)
1302 }
1303
1304 pub fn add_task(
1306 &self,
1307 description: impl Into<String>,
1308 total: impl Into<Option<f64>>,
1309 completed: f64,
1310 ) -> TaskId {
1311 let id = self.with(|progress| progress.add_task(description, total, completed));
1312 self.refresh();
1313 id
1314 }
1315
1316 pub fn reset(&self, id: TaskId, start: bool, total: Option<f64>, completed: f64) {
1318 self.with(|progress| progress.reset(id, start, total, completed));
1319 self.refresh();
1320 }
1321
1322 pub fn advance(&self, id: TaskId, amount: f64) {
1324 self.with(|progress| progress.advance(id, amount));
1325 }
1326
1327 pub fn update(&self, id: TaskId, update: TaskUpdate) {
1330 let refresh = update.refresh;
1331 self.with(|progress| progress.update(id, update));
1332 if refresh {
1333 self.refresh();
1334 }
1335 }
1336
1337 pub fn refresh(&self) {
1344 if let Some(live) = &self.live {
1345 if self.held_by(std::thread::current().id()) {
1346 live.refresh();
1347 } else {
1348 live.refresh_wait();
1349 }
1350 }
1351 }
1352
1353 pub fn print(&self, renderable: Box<dyn Renderable + Send>) {
1358 if self.live.is_some() {
1359 self.handle.print(renderable);
1360 } else {
1361 self.print_disabled(|console| console.print(renderable.as_ref()));
1362 }
1363 }
1364
1365 fn print_disabled(&self, print: impl FnOnce(&Console)) {
1367 if let Some(disabled) = &self.disabled {
1368 let mut guard = disabled
1369 .lock()
1370 .unwrap_or_else(|poisoned| poisoned.into_inner());
1371 let (console, writer) = &mut *guard;
1372 let output = console.capture(print);
1373 let _ = writer.write_all(output.as_bytes());
1374 }
1375 }
1376
1377 pub fn print_str(&self, content: impl Into<String>) {
1379 if self.live.is_some() {
1380 self.handle.print_str(content);
1381 } else {
1382 let content = content.into();
1383 self.print_disabled(|console| console.print_str(&content));
1384 }
1385 }
1386
1387 #[track_caller]
1393 pub fn log(
1394 &self,
1395 objects: &[crate::console::LogObject<'_>],
1396 options: &crate::console::LogOptions<'_>,
1397 ) -> crate::errors::Result<()> {
1398 let caller = std::panic::Location::caller();
1399 self.log_at(objects, options, caller.file(), caller.line())
1400 }
1401
1402 pub fn log_at(
1405 &self,
1406 objects: &[crate::console::LogObject<'_>],
1407 options: &crate::console::LogOptions<'_>,
1408 file: &str,
1409 line: u32,
1410 ) -> crate::errors::Result<()> {
1411 if self.live.is_some() {
1412 return self.handle.log_at(objects, options, file, line);
1413 }
1414 let mut result = Ok(());
1415 self.print_disabled(|console| result = console.log_at(objects, options, file, line));
1416 result
1417 }
1418
1419 pub fn stdout(
1423 &self,
1424 ) -> crate::file_proxy::FileProxy<crate::live::LiveHandle<W>, std::io::Stdout> {
1425 self.handle
1426 .file_proxy(crate::file_proxy::ProxyStream::Stdout, std::io::stdout())
1427 }
1428
1429 pub fn stderr(
1431 &self,
1432 ) -> crate::file_proxy::FileProxy<crate::live::LiveHandle<W>, std::io::Stderr> {
1433 self.handle
1434 .file_proxy(crate::file_proxy::ProxyStream::Stderr, std::io::stderr())
1435 }
1436
1437 pub fn file_proxy<F: std::io::Write>(
1440 &self,
1441 stream: crate::file_proxy::ProxyStream,
1442 file: F,
1443 ) -> crate::file_proxy::FileProxy<crate::live::LiveHandle<W>, F> {
1444 self.handle.file_proxy(stream, file)
1445 }
1446
1447 pub fn track<I: IntoIterator>(
1451 &self,
1452 iter: I,
1453 total: Option<f64>,
1454 description: impl Into<String>,
1455 ) -> Track<'_, I::IntoIter, W> {
1456 self.track_with(iter, total, 0.0, None, description, 0.1)
1457 }
1458
1459 pub fn track_with<I: IntoIterator>(
1468 &self,
1469 iter: I,
1470 total: Option<f64>,
1471 completed: f64,
1472 task: Option<TaskId>,
1473 description: impl Into<String>,
1474 update_period: f64,
1475 ) -> Track<'_, I::IntoIter, W> {
1476 let iter = iter.into_iter();
1477 let total = total.or_else(|| exact_length(&iter));
1478 let task = match task {
1479 Some(task) => {
1480 let mut update = TaskUpdate::default().completed(completed);
1481 if let Some(total) = total {
1482 update = update.total(total);
1483 }
1484 self.update(task, update);
1485 task
1486 }
1487 None => self.add_task(description, total, completed),
1488 };
1489 Track {
1490 iter,
1491 progress: self,
1492 state: TrackState::start(self.progress.clone(), task, update_period),
1493 }
1494 }
1495
1496 pub fn wrap_read<R: std::io::Read>(
1501 &self,
1502 reader: R,
1503 total: Option<u64>,
1504 task: Option<TaskId>,
1505 description: impl Into<String>,
1506 ) -> std::io::Result<ProgressReader<'_, R, W>> {
1507 let total = total.map(|total| total as f64).or_else(|| {
1508 task.and_then(|task| self.with(|progress| progress.task(task).and_then(Task::total)))
1509 });
1510 let Some(total) = total else {
1511 return Err(std::io::Error::new(
1512 std::io::ErrorKind::InvalidInput,
1513 "unable to get the total number of bytes, please specify 'total'",
1514 ));
1515 };
1516 let task = self.task_for(task, total, description);
1517 Ok(ProgressReader {
1518 reader,
1519 progress: self,
1520 task,
1521 })
1522 }
1523
1524 pub fn open(
1527 &self,
1528 path: impl AsRef<std::path::Path>,
1529 total: Option<u64>,
1530 task: Option<TaskId>,
1531 description: impl Into<String>,
1532 ) -> std::io::Result<ProgressReader<'_, std::fs::File, W>> {
1533 let file = std::fs::File::open(path)?;
1534 let total = match total {
1535 Some(total) => total,
1536 None => file.metadata()?.len(),
1537 };
1538 let task = self.task_for(task, total as f64, description);
1539 Ok(ProgressReader {
1540 reader: file,
1541 progress: self,
1542 task,
1543 })
1544 }
1545
1546 fn task_for(&self, task: Option<TaskId>, total: f64, description: impl Into<String>) -> TaskId {
1548 match task {
1549 Some(task) => {
1550 self.update(task, TaskUpdate::default().total(total));
1551 task
1552 }
1553 None => self.add_task(description, total, 0.0),
1554 }
1555 }
1556
1557 pub fn stop(mut self) -> (Progress, W) {
1560 let writer = match self.live.take() {
1561 Some(live) => {
1562 let mut writer = live.stop();
1563 if !self.interactive {
1565 let _ = writer.write_all(b"\n");
1566 }
1567 writer
1568 }
1569 None => {
1570 self.disabled
1571 .take()
1572 .expect("a disabled display keeps its writer")
1573 .into_inner()
1574 .unwrap_or_else(|poisoned| poisoned.into_inner())
1575 .1
1576 }
1577 };
1578 let progress = match Arc::try_unwrap(std::mem::replace(
1579 &mut self.progress,
1580 Arc::new(std::sync::Mutex::new(Progress::new())),
1581 )) {
1582 Ok(mutex) => mutex
1583 .into_inner()
1584 .unwrap_or_else(|poisoned| poisoned.into_inner()),
1585 Err(_) => unreachable!("progress still shared after the live display stopped"),
1587 };
1588 (progress, writer)
1589 }
1590}
1591
1592pub struct ProgressReader<'a, R, W: std::io::Write + Send + 'static> {
1595 reader: R,
1596 progress: &'a LiveProgress<W>,
1597 task: TaskId,
1598}
1599
1600impl<R, W: std::io::Write + Send + 'static> ProgressReader<'_, R, W> {
1601 pub fn task(&self) -> TaskId {
1603 self.task
1604 }
1605
1606 pub fn into_inner(self) -> R {
1608 self.reader
1609 }
1610}
1611
1612impl<R: std::io::Read, W: std::io::Write + Send + 'static> std::io::Read
1613 for ProgressReader<'_, R, W>
1614{
1615 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1616 let count = self.reader.read(buf)?;
1617 self.progress.advance(self.task, count as f64);
1618 Ok(count)
1619 }
1620}
1621
1622impl<R: std::io::BufRead, W: std::io::Write + Send + 'static> std::io::BufRead
1623 for ProgressReader<'_, R, W>
1624{
1625 fn fill_buf(&mut self) -> std::io::Result<&[u8]> {
1626 self.reader.fill_buf()
1627 }
1628
1629 fn consume(&mut self, amount: usize) {
1630 self.reader.consume(amount);
1631 self.progress.advance(self.task, amount as f64);
1632 }
1633}
1634
1635fn exact_length<I: Iterator>(iter: &I) -> Option<f64> {
1638 match iter.size_hint() {
1639 (lower, Some(upper)) if lower == upper && lower > 0 => Some(lower as f64),
1640 _ => None,
1641 }
1642}
1643
1644struct TrackState {
1647 task: TaskId,
1648 completed: Arc<std::sync::atomic::AtomicU64>,
1650 pending: bool,
1653 stop: Option<std::sync::mpsc::Sender<()>>,
1654 thread: Option<std::thread::JoinHandle<()>>,
1655}
1656
1657impl TrackState {
1658 fn start(progress: Arc<std::sync::Mutex<Progress>>, task: TaskId, update_period: f64) -> Self {
1659 use std::sync::atomic::Ordering;
1660 let completed = Arc::new(std::sync::atomic::AtomicU64::new(0));
1661 let (stop, stopped) = std::sync::mpsc::channel::<()>();
1662 let count = completed.clone();
1663 let period = std::time::Duration::from_secs_f64(update_period.max(0.0));
1664 let thread = std::thread::spawn(move || {
1665 let mut last = 0;
1666 while let Err(std::sync::mpsc::RecvTimeoutError::Timeout) = stopped.recv_timeout(period)
1668 {
1669 let now = count.load(Ordering::SeqCst);
1670 if now != last {
1671 progress
1672 .lock()
1673 .unwrap_or_else(|poisoned| poisoned.into_inner())
1674 .advance(task, (now - last) as f64);
1675 last = now;
1676 }
1677 }
1678 });
1679 TrackState {
1680 task,
1681 completed,
1682 pending: false,
1683 stop: Some(stop),
1684 thread: Some(thread),
1685 }
1686 }
1687
1688 fn next_requested(&mut self) {
1690 if std::mem::take(&mut self.pending) {
1691 self.completed
1692 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1693 }
1694 }
1695
1696 fn finish<W: std::io::Write + Send + 'static>(&mut self, live: &LiveProgress<W>) {
1699 let Some(thread) = self.thread.take() else {
1700 return;
1701 };
1702 self.next_requested();
1703 drop(self.stop.take());
1704 let _ = thread.join();
1705 let completed = self.completed.load(std::sync::atomic::Ordering::SeqCst);
1706 live.update(
1707 self.task,
1708 TaskUpdate::default()
1709 .completed(completed as f64)
1710 .refresh(true),
1711 );
1712 }
1713}
1714
1715pub struct Track<'a, I: Iterator, W: std::io::Write + Send + 'static> {
1717 iter: I,
1718 progress: &'a LiveProgress<W>,
1719 state: TrackState,
1720}
1721
1722impl<I: Iterator, W: std::io::Write + Send + 'static> Track<'_, I, W> {
1723 pub fn task(&self) -> TaskId {
1725 self.state.task
1726 }
1727}
1728
1729impl<I: Iterator, W: std::io::Write + Send + 'static> Iterator for Track<'_, I, W> {
1730 type Item = I::Item;
1731
1732 fn next(&mut self) -> Option<I::Item> {
1733 self.state.next_requested();
1734 let item = self.iter.next();
1735 if item.is_some() {
1736 self.state.pending = true;
1737 } else {
1738 self.state.finish(self.progress);
1739 }
1740 item
1741 }
1742
1743 fn size_hint(&self) -> (usize, Option<usize>) {
1744 self.iter.size_hint()
1745 }
1746}
1747
1748impl<I: Iterator, W: std::io::Write + Send + 'static> Drop for Track<'_, I, W> {
1749 fn drop(&mut self) {
1752 self.state.pending = false;
1754 self.state.finish(self.progress);
1755 }
1756}
1757
1758pub fn track<I: IntoIterator>(iter: I, description: &str) -> TrackStdout<I::IntoIter> {
1763 let mut columns = Vec::new();
1764 if !description.is_empty() {
1765 columns.push(ProgressColumn::Description);
1766 }
1767 columns.extend([
1768 ProgressColumn::Bar,
1769 ProgressColumn::TaskProgress { show_speed: true },
1770 ProgressColumn::TimeRemaining(TimeRemainingColumn::new(false, true)),
1771 ]);
1772 let iter = iter.into_iter();
1773 let total = exact_length(&iter);
1774 let live = Progress::new()
1775 .columns(columns)
1776 .start(Console::new(), std::io::stdout(), 10.0);
1777 let task = live.add_task(description, total, 0.0);
1778 TrackStdout {
1779 iter,
1780 state: TrackState::start(live.progress.clone(), task, 0.1),
1781 live: Some(live),
1782 }
1783}
1784
1785pub struct TrackStdout<I: Iterator> {
1787 iter: I,
1788 live: Option<LiveProgress<std::io::Stdout>>,
1789 state: TrackState,
1790}
1791
1792impl<I: Iterator> Iterator for TrackStdout<I> {
1793 type Item = I::Item;
1794
1795 fn next(&mut self) -> Option<I::Item> {
1796 self.live.as_ref()?;
1797 self.state.next_requested();
1798 match self.iter.next() {
1799 Some(item) => {
1800 self.state.pending = true;
1801 Some(item)
1802 }
1803 None => {
1804 if let Some(live) = self.live.take() {
1805 self.state.finish(&live);
1806 live.stop();
1807 }
1808 None
1809 }
1810 }
1811 }
1812}
1813
1814impl<I: Iterator> Drop for TrackStdout<I> {
1815 fn drop(&mut self) {
1816 if let Some(live) = self.live.take() {
1817 self.state.pending = false;
1818 self.state.finish(&live);
1819 live.stop();
1820 }
1821 }
1822}
1823
1824fn whole_number(value: f64) -> FormatValue {
1826 if value.fract() == 0.0 && value.abs() < 9_007_199_254_740_992.0 {
1827 FormatValue::Int(value as i64)
1828 } else {
1829 FormatValue::Float(value)
1830 }
1831}
1832
1833#[cfg(test)]
1834mod tests {
1835 use super::*;
1836 use crate::color::ColorSystem;
1837
1838 fn render(progress: &Progress) -> String {
1839 Console::builder()
1840 .force_terminal(true)
1841 .color_system(Some(ColorSystem::Truecolor))
1842 .width(50)
1843 .no_color(false)
1844 .build()
1845 .render_to_string(progress)
1846 }
1847
1848 #[test]
1849 fn three_tasks_match_upstream() {
1850 let mut progress = Progress::new().columns(vec![
1852 ProgressColumn::Description,
1853 ProgressColumn::Bar,
1854 ProgressColumn::Percentage,
1855 ]);
1856 progress.add_task("Downloading", 100.0, 50.0);
1857 progress.add_task("Processing", 100.0, 100.0);
1858 progress.add_task("Waiting", 100.0, 0.0);
1859 let expected = concat!(
1860 "Downloading \x1b[38;2;249;38;114m━━━━━━━━━━━━━━━━\x1b[0m",
1861 "\x1b[38;2;249;38;114m╸\x1b[0m\x1b[38;5;237m━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 50%\x1b[0m\n",
1862 "Processing \x1b[38;2;114;156;31m",
1863 "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m100%\x1b[0m\n",
1864 "Waiting \x1b[38;5;237m",
1865 "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 0%\x1b[0m",
1866 );
1867 assert_eq!(render(&progress), expected);
1868 }
1869
1870 #[test]
1871 fn download_text_matches_upstream() {
1872 let dl = |completed: f64, total: f64| {
1874 let mut progress = Progress::new();
1875 let id = progress.add_task("", total, completed);
1876 progress.task(id).unwrap().download_text(false)
1877 };
1878 assert_eq!(dl(500.0, 1000.0), "0.5/1.0 kB");
1879 assert_eq!(dl(500.0, 999.0), "500/999 bytes");
1880 assert_eq!(dl(1_500_000.0, 3_000_000.0), "1.5/3.0 MB");
1881 assert_eq!(dl(0.0, 1024.0), "0.0/1.0 kB");
1882 assert_eq!(dl(2_500_000_000.0, 10_000_000_000.0), "2.5/10.0 GB");
1883 assert_eq!(dl(250.0, 250.0), "250/250 bytes");
1884 }
1885
1886 #[test]
1887 fn download_column_in_grid_matches_upstream() {
1888 let mut progress = Progress::new().columns(vec![
1890 ProgressColumn::Description,
1891 ProgressColumn::Bar,
1892 ProgressColumn::Download,
1893 ]);
1894 progress.add_task("File", 1000.0, 500.0);
1895 let expected = concat!(
1896 "File \x1b[38;2;249;38;114m━━━━━━━━━━━━━━━━━\x1b[0m\x1b[38;5;237m╺\x1b[0m",
1897 "\x1b[38;5;237m━━━━━━━━━━━━━━━━\x1b[0m \x1b[32m0.5/1.0 kB\x1b[0m",
1898 );
1899 assert_eq!(render(&progress), expected);
1900 }
1901
1902 #[test]
1903 fn custom_columns_with_mofn_match_upstream() {
1904 let mut progress = Progress::new().columns(vec![
1907 ProgressColumn::Description,
1908 ProgressColumn::Bar,
1909 ProgressColumn::MofN,
1910 ]);
1911 progress.add_task("A", 5.0, 3.0);
1912 progress.add_task("B", 100.0, 50.0);
1913 let console = Console::builder()
1914 .force_terminal(true)
1915 .color_system(Some(ColorSystem::Truecolor))
1916 .width(40)
1917 .no_color(false)
1918 .build();
1919 let expected = concat!(
1920 "A \x1b[38;2;249;38;114m━━━━━━━━━━━━━━━━━━\x1b[0m\x1b[38;5;237m╺\x1b[0m",
1921 "\x1b[38;5;237m━━━━━━━━━━━\x1b[0m \x1b[32m3/5 \x1b[0m\n",
1922 "B \x1b[38;2;249;38;114m━━━━━━━━━━━━━━━\x1b[0m\x1b[38;5;237m╺\x1b[0m",
1923 "\x1b[38;5;237m━━━━━━━━━━━━━━\x1b[0m \x1b[32m 50/100\x1b[0m",
1924 );
1925 assert_eq!(console.render_to_string(&progress), expected);
1926 }
1927
1928 fn live(columns: Vec<ProgressColumn>) -> LiveProgress<Vec<u8>> {
1929 let console = Console::builder()
1930 .force_terminal(true)
1931 .color_system(Some(ColorSystem::Truecolor))
1932 .width(40)
1933 .build();
1934 Progress::new()
1935 .columns(columns)
1936 .clock(|| 0.0)
1937 .start(console, Vec::new(), 1e-9)
1938 }
1939
1940 fn within_deadline<R: Send + 'static>(f: impl FnOnce() -> R + Send + 'static) -> R {
1943 let (done, wait) = std::sync::mpsc::channel();
1944 std::thread::spawn(move || {
1945 let _ = done.send(std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)));
1946 });
1947 match wait.recv_timeout(std::time::Duration::from_secs(10)) {
1948 Ok(Ok(value)) => value,
1949 Ok(Err(payload)) => std::panic::resume_unwind(payload),
1950 Err(_) => panic!("deadlocked: did not finish within 10s"),
1951 }
1952 }
1953
1954 #[test]
1955 fn refresh_inside_with_does_not_deadlock() {
1956 let output = within_deadline(|| {
1959 let live = live(vec![ProgressColumn::Description, ProgressColumn::MofN]);
1960 live.with(|progress| {
1961 let task = progress.add_task("inside", Some(2.0), 1.0);
1962 live.refresh();
1963 task
1964 });
1965 live.refresh();
1966 String::from_utf8(live.stop().1).unwrap()
1967 });
1968 assert!(
1969 output.ends_with("inside \x1b[32m1/2\x1b[0m\n\x1b[?25h"),
1970 "{output:?}"
1971 );
1972 }
1973
1974 #[test]
1975 fn nested_with_panics_instead_of_deadlocking() {
1976 let result = within_deadline(|| {
1977 let live = live(vec![ProgressColumn::Description]);
1978 let nested = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1979 live.with(|_| live.add_task("nested", None, 0.0))
1980 }));
1981 let task = live.add_task("after", None, 0.0);
1983 live.stop();
1984 (nested.is_err(), task)
1985 });
1986 assert!(result.0, "a nested `with` must be refused, not deadlock");
1987 }
1988
1989 #[test]
1990 fn the_track_thread_advances_by_the_items_counted() {
1991 let live = live(vec![ProgressColumn::Description, ProgressColumn::MofN]);
1992 let tracked = live.track_with(0..4, None, 0.0, None, "n", 0.001);
1993 let task = tracked.task();
1994 let mut seen = 0.0;
1995 for item in tracked {
1996 if item == 2 {
1997 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
1999 while seen < 2.0 && std::time::Instant::now() < deadline {
2000 std::thread::sleep(std::time::Duration::from_millis(1));
2001 seen = live.with(|progress| progress.task(task).unwrap().completed());
2002 }
2003 }
2004 }
2005 assert_eq!(seen, 2.0);
2006 let (progress, _) = live.stop();
2007 assert_eq!(progress.task(task).unwrap().completed(), 4.0);
2008 }
2009
2010 #[test]
2011 fn breaking_out_of_track_counts_only_finished_items() {
2012 let live = live(vec![ProgressColumn::Description, ProgressColumn::MofN]);
2013 let tracked = live.track_with(0..10, None, 0.0, None, "n", 1e9);
2014 let task = tracked.task();
2015 for item in tracked {
2016 if item == 3 {
2017 break;
2018 }
2019 }
2020 let (progress, _) = live.stop();
2021 assert_eq!(progress.task(task).unwrap().completed(), 3.0);
2022 }
2023
2024 #[test]
2025 fn track_counts_each_item_after_its_loop_body() {
2026 let live = live(vec![ProgressColumn::Description, ProgressColumn::MofN]);
2027 let mut seen = Vec::new();
2028 let tracked = live.track_with(vec!['a', 'b', 'c'], None, 0.0, None, "letters", 1e9);
2031 let task = tracked.task();
2032 for item in tracked {
2033 let completed = live.with(|progress| progress.task(task).unwrap().completed());
2034 seen.push((item, completed));
2035 }
2036 assert_eq!(seen, vec![('a', 0.0), ('b', 0.0), ('c', 0.0)]);
2037 let (progress, bytes) = live.stop();
2038 let task = progress.task(task).unwrap();
2039 assert_eq!((task.total(), task.completed()), (Some(3.0), 3.0));
2040 let output = String::from_utf8(bytes).unwrap();
2041 assert!(
2042 output.ends_with("letters \x1b[32m3/3\x1b[0m\n\x1b[?25h"),
2043 "{output:?}"
2044 );
2045 }
2046
2047 #[test]
2048 fn track_leaves_an_iterator_of_unknown_length_indeterminate() {
2049 let live = live(vec![ProgressColumn::MofN]);
2050 let task = {
2051 let mut tracked = live.track((0..10).filter(|n| n % 3 == 0), None, "");
2052 let task = tracked.task();
2053 assert_eq!(tracked.by_ref().count(), 4);
2054 task
2055 };
2056 let with_total = {
2057 let mut tracked = live.track(0..2, Some(5.0), "");
2058 tracked.by_ref().for_each(drop);
2059 tracked.task()
2060 };
2061 let (progress, _) = live.stop();
2062 assert_eq!(progress.task(task).unwrap().total(), None);
2063 assert_eq!(progress.task(task).unwrap().completed(), 4.0);
2064 assert_eq!(progress.task(with_total).unwrap().total(), Some(5.0));
2065 }
2066
2067 fn quiet_console() -> Console {
2068 Console::builder().force_terminal(false).width(40).build()
2069 }
2070
2071 #[test]
2072 fn wrap_read_advances_by_the_bytes_read() {
2073 use std::io::Read;
2074 let live = Progress::new()
2075 .disable(true)
2076 .start(quiet_console(), Vec::new(), 1.0);
2077 let mut reader = live
2078 .wrap_read(&b"hello world"[..], Some(11), None, "Reading...")
2079 .expect("total given");
2080 let task = reader.task();
2081 let mut buf = [0u8; 4];
2082 reader.read_exact(&mut buf).unwrap();
2083 assert_eq!(live.with(|p| p.task(task).unwrap().completed()), 4.0);
2084 let mut rest = Vec::new();
2085 reader.read_to_end(&mut rest).unwrap();
2086 assert!(live.with(|p| p.task(task).unwrap().finished()));
2087 let (_, out) = live.stop();
2088 assert!(out.is_empty(), "a disabled display writes nothing");
2089 }
2090
2091 #[test]
2092 fn wrap_read_needs_a_total() {
2093 let live = Progress::new()
2094 .disable(true)
2095 .start(quiet_console(), Vec::new(), 1.0);
2096 let err = live
2097 .wrap_read(&b""[..], None, None, "x")
2098 .err()
2099 .expect("no total");
2100 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
2101 let task = live.add_task("sized", 5.0, 0.0);
2102 assert!(live.wrap_read(&b""[..], None, Some(task), "x").is_ok());
2103 }
2104
2105 #[test]
2106 fn open_takes_the_file_size_as_total() {
2107 use std::io::Read;
2108 let path = std::env::temp_dir().join(format!("rs-rich-open-{}", std::process::id()));
2109 std::fs::write(&path, b"0123456789").unwrap();
2110 let live = Progress::new()
2111 .disable(true)
2112 .start(quiet_console(), Vec::new(), 1.0);
2113 let mut reader = live.open(&path, None, None, "Reading...").unwrap();
2114 let task = reader.task();
2115 assert_eq!(live.with(|p| p.task(task).unwrap().total()), Some(10.0));
2116 std::io::copy(&mut reader, &mut std::io::sink()).unwrap();
2117 assert_eq!(live.with(|p| p.task(task).unwrap().completed()), 10.0);
2118 let _ = reader.read(&mut [0u8; 1]);
2119 drop(reader);
2120 std::fs::remove_file(path).unwrap();
2121 }
2122}