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}
197
198#[derive(Clone, Debug)]
200pub struct BarColumn {
201 bar_width: Option<usize>,
202 style: StyleType,
203 complete_style: StyleType,
204 finished_style: StyleType,
205 pulse_style: StyleType,
206}
207
208impl Default for BarColumn {
209 fn default() -> Self {
210 BarColumn {
211 bar_width: Some(40),
212 style: "bar.back".into(),
213 complete_style: "bar.complete".into(),
214 finished_style: "bar.finished".into(),
215 pulse_style: "bar.pulse".into(),
216 }
217 }
218}
219
220impl BarColumn {
221 pub fn new() -> Self {
223 BarColumn::default()
224 }
225
226 pub fn bar_width(mut self, width: Option<usize>) -> Self {
228 self.bar_width = width;
229 self
230 }
231
232 pub fn style(mut self, style: impl Into<StyleType>) -> Self {
234 self.style = style.into();
235 self
236 }
237
238 pub fn complete_style(mut self, style: impl Into<StyleType>) -> Self {
240 self.complete_style = style.into();
241 self
242 }
243
244 pub fn finished_style(mut self, style: impl Into<StyleType>) -> Self {
246 self.finished_style = style.into();
247 self
248 }
249
250 pub fn pulse_style(mut self, style: impl Into<StyleType>) -> Self {
252 self.pulse_style = style.into();
253 self
254 }
255
256 fn render(&self, task: &Task) -> ProgressBar {
258 let bar = match task.total {
259 Some(total) => ProgressBar::new(total.max(0.0), task.completed.max(0.0)),
260 None => ProgressBar::indeterminate(),
261 };
262 let bar = match self.bar_width {
263 Some(width) => bar.width(width.max(1)),
264 None => bar,
265 };
266 bar.pulse(!task.started())
267 .animation_time(task.now())
268 .style(self.style.clone())
269 .complete_style(self.complete_style.clone())
270 .finished_style(self.finished_style.clone())
271 .pulse_style(self.pulse_style.clone())
272 }
273}
274
275impl ProgressColumn {
276 pub fn time_remaining() -> Self {
278 ProgressColumn::TimeRemaining(TimeRemainingColumn::new(false, false))
279 }
280
281 pub fn spinner() -> Self {
283 ProgressColumn::Spinner(SpinnerColumn::new("dots", " "))
284 }
285
286 pub fn with_table_column(self, options: ColumnOptions) -> Self {
290 let inner = match self {
291 ProgressColumn::WithTableColumn(inner, _) => *inner,
292 column => column,
293 };
294 ProgressColumn::WithTableColumn(Box::new(inner), options)
295 }
296
297 fn table_column(&self) -> ColumnOptions {
300 match self {
301 ProgressColumn::WithTableColumn(_, options) => options.clone(),
302 ProgressColumn::Description
303 | ProgressColumn::Text(..)
304 | ProgressColumn::TextFormat(_)
305 | ProgressColumn::Percentage
306 | ProgressColumn::TaskProgress { .. } => ColumnOptions {
307 no_wrap: true,
308 ..ColumnOptions::default()
309 },
310 _ => ColumnOptions::default(),
311 }
312 }
313
314 fn table_cell(&self, task: &Task) -> Cell {
316 match self {
317 ProgressColumn::WithTableColumn(inner, _) => inner.table_cell(task),
318 ProgressColumn::Bar => Cell::Renderable(Arc::new(BarColumn::default().render(task))),
319 ProgressColumn::BarWith(column) => Cell::Renderable(Arc::new(column.render(task))),
320 ProgressColumn::Renderable(renderable) => Cell::Renderable(renderable.clone()),
321 column => Cell::Text(column.cell(task)),
322 }
323 }
324
325 fn cell(&self, task: &Task) -> Text {
327 let named = |plain: String, style: &str| Text::styled(plain, style);
328 match self {
329 ProgressColumn::Description => {
332 let markup = format!("[progress.description]{}", task.description);
333 Text::from_markup(&markup)
334 .unwrap_or_else(|_| Text::new(task.description.clone()))
335 .justify(Justify::Left)
336 }
337 ProgressColumn::Text(text, style) => {
338 Text::styled(text.clone(), style.clone()).justify(Justify::Left)
339 }
340 ProgressColumn::TextFormat(column) => column.render(task),
341 ProgressColumn::Bar
342 | ProgressColumn::BarWith(_)
343 | ProgressColumn::Renderable(_)
344 | ProgressColumn::WithTableColumn(..) => {
345 unreachable!("bar, renderable and wrapped columns have no text cell")
346 }
347 ProgressColumn::Percentage => task.percentage_cell().justify(Justify::Left),
348 ProgressColumn::TaskProgress { show_speed } => {
349 if task.total.is_none() && *show_speed {
350 render_speed(
351 task.finished_speed
352 .filter(|s| *s != 0.0)
353 .or_else(|| task.speed()),
354 )
355 } else {
356 task.percentage_cell().justify(Justify::Left)
357 }
358 }
359 ProgressColumn::MofN => named(task.mofn_text(), "progress.download"),
360 ProgressColumn::Download => named(task.download_text(false), "progress.download"),
361 ProgressColumn::BinaryDownload => named(task.download_text(true), "progress.download"),
362 ProgressColumn::TimeElapsed => {
363 let elapsed = if task.finished() {
364 task.finished_time
365 } else {
366 task.elapsed()
367 };
368 let text = match elapsed {
369 None => "-:--:--".to_string(),
370 Some(elapsed) => timedelta(elapsed.max(0.0) as i64),
371 };
372 named(text, "progress.elapsed")
373 }
374 ProgressColumn::TimeRemaining(column) => column.render(task),
375 ProgressColumn::TransferSpeed => {
376 let speed = task
377 .finished_speed
378 .filter(|s| *s != 0.0)
379 .or_else(|| task.speed());
380 let text = match speed {
381 None => "?".to_string(),
382 Some(speed) => format!("{}/s", filesize::decimal_signed(speed as i64)),
383 };
384 named(text, "progress.data.speed")
385 }
386 ProgressColumn::FileSize => named(
389 filesize::decimal_signed(task.completed as i64),
390 "progress.filesize",
391 ),
392 ProgressColumn::TotalFileSize => named(
393 task.total
394 .map_or_else(String::new, |total| filesize::decimal_signed(total as i64)),
395 "progress.filesize.total",
396 ),
397 ProgressColumn::Spinner(column) => {
398 if task.finished() {
399 Text::from_markup(&column.finished_text)
400 .unwrap_or_else(|_| Text::new(column.finished_text.clone()))
401 } else {
402 let mut frame = column.spinner.render(task.now());
405 frame.set_base_style(column.style.clone());
406 frame
407 }
408 }
409 }
410 }
411}
412
413impl TimeRemainingColumn {
414 fn render(&self, task: &Task) -> Text {
415 let now = task.now();
418 if task.completed == 0.0 {
419 if let Some((timestamp, text)) = self.cache.borrow().get(&task.id) {
420 if timestamp + 0.5 > now {
421 return text.clone();
422 }
423 }
424 }
425 let (task_time, style) = if self.elapsed_when_finished && task.finished() {
426 (task.finished_time, "progress.elapsed")
427 } else {
428 (task.time_remaining(), "progress.remaining")
429 };
430 let text = if task.total.is_none() {
431 Text::styled("", style)
432 } else {
433 match task_time {
434 None => Text::styled(if self.compact { "--:--" } else { "-:--:--" }, style),
435 Some(task_time) => {
436 let whole = task_time as i64;
437 let (minutes, seconds) = (whole.div_euclid(60), whole.rem_euclid(60));
438 let (hours, minutes) = (minutes.div_euclid(60), minutes.rem_euclid(60));
439 let formatted = if self.compact && hours == 0 {
440 format!("{minutes:02}:{seconds:02}")
441 } else {
442 format!("{hours}:{minutes:02}:{seconds:02}")
443 };
444 Text::styled(formatted, style)
445 }
446 }
447 };
448 self.cache.borrow_mut().insert(task.id, (now, text.clone()));
449 text
450 }
451}
452
453fn render_speed(speed: Option<f64>) -> Text {
456 let Some(speed) = speed else {
457 return Text::styled("", "progress.percentage");
458 };
459 let (unit, suffix) = filesize::pick_unit_and_suffix_signed(
460 speed as i64,
461 &["", "×10³", "×10⁶", "×10⁹", "×10¹²"],
462 1000,
463 );
464 let data_speed = speed / unit as f64;
465 Text::styled(
466 format!("{data_speed:.1}{suffix} it/s"),
467 "progress.percentage",
468 )
469}
470
471fn timedelta(total_seconds: i64) -> String {
474 let days = total_seconds / 86_400;
475 let rest = total_seconds % 86_400;
476 let clock = format!("{}:{:02}:{:02}", rest / 3600, rest % 3600 / 60, rest % 60);
477 match days {
478 0 => clock,
479 1 => format!("1 day, {clock}"),
480 days => format!("{days} days, {clock}"),
481 }
482}
483
484fn grouped(value: f64, precision: usize) -> String {
486 let formatted = format!("{value:.precision$}");
487 let (sign, digits) = match formatted.strip_prefix('-') {
488 Some(rest) => ("-", rest),
489 None => ("", formatted.as_str()),
490 };
491 let (integer, fraction) = match digits.split_once('.') {
492 Some((integer, fraction)) => (integer, Some(fraction)),
493 None => (digits, None),
494 };
495 let mut grouped = String::new();
496 for (index, digit) in integer.chars().enumerate() {
497 if index > 0 && (integer.len() - index) % 3 == 0 {
498 grouped.push(',');
499 }
500 grouped.push(digit);
501 }
502 match fraction {
503 Some(fraction) => format!("{sign}{grouped}.{fraction}"),
504 None => format!("{sign}{grouped}"),
505 }
506}
507
508pub struct Task {
511 id: TaskId,
512 description: String,
513 total: Option<f64>,
514 completed: f64,
515 visible: bool,
516 start_time: Option<f64>,
517 stop_time: Option<f64>,
518 finished_time: Option<f64>,
519 finished_speed: Option<f64>,
520 samples: VecDeque<(f64, f64)>,
522 fields: BTreeMap<String, FormatValue>,
524 get_time: GetTime,
525}
526
527impl Task {
528 pub fn fields(&self) -> &BTreeMap<String, FormatValue> {
530 &self.fields
531 }
532
533 fn format_field(&self, name: &str) -> Option<FormatValue> {
537 let attribute = name.strip_prefix("task.")?;
538 if let Some(key) = attribute
539 .strip_prefix("fields[")
540 .and_then(|rest| rest.strip_suffix(']'))
541 {
542 return self.fields.get(key).cloned();
543 }
544 Some(match attribute {
545 "id" => FormatValue::Int(self.id.0 as i64),
546 "description" => FormatValue::Str(self.description.clone()),
547 "total" => self.total.map_or(FormatValue::None, whole_number),
550 "completed" => whole_number(self.completed),
551 "visible" => FormatValue::Bool(self.visible),
552 "started" => FormatValue::Bool(self.started()),
553 "finished" => FormatValue::Bool(self.finished()),
554 "percentage" => FormatValue::Float(self.percentage()),
555 "remaining" => self.remaining().into(),
556 "elapsed" => self.elapsed().into(),
557 "speed" => self.speed().into(),
558 "time_remaining" => self.time_remaining().into(),
559 "start_time" => self.start_time.into(),
560 "stop_time" => self.stop_time.into(),
561 "finished_time" => self.finished_time.into(),
562 "finished_speed" => self.finished_speed.into(),
563 _ => return None,
564 })
565 }
566 fn now(&self) -> f64 {
567 (self.get_time)()
568 }
569
570 pub fn id(&self) -> TaskId {
572 self.id
573 }
574
575 pub fn description(&self) -> &str {
577 &self.description
578 }
579
580 pub fn total(&self) -> Option<f64> {
582 self.total
583 }
584
585 pub fn completed(&self) -> f64 {
587 self.completed
588 }
589
590 pub fn visible(&self) -> bool {
592 self.visible
593 }
594
595 pub fn started(&self) -> bool {
597 self.start_time.is_some()
598 }
599
600 pub fn remaining(&self) -> Option<f64> {
602 self.total.map(|total| total - self.completed)
603 }
604
605 pub fn elapsed(&self) -> Option<f64> {
607 let start = self.start_time?;
608 Some(self.stop_time.unwrap_or_else(|| self.now()) - start)
609 }
610
611 pub fn finished(&self) -> bool {
613 self.finished_time.is_some()
614 }
615
616 pub fn finished_time(&self) -> Option<f64> {
618 self.finished_time
619 }
620
621 pub fn percentage(&self) -> f64 {
623 match self.total {
624 Some(total) if total != 0.0 => (self.completed / total * 100.0).clamp(0.0, 100.0),
625 _ => 0.0,
626 }
627 }
628
629 pub fn speed(&self) -> Option<f64> {
631 self.start_time?;
632 let (first, _) = *self.samples.front()?;
633 let (last, _) = *self.samples.back()?;
634 let total_time = last - first;
635 if total_time == 0.0 {
636 return None;
637 }
638 let total_completed: f64 = self.samples.iter().skip(1).map(|(_, done)| done).sum();
639 Some(total_completed / total_time)
640 }
641
642 pub fn time_remaining(&self) -> Option<f64> {
644 if self.finished() {
645 return Some(0.0);
646 }
647 let speed = self.speed().filter(|speed| *speed != 0.0)?;
648 let remaining = self.remaining()?;
649 Some((remaining / speed).ceil())
650 }
651
652 fn clear_progress(&mut self) {
654 self.samples.clear();
655 self.finished_time = None;
656 self.finished_speed = None;
657 }
658
659 fn percentage_cell(&self) -> Text {
662 if self.total.is_none() {
663 return Text::new("");
664 }
665 let mut text = Text::new(format!("{:>3.0}%", self.percentage()));
666 let len = text.plain().len();
667 text.stylize("progress.percentage", 0, len);
668 text
669 }
670
671 fn mofn_text(&self) -> String {
674 let completed = self.completed as i64;
675 let total = self
676 .total
677 .map_or_else(|| "?".to_string(), |total| (total as i64).to_string());
678 let total_width = total.chars().count();
679 format!("{completed:>total_width$}/{total}")
680 }
681
682 fn download_text(&self, binary: bool) -> String {
685 const DECIMAL: &[&str] = &["bytes", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
686 const BINARY: &[&str] = &[
687 "bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB",
688 ];
689 let completed = self.completed as i64;
691 let base_size = self.total.map_or(completed, |total| total as i64);
692 let (unit, suffix) = if binary {
693 filesize::pick_unit_and_suffix_signed(base_size, BINARY, 1024)
694 } else {
695 filesize::pick_unit_and_suffix_signed(base_size, DECIMAL, 1000)
696 };
697 let precision = if unit == 1 { 0 } else { 1 };
698 let completed_str = grouped(completed as f64 / unit as f64, precision);
699 let total_str = self.total.map_or_else(
700 || "?".to_string(),
701 |total| grouped((total as i64) as f64 / unit as f64, precision),
702 );
703 format!("{completed_str}/{total_str} {suffix}")
704 }
705}
706
707#[derive(Debug, Clone, Default)]
710pub struct TaskUpdate {
711 pub total: Option<f64>,
712 pub completed: Option<f64>,
713 pub advance: Option<f64>,
714 pub description: Option<String>,
715 pub visible: Option<bool>,
716 pub fields: Vec<(String, FormatValue)>,
718 pub refresh: bool,
720}
721
722impl TaskUpdate {
723 pub fn total(mut self, total: f64) -> Self {
724 self.total = Some(total);
725 self
726 }
727
728 pub fn completed(mut self, completed: f64) -> Self {
729 self.completed = Some(completed);
730 self
731 }
732
733 pub fn advance(mut self, advance: f64) -> Self {
734 self.advance = Some(advance);
735 self
736 }
737
738 pub fn description(mut self, description: impl Into<String>) -> Self {
739 self.description = Some(description.into());
740 self
741 }
742
743 pub fn visible(mut self, visible: bool) -> Self {
744 self.visible = Some(visible);
745 self
746 }
747
748 pub fn refresh(mut self, refresh: bool) -> Self {
751 self.refresh = refresh;
752 self
753 }
754
755 pub fn field(mut self, name: impl Into<String>, value: impl Into<FormatValue>) -> Self {
757 self.fields.push((name.into(), value.into()));
758 self
759 }
760}
761
762pub struct Progress {
764 tasks: Vec<Task>,
765 next_id: usize,
766 columns: Vec<ProgressColumn>,
767 get_time: GetTime,
768 speed_estimate_period: f64,
769 expand: bool,
770 transient: bool,
771 disable: bool,
772}
773
774impl Default for Progress {
775 fn default() -> Self {
776 Progress {
777 tasks: Vec::new(),
778 next_id: 0,
779 columns: Progress::default_columns(),
780 get_time: Arc::new(monotonic),
781 speed_estimate_period: 30.0,
782 expand: false,
783 transient: false,
784 disable: false,
785 }
786 }
787}
788
789impl Progress {
790 pub fn new() -> Self {
791 Progress::default()
792 }
793
794 pub fn default_columns() -> Vec<ProgressColumn> {
797 vec![
798 ProgressColumn::Description,
799 ProgressColumn::Bar,
800 ProgressColumn::Percentage,
801 ProgressColumn::time_remaining(),
802 ]
803 }
804
805 pub fn columns(mut self, columns: Vec<ProgressColumn>) -> Self {
807 self.columns = columns;
808 self
809 }
810
811 pub fn clock(mut self, clock: impl Fn() -> f64 + Send + Sync + 'static) -> Self {
814 self.get_time = Arc::new(clock);
815 for task in &mut self.tasks {
816 task.get_time = self.get_time.clone();
817 }
818 self
819 }
820
821 pub fn expand(mut self, expand: bool) -> Self {
823 self.expand = expand;
824 self
825 }
826
827 pub fn transient(mut self, transient: bool) -> Self {
829 self.transient = transient;
830 self
831 }
832
833 pub fn disable(mut self, disable: bool) -> Self {
836 self.disable = disable;
837 self
838 }
839
840 pub fn speed_estimate_period(mut self, seconds: f64) -> Self {
842 self.speed_estimate_period = seconds;
843 self
844 }
845
846 fn now(&self) -> f64 {
847 (self.get_time)()
848 }
849
850 fn task_mut(&mut self, id: TaskId) -> Option<&mut Task> {
851 self.tasks.iter_mut().find(|task| task.id == id)
852 }
853
854 pub fn add_task(
857 &mut self,
858 description: impl Into<String>,
859 total: impl Into<Option<f64>>,
860 completed: f64,
861 ) -> TaskId {
862 let id = self.push_task(description.into(), total.into(), completed);
863 self.start_task(id);
864 id
865 }
866
867 pub fn add_unstarted_task(
870 &mut self,
871 description: impl Into<String>,
872 total: impl Into<Option<f64>>,
873 completed: f64,
874 ) -> TaskId {
875 self.push_task(description.into(), total.into(), completed)
876 }
877
878 pub fn add_task_with<K: Into<String>, V: Into<FormatValue>>(
881 &mut self,
882 description: impl Into<String>,
883 total: impl Into<Option<f64>>,
884 completed: f64,
885 start: bool,
886 fields: impl IntoIterator<Item = (K, V)>,
887 ) -> TaskId {
888 let id = self.push_task(description.into(), total.into(), completed);
889 if let Some(task) = self.task_mut(id) {
890 task.fields = fields
891 .into_iter()
892 .map(|(name, value)| (name.into(), value.into()))
893 .collect();
894 }
895 if start {
896 self.start_task(id);
897 }
898 id
899 }
900
901 fn push_task(&mut self, description: String, total: Option<f64>, completed: f64) -> TaskId {
902 let id = TaskId(self.next_id);
903 self.next_id += 1;
904 self.tasks.push(Task {
905 id,
906 description,
907 total,
908 completed,
909 visible: true,
910 start_time: None,
911 stop_time: None,
912 finished_time: None,
913 finished_speed: None,
914 samples: VecDeque::new(),
915 fields: BTreeMap::new(),
916 get_time: self.get_time.clone(),
917 });
918 id
919 }
920
921 pub fn task(&self, id: TaskId) -> Option<&Task> {
923 self.tasks.iter().find(|task| task.id == id)
924 }
925
926 pub fn tasks(&self) -> &[Task] {
928 &self.tasks
929 }
930
931 pub fn finished(&self) -> bool {
933 self.tasks.iter().all(Task::finished)
934 }
935
936 pub fn start_task(&mut self, id: TaskId) {
938 let now = self.now();
939 if let Some(task) = self.task_mut(id) {
940 task.start_time.get_or_insert(now);
941 }
942 }
943
944 pub fn stop_task(&mut self, id: TaskId) {
946 let now = self.now();
947 if let Some(task) = self.task_mut(id) {
948 task.start_time.get_or_insert(now);
949 task.stop_time = Some(now);
950 }
951 }
952
953 pub fn update(&mut self, id: TaskId, update: TaskUpdate) {
957 let now = self.now();
958 let period = self.speed_estimate_period;
959 let Some(task) = self.task_mut(id) else {
960 return;
961 };
962 let completed_start = task.completed;
963 if let Some(total) = update.total {
964 if Some(total) != task.total {
965 task.total = Some(total);
966 task.clear_progress();
967 }
968 }
969 if let Some(advance) = update.advance {
970 task.completed += advance;
971 }
972 if let Some(completed) = update.completed {
973 task.completed = completed;
974 }
975 if let Some(description) = update.description {
976 task.description = description;
977 }
978 if let Some(visible) = update.visible {
979 task.visible = visible;
980 }
981 task.fields.extend(update.fields);
982 let update_completed = task.completed - completed_start;
983 let old_sample_time = now - period;
984 while task
985 .samples
986 .front()
987 .is_some_and(|(time, _)| *time < old_sample_time)
988 {
989 task.samples.pop_front();
990 }
991 if update_completed > 0.0 {
992 task.samples.push_back((now, update_completed));
993 if task.samples.len() > MAX_SAMPLES {
994 task.samples.pop_front();
995 }
996 }
997 if task.total.is_some_and(|total| task.completed >= total) && task.finished_time.is_none() {
998 task.finished_time = task.elapsed();
999 }
1000 }
1001
1002 pub fn advance(&mut self, id: TaskId, amount: f64) {
1005 let now = self.now();
1006 let period = self.speed_estimate_period;
1007 let Some(task) = self.task_mut(id) else {
1008 return;
1009 };
1010 let completed_start = task.completed;
1011 task.completed += amount;
1012 let update_completed = task.completed - completed_start;
1013 let old_sample_time = now - period;
1014 while task
1015 .samples
1016 .front()
1017 .is_some_and(|(time, _)| *time < old_sample_time)
1018 {
1019 task.samples.pop_front();
1020 }
1021 while task.samples.len() > MAX_SAMPLES {
1022 task.samples.pop_front();
1023 }
1024 task.samples.push_back((now, update_completed));
1025 if task.samples.len() > MAX_SAMPLES {
1026 task.samples.pop_front();
1027 }
1028 if task.total.is_some_and(|total| task.completed >= total) && task.finished_time.is_none() {
1029 task.finished_time = task.elapsed();
1030 task.finished_speed = task.speed();
1031 }
1032 }
1033
1034 pub fn reset(&mut self, id: TaskId, start: bool, total: Option<f64>, completed: f64) {
1038 let now = self.now();
1039 let Some(task) = self.task_mut(id) else {
1040 return;
1041 };
1042 task.clear_progress();
1043 task.start_time = start.then_some(now);
1044 if let Some(total) = total {
1045 task.total = Some(total);
1046 }
1047 task.completed = completed;
1048 task.finished_time = None;
1049 }
1050
1051 pub fn remove_task(&mut self, id: TaskId) {
1053 self.tasks.retain(|task| task.id != id);
1054 }
1055}
1056
1057impl Progress {
1058 pub fn make_tasks_table(&self) -> Table {
1063 let mut table = Table::grid().padding(0, 1, 0, 1).expand(self.expand);
1064 for column in &self.columns {
1065 table.add_column_with(Text::new(""), column.table_column());
1066 }
1067 for task in self.tasks.iter().filter(|task| task.visible) {
1068 let cells = self
1071 .columns
1072 .iter()
1073 .map(|column| column.table_cell(task))
1074 .collect();
1075 table.add_row_cells(cells);
1076 }
1077 table
1078 }
1079}
1080
1081impl Renderable for Progress {
1082 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
1083 self.make_tasks_table().rich_render(console, options)
1084 }
1085}
1086
1087struct ProgressView(Arc<std::sync::Mutex<Progress>>);
1089
1090impl Renderable for ProgressView {
1091 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
1092 match self.0.lock() {
1093 Ok(progress) => progress.rich_render(console, options),
1094 Err(poisoned) => poisoned.into_inner().rich_render(console, options),
1095 }
1096 }
1097}
1098
1099impl Progress {
1100 pub fn start<W: std::io::Write + Send + 'static>(
1105 self,
1106 console: Console,
1107 writer: W,
1108 refresh_per_second: f64,
1109 ) -> LiveProgress<W> {
1110 if self.disable {
1112 return LiveProgress {
1113 progress: Arc::new(std::sync::Mutex::new(self)),
1114 live: None,
1115 writer: Some(writer),
1116 interactive: true,
1117 holder: std::sync::Mutex::new(None),
1118 };
1119 }
1120 let transient = self.transient;
1121 let interactive = console.is_terminal();
1122 let shared = Arc::new(std::sync::Mutex::new(self));
1123 let live = crate::live::Live::spawn_with(
1124 Box::new(ProgressView(shared.clone())),
1125 console,
1126 writer,
1127 refresh_per_second,
1128 transient,
1129 );
1130 LiveProgress {
1131 progress: shared,
1132 live: Some(live),
1133 writer: None,
1134 interactive,
1135 holder: std::sync::Mutex::new(None),
1136 }
1137 }
1138}
1139
1140pub struct LiveProgress<W: std::io::Write + Send + 'static> {
1143 progress: Arc<std::sync::Mutex<Progress>>,
1144 live: Option<crate::live::AutoLive<W>>,
1145 writer: Option<W>,
1147 interactive: bool,
1149 holder: std::sync::Mutex<Option<std::thread::ThreadId>>,
1153}
1154
1155struct HolderGuard<'a>(&'a std::sync::Mutex<Option<std::thread::ThreadId>>);
1158
1159impl Drop for HolderGuard<'_> {
1160 fn drop(&mut self) {
1161 *self
1162 .0
1163 .lock()
1164 .unwrap_or_else(|poisoned| poisoned.into_inner()) = None;
1165 }
1166}
1167
1168impl<W: std::io::Write + Send + 'static> LiveProgress<W> {
1169 pub fn with<R>(&self, f: impl FnOnce(&mut Progress) -> R) -> R {
1176 let current = std::thread::current().id();
1177 assert!(
1178 !self.held_by(current),
1179 "LiveProgress::with re-entered from inside a `with` closure; \
1180 use the `&mut Progress` it was given instead"
1181 );
1182 let mut progress = match self.progress.lock() {
1183 Ok(progress) => progress,
1184 Err(poisoned) => poisoned.into_inner(),
1185 };
1186 *self
1187 .holder
1188 .lock()
1189 .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(current);
1190 let _holder = HolderGuard(&self.holder);
1191 f(&mut progress)
1192 }
1193
1194 fn held_by(&self, thread: std::thread::ThreadId) -> bool {
1196 *self
1197 .holder
1198 .lock()
1199 .unwrap_or_else(|poisoned| poisoned.into_inner())
1200 == Some(thread)
1201 }
1202
1203 pub fn add_task(
1205 &self,
1206 description: impl Into<String>,
1207 total: impl Into<Option<f64>>,
1208 completed: f64,
1209 ) -> TaskId {
1210 let id = self.with(|progress| progress.add_task(description, total, completed));
1211 self.refresh();
1212 id
1213 }
1214
1215 pub fn reset(&self, id: TaskId, start: bool, total: Option<f64>, completed: f64) {
1217 self.with(|progress| progress.reset(id, start, total, completed));
1218 self.refresh();
1219 }
1220
1221 pub fn advance(&self, id: TaskId, amount: f64) {
1223 self.with(|progress| progress.advance(id, amount));
1224 }
1225
1226 pub fn update(&self, id: TaskId, update: TaskUpdate) {
1229 let refresh = update.refresh;
1230 self.with(|progress| progress.update(id, update));
1231 if refresh {
1232 self.refresh();
1233 }
1234 }
1235
1236 pub fn refresh(&self) {
1243 if let Some(live) = &self.live {
1244 if self.held_by(std::thread::current().id()) {
1245 live.refresh();
1246 } else {
1247 live.refresh_wait();
1248 }
1249 }
1250 }
1251
1252 pub fn track<I: IntoIterator>(
1256 &self,
1257 iter: I,
1258 total: Option<f64>,
1259 description: impl Into<String>,
1260 ) -> Track<'_, I::IntoIter, W> {
1261 let iter = iter.into_iter();
1262 let total = total.or_else(|| match iter.size_hint() {
1263 (lower, Some(upper)) if lower == upper && lower > 0 => Some(lower as f64),
1264 _ => None,
1265 });
1266 let task = self.add_task(description, total, 0.0);
1267 Track {
1268 iter,
1269 progress: self,
1270 task,
1271 pending: false,
1272 }
1273 }
1274
1275 pub fn wrap_read<R: std::io::Read>(
1280 &self,
1281 reader: R,
1282 total: Option<u64>,
1283 task: Option<TaskId>,
1284 description: impl Into<String>,
1285 ) -> std::io::Result<ProgressReader<'_, R, W>> {
1286 let total = total.map(|total| total as f64).or_else(|| {
1287 task.and_then(|task| self.with(|progress| progress.task(task).and_then(Task::total)))
1288 });
1289 let Some(total) = total else {
1290 return Err(std::io::Error::new(
1291 std::io::ErrorKind::InvalidInput,
1292 "unable to get the total number of bytes, please specify 'total'",
1293 ));
1294 };
1295 let task = self.task_for(task, total, description);
1296 Ok(ProgressReader {
1297 reader,
1298 progress: self,
1299 task,
1300 })
1301 }
1302
1303 pub fn open(
1306 &self,
1307 path: impl AsRef<std::path::Path>,
1308 total: Option<u64>,
1309 task: Option<TaskId>,
1310 description: impl Into<String>,
1311 ) -> std::io::Result<ProgressReader<'_, std::fs::File, W>> {
1312 let file = std::fs::File::open(path)?;
1313 let total = match total {
1314 Some(total) => total,
1315 None => file.metadata()?.len(),
1316 };
1317 let task = self.task_for(task, total as f64, description);
1318 Ok(ProgressReader {
1319 reader: file,
1320 progress: self,
1321 task,
1322 })
1323 }
1324
1325 fn task_for(&self, task: Option<TaskId>, total: f64, description: impl Into<String>) -> TaskId {
1327 match task {
1328 Some(task) => {
1329 self.update(task, TaskUpdate::default().total(total));
1330 task
1331 }
1332 None => self.add_task(description, total, 0.0),
1333 }
1334 }
1335
1336 pub fn stop(mut self) -> (Progress, W) {
1339 let writer = match self.live.take() {
1340 Some(live) => {
1341 let mut writer = live.stop();
1342 if !self.interactive {
1344 let _ = writer.write_all(b"\n");
1345 }
1346 writer
1347 }
1348 None => self
1349 .writer
1350 .take()
1351 .expect("a disabled display keeps its writer"),
1352 };
1353 let progress = match Arc::try_unwrap(std::mem::replace(
1354 &mut self.progress,
1355 Arc::new(std::sync::Mutex::new(Progress::new())),
1356 )) {
1357 Ok(mutex) => mutex
1358 .into_inner()
1359 .unwrap_or_else(|poisoned| poisoned.into_inner()),
1360 Err(_) => unreachable!("progress still shared after the live display stopped"),
1362 };
1363 (progress, writer)
1364 }
1365}
1366
1367pub struct ProgressReader<'a, R, W: std::io::Write + Send + 'static> {
1370 reader: R,
1371 progress: &'a LiveProgress<W>,
1372 task: TaskId,
1373}
1374
1375impl<R, W: std::io::Write + Send + 'static> ProgressReader<'_, R, W> {
1376 pub fn task(&self) -> TaskId {
1378 self.task
1379 }
1380
1381 pub fn into_inner(self) -> R {
1383 self.reader
1384 }
1385}
1386
1387impl<R: std::io::Read, W: std::io::Write + Send + 'static> std::io::Read
1388 for ProgressReader<'_, R, W>
1389{
1390 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1391 let count = self.reader.read(buf)?;
1392 self.progress.advance(self.task, count as f64);
1393 Ok(count)
1394 }
1395}
1396
1397impl<R: std::io::BufRead, W: std::io::Write + Send + 'static> std::io::BufRead
1398 for ProgressReader<'_, R, W>
1399{
1400 fn fill_buf(&mut self) -> std::io::Result<&[u8]> {
1401 self.reader.fill_buf()
1402 }
1403
1404 fn consume(&mut self, amount: usize) {
1405 self.reader.consume(amount);
1406 self.progress.advance(self.task, amount as f64);
1407 }
1408}
1409
1410pub struct Track<'a, I: Iterator, W: std::io::Write + Send + 'static> {
1412 iter: I,
1413 progress: &'a LiveProgress<W>,
1414 task: TaskId,
1415 pending: bool,
1418}
1419
1420impl<I: Iterator, W: std::io::Write + Send + 'static> Track<'_, I, W> {
1421 pub fn task(&self) -> TaskId {
1423 self.task
1424 }
1425}
1426
1427impl<I: Iterator, W: std::io::Write + Send + 'static> Iterator for Track<'_, I, W> {
1428 type Item = I::Item;
1429
1430 fn next(&mut self) -> Option<I::Item> {
1431 if std::mem::take(&mut self.pending) {
1432 self.progress.advance(self.task, 1.0);
1433 }
1434 let item = self.iter.next();
1435 if item.is_some() {
1436 self.pending = true;
1437 } else {
1438 self.progress.refresh();
1439 }
1440 item
1441 }
1442
1443 fn size_hint(&self) -> (usize, Option<usize>) {
1444 self.iter.size_hint()
1445 }
1446}
1447
1448pub fn track<I: IntoIterator>(iter: I, description: &str) -> TrackStdout<I::IntoIter> {
1453 let mut columns = Vec::new();
1454 if !description.is_empty() {
1455 columns.push(ProgressColumn::Description);
1456 }
1457 columns.extend([
1458 ProgressColumn::Bar,
1459 ProgressColumn::TaskProgress { show_speed: true },
1460 ProgressColumn::TimeRemaining(TimeRemainingColumn::new(false, true)),
1461 ]);
1462 let iter = iter.into_iter();
1463 let total = match iter.size_hint() {
1464 (lower, Some(upper)) if lower == upper && lower > 0 => Some(lower as f64),
1465 _ => None,
1466 };
1467 let live = Progress::new()
1468 .columns(columns)
1469 .start(Console::new(), std::io::stdout(), 10.0);
1470 let task = live.add_task(description, total, 0.0);
1471 TrackStdout {
1472 iter,
1473 live: Some(live),
1474 task,
1475 pending: false,
1476 }
1477}
1478
1479pub struct TrackStdout<I: Iterator> {
1481 iter: I,
1482 live: Option<LiveProgress<std::io::Stdout>>,
1483 task: TaskId,
1484 pending: bool,
1485}
1486
1487impl<I: Iterator> Iterator for TrackStdout<I> {
1488 type Item = I::Item;
1489
1490 fn next(&mut self) -> Option<I::Item> {
1491 let live = self.live.as_ref()?;
1492 if std::mem::take(&mut self.pending) {
1493 live.advance(self.task, 1.0);
1494 }
1495 match self.iter.next() {
1496 Some(item) => {
1497 self.pending = true;
1498 Some(item)
1499 }
1500 None => {
1501 if let Some(live) = self.live.take() {
1502 live.stop();
1503 }
1504 None
1505 }
1506 }
1507 }
1508}
1509
1510impl<I: Iterator> Drop for TrackStdout<I> {
1511 fn drop(&mut self) {
1512 if let Some(live) = self.live.take() {
1513 live.stop();
1514 }
1515 }
1516}
1517
1518fn whole_number(value: f64) -> FormatValue {
1520 if value.fract() == 0.0 && value.abs() < 9_007_199_254_740_992.0 {
1521 FormatValue::Int(value as i64)
1522 } else {
1523 FormatValue::Float(value)
1524 }
1525}
1526
1527#[cfg(test)]
1528mod tests {
1529 use super::*;
1530 use crate::color::ColorSystem;
1531
1532 fn render(progress: &Progress) -> String {
1533 Console::builder()
1534 .force_terminal(true)
1535 .color_system(Some(ColorSystem::Truecolor))
1536 .width(50)
1537 .no_color(false)
1538 .build()
1539 .render_to_string(progress)
1540 }
1541
1542 #[test]
1543 fn three_tasks_match_upstream() {
1544 let mut progress = Progress::new().columns(vec![
1546 ProgressColumn::Description,
1547 ProgressColumn::Bar,
1548 ProgressColumn::Percentage,
1549 ]);
1550 progress.add_task("Downloading", 100.0, 50.0);
1551 progress.add_task("Processing", 100.0, 100.0);
1552 progress.add_task("Waiting", 100.0, 0.0);
1553 let expected = concat!(
1554 "Downloading \x1b[38;2;249;38;114m━━━━━━━━━━━━━━━━\x1b[0m",
1555 "\x1b[38;2;249;38;114m╸\x1b[0m\x1b[38;5;237m━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 50%\x1b[0m\n",
1556 "Processing \x1b[38;2;114;156;31m",
1557 "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m100%\x1b[0m\n",
1558 "Waiting \x1b[38;5;237m",
1559 "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 0%\x1b[0m",
1560 );
1561 assert_eq!(render(&progress), expected);
1562 }
1563
1564 #[test]
1565 fn download_text_matches_upstream() {
1566 let dl = |completed: f64, total: f64| {
1568 let mut progress = Progress::new();
1569 let id = progress.add_task("", total, completed);
1570 progress.task(id).unwrap().download_text(false)
1571 };
1572 assert_eq!(dl(500.0, 1000.0), "0.5/1.0 kB");
1573 assert_eq!(dl(500.0, 999.0), "500/999 bytes");
1574 assert_eq!(dl(1_500_000.0, 3_000_000.0), "1.5/3.0 MB");
1575 assert_eq!(dl(0.0, 1024.0), "0.0/1.0 kB");
1576 assert_eq!(dl(2_500_000_000.0, 10_000_000_000.0), "2.5/10.0 GB");
1577 assert_eq!(dl(250.0, 250.0), "250/250 bytes");
1578 }
1579
1580 #[test]
1581 fn download_column_in_grid_matches_upstream() {
1582 let mut progress = Progress::new().columns(vec![
1584 ProgressColumn::Description,
1585 ProgressColumn::Bar,
1586 ProgressColumn::Download,
1587 ]);
1588 progress.add_task("File", 1000.0, 500.0);
1589 let expected = concat!(
1590 "File \x1b[38;2;249;38;114m━━━━━━━━━━━━━━━━━\x1b[0m\x1b[38;5;237m╺\x1b[0m",
1591 "\x1b[38;5;237m━━━━━━━━━━━━━━━━\x1b[0m \x1b[32m0.5/1.0 kB\x1b[0m",
1592 );
1593 assert_eq!(render(&progress), expected);
1594 }
1595
1596 #[test]
1597 fn custom_columns_with_mofn_match_upstream() {
1598 let mut progress = Progress::new().columns(vec![
1601 ProgressColumn::Description,
1602 ProgressColumn::Bar,
1603 ProgressColumn::MofN,
1604 ]);
1605 progress.add_task("A", 5.0, 3.0);
1606 progress.add_task("B", 100.0, 50.0);
1607 let console = Console::builder()
1608 .force_terminal(true)
1609 .color_system(Some(ColorSystem::Truecolor))
1610 .width(40)
1611 .no_color(false)
1612 .build();
1613 let expected = concat!(
1614 "A \x1b[38;2;249;38;114m━━━━━━━━━━━━━━━━━━\x1b[0m\x1b[38;5;237m╺\x1b[0m",
1615 "\x1b[38;5;237m━━━━━━━━━━━\x1b[0m \x1b[32m3/5 \x1b[0m\n",
1616 "B \x1b[38;2;249;38;114m━━━━━━━━━━━━━━━\x1b[0m\x1b[38;5;237m╺\x1b[0m",
1617 "\x1b[38;5;237m━━━━━━━━━━━━━━\x1b[0m \x1b[32m 50/100\x1b[0m",
1618 );
1619 assert_eq!(console.render_to_string(&progress), expected);
1620 }
1621
1622 fn live(columns: Vec<ProgressColumn>) -> LiveProgress<Vec<u8>> {
1623 let console = Console::builder()
1624 .force_terminal(true)
1625 .color_system(Some(ColorSystem::Truecolor))
1626 .width(40)
1627 .build();
1628 Progress::new()
1629 .columns(columns)
1630 .clock(|| 0.0)
1631 .start(console, Vec::new(), 1e-9)
1632 }
1633
1634 fn within_deadline<R: Send + 'static>(f: impl FnOnce() -> R + Send + 'static) -> R {
1637 let (done, wait) = std::sync::mpsc::channel();
1638 std::thread::spawn(move || {
1639 let _ = done.send(std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)));
1640 });
1641 match wait.recv_timeout(std::time::Duration::from_secs(10)) {
1642 Ok(Ok(value)) => value,
1643 Ok(Err(payload)) => std::panic::resume_unwind(payload),
1644 Err(_) => panic!("deadlocked: did not finish within 10s"),
1645 }
1646 }
1647
1648 #[test]
1649 fn refresh_inside_with_does_not_deadlock() {
1650 let output = within_deadline(|| {
1653 let live = live(vec![ProgressColumn::Description, ProgressColumn::MofN]);
1654 live.with(|progress| {
1655 let task = progress.add_task("inside", Some(2.0), 1.0);
1656 live.refresh();
1657 task
1658 });
1659 live.refresh();
1660 String::from_utf8(live.stop().1).unwrap()
1661 });
1662 assert!(
1663 output.ends_with("inside \x1b[32m1/2\x1b[0m\n\x1b[?25h"),
1664 "{output:?}"
1665 );
1666 }
1667
1668 #[test]
1669 fn nested_with_panics_instead_of_deadlocking() {
1670 let result = within_deadline(|| {
1671 let live = live(vec![ProgressColumn::Description]);
1672 let nested = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1673 live.with(|_| live.add_task("nested", None, 0.0))
1674 }));
1675 let task = live.add_task("after", None, 0.0);
1677 live.stop();
1678 (nested.is_err(), task)
1679 });
1680 assert!(result.0, "a nested `with` must be refused, not deadlock");
1681 }
1682
1683 #[test]
1684 fn track_counts_each_item_after_its_loop_body() {
1685 let live = live(vec![ProgressColumn::Description, ProgressColumn::MofN]);
1686 let mut seen = Vec::new();
1687 let tracked = live.track(vec!['a', 'b', 'c'], None, "letters");
1688 let task = tracked.task();
1689 for item in tracked {
1690 let completed = live.with(|progress| progress.task(task).unwrap().completed());
1693 seen.push((item, completed));
1694 }
1695 assert_eq!(seen, vec![('a', 0.0), ('b', 1.0), ('c', 2.0)]);
1696 let (progress, bytes) = live.stop();
1697 let task = progress.task(task).unwrap();
1698 assert_eq!((task.total(), task.completed()), (Some(3.0), 3.0));
1699 let output = String::from_utf8(bytes).unwrap();
1700 assert!(
1701 output.ends_with("letters \x1b[32m3/3\x1b[0m\n\x1b[?25h"),
1702 "{output:?}"
1703 );
1704 }
1705
1706 #[test]
1707 fn track_leaves_an_iterator_of_unknown_length_indeterminate() {
1708 let live = live(vec![ProgressColumn::MofN]);
1709 let task = {
1710 let mut tracked = live.track((0..10).filter(|n| n % 3 == 0), None, "");
1711 let task = tracked.task();
1712 assert_eq!(tracked.by_ref().count(), 4);
1713 task
1714 };
1715 let with_total = {
1716 let mut tracked = live.track(0..2, Some(5.0), "");
1717 tracked.by_ref().for_each(drop);
1718 tracked.task()
1719 };
1720 let (progress, _) = live.stop();
1721 assert_eq!(progress.task(task).unwrap().total(), None);
1722 assert_eq!(progress.task(task).unwrap().completed(), 4.0);
1723 assert_eq!(progress.task(with_total).unwrap().total(), Some(5.0));
1724 }
1725
1726 fn quiet_console() -> Console {
1727 Console::builder().force_terminal(false).width(40).build()
1728 }
1729
1730 #[test]
1731 fn wrap_read_advances_by_the_bytes_read() {
1732 use std::io::Read;
1733 let live = Progress::new()
1734 .disable(true)
1735 .start(quiet_console(), Vec::new(), 1.0);
1736 let mut reader = live
1737 .wrap_read(&b"hello world"[..], Some(11), None, "Reading...")
1738 .expect("total given");
1739 let task = reader.task();
1740 let mut buf = [0u8; 4];
1741 reader.read_exact(&mut buf).unwrap();
1742 assert_eq!(live.with(|p| p.task(task).unwrap().completed()), 4.0);
1743 let mut rest = Vec::new();
1744 reader.read_to_end(&mut rest).unwrap();
1745 assert!(live.with(|p| p.task(task).unwrap().finished()));
1746 let (_, out) = live.stop();
1747 assert!(out.is_empty(), "a disabled display writes nothing");
1748 }
1749
1750 #[test]
1751 fn wrap_read_needs_a_total() {
1752 let live = Progress::new()
1753 .disable(true)
1754 .start(quiet_console(), Vec::new(), 1.0);
1755 let err = live
1756 .wrap_read(&b""[..], None, None, "x")
1757 .err()
1758 .expect("no total");
1759 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
1760 let task = live.add_task("sized", 5.0, 0.0);
1761 assert!(live.wrap_read(&b""[..], None, Some(task), "x").is_ok());
1762 }
1763
1764 #[test]
1765 fn open_takes_the_file_size_as_total() {
1766 use std::io::Read;
1767 let path = std::env::temp_dir().join(format!("rs-rich-open-{}", std::process::id()));
1768 std::fs::write(&path, b"0123456789").unwrap();
1769 let live = Progress::new()
1770 .disable(true)
1771 .start(quiet_console(), Vec::new(), 1.0);
1772 let mut reader = live.open(&path, None, None, "Reading...").unwrap();
1773 let task = reader.task();
1774 assert_eq!(live.with(|p| p.task(task).unwrap().total()), Some(10.0));
1775 std::io::copy(&mut reader, &mut std::io::sink()).unwrap();
1776 assert_eq!(live.with(|p| p.task(task).unwrap().completed()), 10.0);
1777 let _ = reader.read(&mut [0u8; 1]);
1778 drop(reader);
1779 std::fs::remove_file(path).unwrap();
1780 }
1781}