1use std::collections::{HashMap, VecDeque};
6use std::fs::File;
7use std::io::Stdout;
8use std::io::{self, BufRead, Read, Seek, SeekFrom};
9use std::path::Path;
10use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
11use std::sync::{Arc, Mutex};
12use std::thread;
13use std::time::{Duration, Instant};
14
15use crate::console::ConsoleOptions;
16use crate::console::OverflowMethod;
17use crate::filesize;
18use crate::live::{Live, LiveOptions};
19use crate::progress_bar::ProgressBar;
20use crate::spinner::Spinner;
21use crate::style::Style;
22use crate::table::{Column, Row, Table};
23use crate::text::Text;
24use crate::{Console, JustifyMethod, Renderable, Segments};
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub struct TaskID(pub usize);
28
29#[derive(Debug, Clone)]
30struct ProgressSample {
31 timestamp: f64,
32 completed: f64,
33}
34
35#[derive(Debug, Clone)]
36pub struct ProgressTask {
37 pub id: TaskID,
38 pub description: String,
39 pub total: Option<f64>,
40 pub completed: f64,
41 pub visible: bool,
42 pub fields: HashMap<String, String>,
43
44 pub finished_time: Option<f64>,
45 pub finished_speed: Option<f64>,
46
47 start_time: Option<f64>,
48 stop_time: Option<f64>,
49 progress: VecDeque<ProgressSample>,
50}
51
52impl ProgressTask {
53 fn started(&self) -> bool {
54 self.start_time.is_some()
55 }
56
57 fn finished(&self) -> bool {
58 self.finished_time.is_some()
59 }
60
61 fn remaining(&self) -> Option<f64> {
62 self.total.map(|t| t - self.completed)
63 }
64
65 fn elapsed(&self, now: f64) -> Option<f64> {
66 let start = self.start_time?;
67 if let Some(stop) = self.stop_time {
68 return Some(stop - start);
69 }
70 Some(now - start)
71 }
72
73 fn percentage(&self) -> f64 {
74 let Some(total) = self.total else { return 0.0 };
75 if total <= 0.0 {
76 return 0.0;
77 }
78 ((self.completed / total) * 100.0).clamp(0.0, 100.0)
79 }
80
81 fn speed(&self) -> Option<f64> {
82 if !self.started() {
83 return None;
84 }
85 let first = self.progress.front()?;
86 let last = self.progress.back()?;
87 let total_time = last.timestamp - first.timestamp;
88 if total_time == 0.0 {
89 return None;
90 }
91 let total_completed: f64 = self.progress.iter().skip(1).map(|s| s.completed).sum();
93 Some(total_completed / total_time)
94 }
95
96 fn time_remaining(&self) -> Option<f64> {
97 if self.finished() {
98 return Some(0.0);
99 }
100 let speed = self.speed()?;
101 if speed <= 0.0 {
102 return None;
103 }
104 let remaining = self.remaining()?;
105 if remaining <= 0.0 {
106 return Some(0.0);
107 }
108 Some((remaining / speed).ceil())
109 }
110}
111
112pub trait ProgressColumn: Send + Sync {
113 fn table_column(&self) -> Column;
114 fn render(
115 &self,
116 task: &ProgressTask,
117 now: f64,
118 options: &ConsoleOptions,
119 ) -> Box<dyn Renderable + Send + Sync>;
120 fn max_refresh(&self) -> Option<Duration> {
121 None
122 }
123}
124
125#[derive(Debug)]
126pub struct SpinnerColumn {
127 spinner: Spinner,
128 finished_text: Text,
129 start_time: Mutex<Option<f64>>,
130 style_name: String,
131}
132
133impl SpinnerColumn {
134 pub fn new() -> Self {
135 Self::with_spinner("dots")
136 }
137
138 pub fn with_spinner(name: &str) -> Self {
139 let spinner = Spinner::new(name).unwrap_or_else(|_| Spinner::new("dots").unwrap());
140 Self {
141 spinner,
142 finished_text: Text::plain(" "),
143 start_time: Mutex::new(None),
144 style_name: "progress.spinner".to_string(),
145 }
146 }
147
148 pub fn with_style_name(mut self, style: &str) -> Self {
149 self.style_name = style.to_string();
150 self
151 }
152}
153
154impl ProgressColumn for SpinnerColumn {
155 fn table_column(&self) -> Column {
156 Column::new().no_wrap(true)
157 }
158
159 fn render(
160 &self,
161 task: &ProgressTask,
162 now: f64,
163 options: &ConsoleOptions,
164 ) -> Box<dyn Renderable + Send + Sync> {
165 if task.finished() {
166 return Box::new(self.finished_text.clone());
167 }
168 let mut start = self
169 .start_time
170 .lock()
171 .expect("spinner start mutex poisoned");
172 let start_time = *start.get_or_insert(now);
173 let style = options.get_style(&self.style_name);
174 Box::new(self.spinner.render_at(now, Some(start_time), style))
175 }
176}
177
178#[derive(Debug, Clone)]
179pub struct TextColumn {
180 text_format: String,
181 style_name: String,
182 justify: JustifyMethod,
183 markup: bool,
184}
185
186impl TextColumn {
187 pub fn new(text_format: &str) -> Self {
188 Self {
189 text_format: text_format.to_string(),
190 style_name: "none".to_string(),
191 justify: JustifyMethod::Left,
192 markup: true,
193 }
194 }
195
196 pub fn with_style_name(mut self, style: &str) -> Self {
197 self.style_name = style.to_string();
198 self
199 }
200
201 pub fn with_justify(mut self, justify: JustifyMethod) -> Self {
202 self.justify = justify;
203 self
204 }
205
206 pub fn with_markup(mut self, markup: bool) -> Self {
207 self.markup = markup;
208 self
209 }
210}
211
212impl ProgressColumn for TextColumn {
213 fn table_column(&self) -> Column {
214 Column::new().no_wrap(true).justify(self.justify)
216 }
217
218 fn render(
219 &self,
220 task: &ProgressTask,
221 now: f64,
222 options: &ConsoleOptions,
223 ) -> Box<dyn Renderable + Send + Sync> {
224 let formatted = format_task_template(&self.text_format, task, now);
225 let mut text = if self.markup {
226 Text::from_markup(&formatted, true).unwrap_or_else(|_| Text::plain(&formatted))
227 } else {
228 Text::plain(&formatted)
229 };
230 if self.style_name != "none" {
231 if let Some(style) = options.get_style(&self.style_name) {
232 text.stylize_before(style, 0, None);
233 }
234 }
235 Box::new(text)
236 }
237}
238
239#[derive(Debug, Clone)]
240pub struct BarColumn {
241 bar_width: Option<usize>,
242 style: String,
243 complete_style: String,
244 finished_style: String,
245 pulse_style: String,
246}
247
248impl BarColumn {
249 pub fn new() -> Self {
250 Self {
251 bar_width: Some(40),
253 style: "bar.back".to_string(),
254 complete_style: "bar.complete".to_string(),
255 finished_style: "bar.finished".to_string(),
256 pulse_style: "bar.pulse".to_string(),
257 }
258 }
259
260 pub fn with_bar_width(mut self, width: Option<usize>) -> Self {
261 self.bar_width = width;
262 self
263 }
264}
265
266impl ProgressColumn for BarColumn {
267 fn table_column(&self) -> Column {
268 Column::new()
269 }
270
271 fn render(
272 &self,
273 task: &ProgressTask,
274 now: f64,
275 _options: &ConsoleOptions,
276 ) -> Box<dyn Renderable + Send + Sync> {
277 let mut bar = ProgressBar::new();
278 bar.total = task.total.map(|t| t.max(0.0));
279 bar.completed = task.completed.max(0.0);
280 bar.width = self.bar_width.map(|w| w.max(1));
281 bar.pulse = !task.started();
282 bar.animation_time = Some(now);
283 bar.style = self.style.clone();
284 bar.complete_style = self.complete_style.clone();
285 bar.finished_style = self.finished_style.clone();
286 bar.pulse_style = self.pulse_style.clone();
287 Box::new(bar)
288 }
289}
290
291#[derive(Debug, Clone)]
292pub struct TaskProgressColumn {
293 show_speed: bool,
294}
295
296impl TaskProgressColumn {
297 pub fn new(show_speed: bool) -> Self {
298 Self { show_speed }
299 }
300
301 fn render_speed(speed: Option<f64>) -> Text {
302 let Some(speed) = speed else {
303 return Text::plain("");
304 };
305 let speed = speed.max(0.0);
306 let (unit, suffix) = filesize::pick_unit_and_suffix(
307 speed as u64,
308 &["", "×10³", "×10⁶", "×10⁹", "×10¹²"],
309 1000,
310 );
311 let data_speed = speed / unit as f64;
312 Text::from_markup(
313 &format!("[progress.percentage]{data_speed:.1}{suffix} it/s"),
314 true,
315 )
316 .unwrap_or_else(|_| Text::plain(format!("{data_speed:.1}{suffix} it/s")))
317 }
318}
319
320impl ProgressColumn for TaskProgressColumn {
321 fn table_column(&self) -> Column {
322 Column::new().no_wrap(true)
323 }
324
325 fn render(
326 &self,
327 task: &ProgressTask,
328 _now: f64,
329 _options: &ConsoleOptions,
330 ) -> Box<dyn Renderable + Send + Sync> {
331 if task.total.is_none() {
333 if self.show_speed {
334 return Box::new(Self::render_speed(
335 task.finished_speed.or_else(|| task.speed()),
336 ));
337 }
338 return Box::new(Text::plain(""));
339 }
340 let percent = task.percentage();
341 Box::new(
342 Text::from_markup(&format!("[progress.percentage]{percent:>3.0}%"), true)
343 .unwrap_or_else(|_| Text::plain(format!("{percent:>3.0}%"))),
344 )
345 }
346}
347
348#[derive(Debug, Clone)]
349pub struct TimeRemainingColumn {
350 pub compact: bool,
351 pub elapsed_when_finished: bool,
352}
353
354impl TimeRemainingColumn {
355 pub fn new(elapsed_when_finished: bool) -> Self {
356 Self {
357 compact: false,
358 elapsed_when_finished,
359 }
360 }
361
362 pub fn with_compact(mut self, compact: bool) -> Self {
363 self.compact = compact;
364 self
365 }
366}
367
368impl ProgressColumn for TimeRemainingColumn {
369 fn table_column(&self) -> Column {
370 Column::new().no_wrap(true)
371 }
372
373 fn max_refresh(&self) -> Option<Duration> {
374 Some(Duration::from_secs_f64(0.5))
376 }
377
378 fn render(
379 &self,
380 task: &ProgressTask,
381 _now: f64,
382 _options: &ConsoleOptions,
383 ) -> Box<dyn Renderable + Send + Sync> {
384 let (task_time, style) = if task.finished() && self.elapsed_when_finished {
385 (task.finished_time, "progress.elapsed")
386 } else {
387 (task.time_remaining(), "progress.remaining")
388 };
389
390 if task.total.is_none() {
391 return Box::new(Text::plain(""));
392 }
393
394 let placeholder = if self.compact { "--:--" } else { "-:--:--" };
395 let Some(task_time) = task_time else {
396 return Box::new(
397 Text::from_markup(&format!("[{style}]{placeholder}"), true)
398 .unwrap_or_else(|_| Text::plain(placeholder)),
399 );
400 };
401
402 let secs = task_time.max(0.0) as u64;
403 let minutes_total = secs / 60;
404 let seconds = secs % 60;
405 let hours = minutes_total / 60;
406 let minutes = minutes_total % 60;
407
408 let formatted = if self.compact && hours == 0 {
409 format!("{minutes:02}:{seconds:02}")
410 } else {
411 format!("{hours}:{minutes:02}:{seconds:02}")
412 };
413
414 Box::new(
415 Text::from_markup(&format!("[{style}]{formatted}"), true)
416 .unwrap_or_else(|_| Text::plain(formatted)),
417 )
418 }
419}
420
421#[derive(Debug, Clone)]
422pub struct TimeElapsedColumn;
423
424impl TimeElapsedColumn {
425 pub fn new() -> Self {
426 Self
427 }
428}
429
430impl ProgressColumn for TimeElapsedColumn {
431 fn table_column(&self) -> Column {
432 Column::new().no_wrap(true)
433 }
434
435 fn render(
436 &self,
437 task: &ProgressTask,
438 now: f64,
439 _options: &ConsoleOptions,
440 ) -> Box<dyn Renderable + Send + Sync> {
441 let elapsed = if task.finished() {
442 task.finished_time
443 } else {
444 task.elapsed(now)
445 };
446 let Some(elapsed) = elapsed else {
447 return Box::new(
448 Text::from_markup("[progress.elapsed]-:--:--", true)
449 .unwrap_or_else(|_| Text::plain("-:--:--")),
450 );
451 };
452 let secs = elapsed.max(0.0) as u64;
453 let hours = secs / 3600;
454 let minutes = (secs % 3600) / 60;
455 let seconds = secs % 60;
456 Box::new(
457 Text::from_markup(
458 &format!("[progress.elapsed]{hours}:{minutes:02}:{seconds:02}"),
459 true,
460 )
461 .unwrap_or_else(|_| Text::plain(format!("{hours}:{minutes:02}:{seconds:02}"))),
462 )
463 }
464}
465
466#[derive(Debug, Clone)]
467pub struct FileSizeColumn;
468
469impl FileSizeColumn {
470 pub fn new() -> Self {
471 Self
472 }
473}
474
475impl ProgressColumn for FileSizeColumn {
476 fn table_column(&self) -> Column {
477 Column::new().no_wrap(true)
478 }
479
480 fn render(
481 &self,
482 task: &ProgressTask,
483 _now: f64,
484 _options: &ConsoleOptions,
485 ) -> Box<dyn Renderable + Send + Sync> {
486 let data_size = filesize::decimal(task.completed.max(0.0) as u64);
487 Box::new(
488 Text::from_markup(&format!("[progress.filesize]{data_size}"), true)
489 .unwrap_or_else(|_| Text::plain(data_size)),
490 )
491 }
492}
493
494#[derive(Debug, Clone)]
495pub struct TotalFileSizeColumn;
496
497impl TotalFileSizeColumn {
498 pub fn new() -> Self {
499 Self
500 }
501}
502
503impl ProgressColumn for TotalFileSizeColumn {
504 fn table_column(&self) -> Column {
505 Column::new().no_wrap(true)
506 }
507
508 fn render(
509 &self,
510 task: &ProgressTask,
511 _now: f64,
512 _options: &ConsoleOptions,
513 ) -> Box<dyn Renderable + Send + Sync> {
514 let data_size = task
515 .total
516 .map(|t| filesize::decimal(t.max(0.0) as u64))
517 .unwrap_or_default();
518 Box::new(
519 Text::from_markup(&format!("[progress.filesize.total]{data_size}"), true)
520 .unwrap_or_else(|_| Text::plain(data_size)),
521 )
522 }
523}
524
525#[derive(Debug, Clone)]
526pub struct MofNCompleteColumn {
527 separator: String,
528}
529
530impl MofNCompleteColumn {
531 pub fn new() -> Self {
532 Self {
533 separator: "/".to_string(),
534 }
535 }
536
537 pub fn with_separator(mut self, separator: &str) -> Self {
538 self.separator = separator.to_string();
539 self
540 }
541}
542
543impl ProgressColumn for MofNCompleteColumn {
544 fn table_column(&self) -> Column {
545 Column::new().no_wrap(true)
546 }
547
548 fn render(
549 &self,
550 task: &ProgressTask,
551 _now: f64,
552 _options: &ConsoleOptions,
553 ) -> Box<dyn Renderable + Send + Sync> {
554 let completed = task.completed.max(0.0) as u64;
555 let total = task.total.map(|t| t.max(0.0) as u64);
556 let total_str = total
557 .map(|t| t.to_string())
558 .unwrap_or_else(|| "?".to_string());
559 let total_width = total_str.len();
560 let completed_str = format!("{completed:width$}", width = total_width);
561 let text = format!("{completed_str}{}{}", self.separator, total_str);
562 Box::new(
563 Text::from_markup(&format!("[progress.download]{text}"), true)
564 .unwrap_or_else(|_| Text::plain(text)),
565 )
566 }
567}
568
569#[derive(Debug, Clone)]
570pub struct DownloadColumn {
571 pub binary_units: bool,
572}
573
574impl DownloadColumn {
575 pub fn new() -> Self {
576 Self {
577 binary_units: false,
578 }
579 }
580
581 pub fn with_binary_units(mut self, binary_units: bool) -> Self {
582 self.binary_units = binary_units;
583 self
584 }
585}
586
587impl ProgressColumn for DownloadColumn {
588 fn table_column(&self) -> Column {
589 Column::new().no_wrap(true)
590 }
591
592 fn render(
593 &self,
594 task: &ProgressTask,
595 _now: f64,
596 _options: &ConsoleOptions,
597 ) -> Box<dyn Renderable + Send + Sync> {
598 let completed = task.completed.max(0.0) as u64;
599 let calc_base = task.total.map(|t| t.max(0.0) as u64).unwrap_or(completed);
600 let (unit, suffix) = if self.binary_units {
601 filesize::pick_unit_and_suffix(
602 calc_base,
603 &[
604 "bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB",
605 ],
606 1024,
607 )
608 } else {
609 filesize::pick_unit_and_suffix(
610 calc_base,
611 &["bytes", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"],
612 1000,
613 )
614 };
615 let precision = if unit == 1 { 0 } else { 1 };
616 let completed_ratio = completed as f64 / unit as f64;
617 let completed_str = if precision == 0 {
618 format!("{completed_ratio:.0}")
619 } else {
620 format!("{completed_ratio:.1}")
621 };
622
623 let total_str = if let Some(total) = task.total {
624 let total = total.max(0.0) as u64;
625 let total_ratio = total as f64 / unit as f64;
626 if precision == 0 {
627 format!("{total_ratio:.0}")
628 } else {
629 format!("{total_ratio:.1}")
630 }
631 } else {
632 "?".to_string()
633 };
634
635 let download_status = format!("{completed_str}/{total_str} {suffix}");
636 Box::new(
637 Text::from_markup(&format!("[progress.download]{download_status}"), true)
638 .unwrap_or_else(|_| Text::plain(download_status)),
639 )
640 }
641}
642
643#[derive(Debug, Clone)]
644pub struct TransferSpeedColumn;
645
646impl TransferSpeedColumn {
647 pub fn new() -> Self {
648 Self
649 }
650}
651
652impl ProgressColumn for TransferSpeedColumn {
653 fn table_column(&self) -> Column {
654 Column::new().no_wrap(true)
655 }
656
657 fn render(
658 &self,
659 task: &ProgressTask,
660 _now: f64,
661 _options: &ConsoleOptions,
662 ) -> Box<dyn Renderable + Send + Sync> {
663 let speed = task.finished_speed.or_else(|| task.speed());
664 let Some(speed) = speed else {
665 return Box::new(
666 Text::from_markup("[progress.data.speed]?", true)
667 .unwrap_or_else(|_| Text::plain("?")),
668 );
669 };
670 let data_speed = filesize::decimal(speed.max(0.0) as u64);
671 Box::new(
672 Text::from_markup(&format!("[progress.data.speed]{data_speed}/s"), true)
673 .unwrap_or_else(|_| Text::plain(format!("{data_speed}/s"))),
674 )
675 }
676}
677
678pub struct RenderableColumn {
683 render_fn: Box<dyn Fn(&ProgressTask) -> Box<dyn Renderable + Send + Sync> + Send + Sync>,
684 no_wrap: bool,
685 justify: JustifyMethod,
686}
687
688impl RenderableColumn {
689 pub fn new(
694 f: impl Fn(&ProgressTask) -> Box<dyn Renderable + Send + Sync> + Send + Sync + 'static,
695 ) -> Self {
696 Self {
697 render_fn: Box::new(f),
698 no_wrap: false,
699 justify: JustifyMethod::Left,
700 }
701 }
702
703 pub fn with_no_wrap(mut self, no_wrap: bool) -> Self {
705 self.no_wrap = no_wrap;
706 self
707 }
708
709 pub fn with_justify(mut self, justify: JustifyMethod) -> Self {
711 self.justify = justify;
712 self
713 }
714}
715
716impl std::fmt::Debug for RenderableColumn {
717 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
718 f.debug_struct("RenderableColumn")
719 .field("no_wrap", &self.no_wrap)
720 .field("justify", &self.justify)
721 .finish_non_exhaustive()
722 }
723}
724
725impl ProgressColumn for RenderableColumn {
726 fn table_column(&self) -> Column {
727 Column::new().no_wrap(self.no_wrap).justify(self.justify)
728 }
729
730 fn render(
731 &self,
732 task: &ProgressTask,
733 _now: f64,
734 _options: &ConsoleOptions,
735 ) -> Box<dyn Renderable + Send + Sync> {
736 (self.render_fn)(task)
737 }
738}
739
740struct ProgressState {
741 start: Instant,
742 tasks: HashMap<TaskID, ProgressTask>,
743 order: Vec<TaskID>,
744 next_id: usize,
745 speed_estimate_period: f64,
746 expand: bool,
747 cell_cache: HashMap<(TaskID, usize), (f64, Segments)>,
749}
750
751impl ProgressState {
752 fn now(&self) -> f64 {
753 self.start.elapsed().as_secs_f64()
754 }
755}
756
757#[derive(Clone)]
758struct ProgressRenderable {
759 state: Arc<Mutex<ProgressState>>,
760 columns: Arc<Vec<Box<dyn ProgressColumn>>>,
761}
762
763impl Renderable for ProgressRenderable {
764 fn render(&self, console: &Console, options: &ConsoleOptions) -> Segments {
765 let (tasks, now, expand, cache_snapshot) = {
766 let state = self.state.lock().expect("progress state mutex poisoned");
767 let now = state.now();
768 let tasks: Vec<ProgressTask> = state
769 .order
770 .iter()
771 .filter_map(|id| state.tasks.get(id).cloned())
772 .collect();
773 (tasks, now, state.expand, state.cell_cache.clone())
774 };
775
776 let mut table = Table::grid().with_padding(1, 1).with_expand(expand);
777 for col in self.columns.iter() {
778 table.add_column(col.table_column());
779 }
780
781 let mut new_cache: HashMap<(TaskID, usize), (f64, Segments)> = cache_snapshot;
782
783 for task in tasks.iter().filter(|t| t.visible) {
784 let mut row_cells: Vec<Box<dyn Renderable + Send + Sync>> =
785 Vec::with_capacity(self.columns.len());
786 for (col_index, col) in self.columns.iter().enumerate() {
787 let segs = if let Some(max_refresh) = col.max_refresh() {
788 if task.completed <= 0.0 {
791 let key = (task.id, col_index);
792 if let Some((last_ts, cached)) = new_cache.get(&key) {
793 if now - *last_ts < max_refresh.as_secs_f64() {
794 cached.clone()
795 } else {
796 let renderable = col.render(task, now, options);
797 let segs = renderable.render(console, options);
798 new_cache.insert(key, (now, segs.clone()));
799 segs
800 }
801 } else {
802 let renderable = col.render(task, now, options);
803 let segs = renderable.render(console, options);
804 new_cache.insert(key, (now, segs.clone()));
805 segs
806 }
807 } else {
808 let renderable = col.render(task, now, options);
809 renderable.render(console, options)
810 }
811 } else {
812 let renderable = col.render(task, now, options);
813 renderable.render(console, options)
814 };
815 row_cells.push(Box::new(SegmentsCell::new(segs)));
816 }
817 table.add_row(Row::new(row_cells));
818 }
819
820 {
822 let mut state = self.state.lock().expect("progress state mutex poisoned");
823 state.cell_cache = new_cache;
824 }
825
826 table.render(console, options)
827 }
828}
829
830#[derive(Clone)]
831struct SegmentsCell {
832 segments: Segments,
833}
834
835impl SegmentsCell {
836 fn new(segments: Segments) -> Self {
837 Self { segments }
838 }
839}
840
841impl Renderable for SegmentsCell {
842 fn render(&self, _console: &Console, _options: &ConsoleOptions) -> Segments {
843 self.segments.clone()
844 }
845
846 fn measure(&self, _console: &Console, _options: &ConsoleOptions) -> crate::Measurement {
847 let mut max_width: usize = 0;
849 let mut current_width: usize = 0;
850 for seg in self.segments.iter() {
851 if seg.text.as_ref() == "\n" {
852 max_width = max_width.max(current_width);
853 current_width = 0;
854 continue;
855 }
856 current_width += seg.cell_len();
857 }
858 max_width = max_width.max(current_width);
859 crate::Measurement::new(max_width, max_width)
860 }
861}
862
863enum DeferredConsoleCall {
864 Print {
865 segments: Segments,
866 style: Option<Style>,
867 justify: Option<JustifyMethod>,
868 overflow: Option<OverflowMethod>,
869 no_wrap: bool,
870 end: String,
871 },
872 Log {
873 segments: Segments,
874 file: Option<String>,
875 line: Option<u32>,
876 },
877}
878
879pub struct Progress {
880 state: Arc<Mutex<ProgressState>>,
881 columns: Arc<Vec<Box<dyn ProgressColumn>>>,
882 live: Live,
883 disable: bool,
884 auto_refresh: bool,
885 started: Arc<AtomicBool>,
886 deferred_console_calls: Arc<Mutex<Vec<DeferredConsoleCall>>>,
887}
888
889impl Progress {
890 pub fn new(
891 columns: Vec<Box<dyn ProgressColumn>>,
892 live_options: LiveOptions,
893 disable: bool,
894 expand: bool,
895 ) -> Self {
896 Self::with_console(columns, Console::new(), live_options, disable, expand)
897 }
898
899 pub fn with_console(
900 columns: Vec<Box<dyn ProgressColumn>>,
901 console: Console<Stdout>,
902 live_options: LiveOptions,
903 disable: bool,
904 expand: bool,
905 ) -> Self {
906 let auto_refresh = live_options.auto_refresh;
907 let state = Arc::new(Mutex::new(ProgressState {
908 start: Instant::now(),
909 tasks: HashMap::new(),
910 order: Vec::new(),
911 next_id: 0,
912 speed_estimate_period: 30.0,
913 expand,
914 cell_cache: HashMap::new(),
915 }));
916 let columns = Arc::new(columns);
917 let renderable = ProgressRenderable {
918 state: state.clone(),
919 columns: columns.clone(),
920 };
921 let live = Live::with_console(Box::new(renderable), console, live_options);
922 Self {
923 state,
924 columns,
925 live,
926 disable,
927 auto_refresh,
928 started: Arc::new(AtomicBool::new(false)),
929 deferred_console_calls: Arc::new(Mutex::new(Vec::new())),
930 }
931 }
932
933 pub fn new_default(
936 live_options: LiveOptions,
937 disable: bool,
938 expand: bool,
939 show_speed: bool,
940 ) -> Self {
941 let columns: Vec<Box<dyn ProgressColumn>> = vec![
942 Box::new(TextColumn::new("[progress.description]{task.description}")),
943 Box::new(BarColumn::new()),
944 Box::new(TaskProgressColumn::new(show_speed)),
945 Box::new(TimeRemainingColumn::new(false)),
946 ];
947 Self::new(columns, live_options, disable, expand)
948 }
949
950 pub fn start(&mut self) -> io::Result<()> {
951 if self.disable {
952 return Ok(());
953 }
954 self.live.start(true)?;
955 self.started.store(true, Ordering::SeqCst);
956 self.flush_deferred_console_calls()
957 }
958
959 pub fn stop(&mut self) -> io::Result<()> {
960 if self.disable {
961 return Ok(());
962 }
963 self.started.store(false, Ordering::SeqCst);
964 self.live.stop()
965 }
966
967 pub fn refresh(&self) -> io::Result<()> {
968 if self.disable {
969 return Ok(());
970 }
971 self.live.refresh()
972 }
973
974 pub fn add_task(
975 &self,
976 description: &str,
977 start: bool,
978 total: Option<f64>,
979 completed: f64,
980 visible: bool,
981 ) -> TaskID {
982 let id = {
983 let mut state = self.state.lock().expect("progress state mutex poisoned");
984 let id = TaskID(state.next_id);
985 state.next_id += 1;
986
987 let task = ProgressTask {
988 id,
989 description: description.to_string(),
990 total,
991 completed,
992 visible,
993 fields: HashMap::new(),
994 finished_time: None,
995 finished_speed: None,
996 start_time: None,
997 stop_time: None,
998 progress: VecDeque::with_capacity(1000),
999 };
1000
1001 state.order.push(id);
1002 state.tasks.insert(id, task);
1003 id
1004 };
1005 if start {
1006 self.start_task(id);
1007 }
1008 let _ = self.refresh();
1009 id
1010 }
1011
1012 pub fn remove_task(&self, task_id: TaskID) {
1013 let mut state = self.state.lock().expect("progress state mutex poisoned");
1014 state.tasks.remove(&task_id);
1015 state.order.retain(|&id| id != task_id);
1016 state.cell_cache.retain(|(id, _), _| *id != task_id);
1017 }
1018
1019 pub fn start_task(&self, task_id: TaskID) {
1020 let mut state = self.state.lock().expect("progress state mutex poisoned");
1021 let now = state.now();
1022 if let Some(task) = state.tasks.get_mut(&task_id) {
1023 if task.start_time.is_none() {
1024 task.start_time = Some(now);
1025 }
1026 }
1027 }
1028
1029 pub fn stop_task(&self, task_id: TaskID) {
1030 let mut state = self.state.lock().expect("progress state mutex poisoned");
1031 let now = state.now();
1032 if let Some(task) = state.tasks.get_mut(&task_id) {
1033 if task.start_time.is_none() {
1034 task.start_time = Some(now);
1035 }
1036 task.stop_time = Some(now);
1037 }
1038 }
1039
1040 pub fn advance(&self, task_id: TaskID, advance: f64) {
1041 let mut state = self.state.lock().expect("progress state mutex poisoned");
1042 let now = state.now();
1043 let speed_estimate_period = state.speed_estimate_period;
1044 let Some(task) = state.tasks.get_mut(&task_id) else {
1045 return;
1046 };
1047
1048 let completed_start = task.completed;
1049 task.completed += advance;
1050 let update_completed = task.completed - completed_start;
1051
1052 let old_sample_time = now - speed_estimate_period;
1053 while let Some(front) = task.progress.front() {
1054 if front.timestamp < old_sample_time {
1055 task.progress.pop_front();
1056 } else {
1057 break;
1058 }
1059 }
1060 while task.progress.len() > 1000 {
1061 task.progress.pop_front();
1062 }
1063 task.progress.push_back(ProgressSample {
1064 timestamp: now,
1065 completed: update_completed,
1066 });
1067
1068 if let Some(total) = task.total {
1069 if task.completed >= total && task.finished_time.is_none() {
1070 task.finished_time = task.elapsed(now);
1071 task.finished_speed = task.speed();
1072 }
1073 }
1074 }
1075
1076 pub fn update(
1077 &self,
1078 task_id: TaskID,
1079 total: Option<Option<f64>>,
1080 completed: Option<f64>,
1081 advance: Option<f64>,
1082 description: Option<String>,
1083 visible: Option<bool>,
1084 refresh: bool,
1085 fields: Option<HashMap<String, String>>,
1086 ) {
1087 let mut state = self.state.lock().expect("progress state mutex poisoned");
1088 let now = state.now();
1089 let speed_estimate_period = state.speed_estimate_period;
1090 let Some(task) = state.tasks.get_mut(&task_id) else {
1091 return;
1092 };
1093 let completed_start = task.completed;
1094
1095 if let Some(total) = total {
1096 if task.total != total {
1097 task.total = total;
1098 task.progress.clear();
1099 task.finished_time = None;
1100 task.finished_speed = None;
1101 }
1102 }
1103 if let Some(advance) = advance {
1104 task.completed += advance;
1105 }
1106 if let Some(completed) = completed {
1107 task.completed = completed;
1108 }
1109 if let Some(description) = description {
1110 task.description = description;
1111 }
1112 if let Some(visible) = visible {
1113 task.visible = visible;
1114 }
1115 if let Some(fields) = fields {
1116 task.fields.extend(fields);
1117 }
1118
1119 let update_completed = task.completed - completed_start;
1120 let old_sample_time = now - speed_estimate_period;
1121 while let Some(front) = task.progress.front() {
1122 if front.timestamp < old_sample_time {
1123 task.progress.pop_front();
1124 } else {
1125 break;
1126 }
1127 }
1128 while task.progress.len() > 1000 {
1129 task.progress.pop_front();
1130 }
1131 if update_completed > 0.0 {
1132 task.progress.push_back(ProgressSample {
1133 timestamp: now,
1134 completed: update_completed,
1135 });
1136 }
1137
1138 if let Some(total) = task.total {
1139 if task.completed >= total && task.finished_time.is_none() {
1140 task.finished_time = task.elapsed(now);
1141 }
1142 }
1143 drop(state);
1144 if refresh {
1145 let _ = self.refresh();
1146 }
1147 }
1148
1149 pub fn update_task(
1150 &self,
1151 task_id: TaskID,
1152 total: Option<Option<f64>>,
1153 completed: Option<f64>,
1154 description: Option<String>,
1155 visible: Option<bool>,
1156 ) {
1157 self.update(
1158 task_id,
1159 total,
1160 completed,
1161 None,
1162 description,
1163 visible,
1164 false,
1165 None,
1166 );
1167 }
1168
1169 pub fn reset(
1170 &self,
1171 task_id: TaskID,
1172 start: bool,
1173 total: Option<Option<f64>>,
1174 completed: f64,
1175 visible: Option<bool>,
1176 description: Option<String>,
1177 fields: Option<HashMap<String, String>>,
1178 ) {
1179 let mut state = self.state.lock().expect("progress state mutex poisoned");
1180 let now = state.now();
1181 let Some(task) = state.tasks.get_mut(&task_id) else {
1182 return;
1183 };
1184 task.progress.clear();
1185 task.finished_time = None;
1186 task.finished_speed = None;
1187 task.start_time = if start { Some(now) } else { None };
1188 if let Some(total) = total {
1189 task.total = total;
1190 }
1191 task.completed = completed;
1192 if let Some(visible) = visible {
1193 task.visible = visible;
1194 }
1195 if let Some(fields) = fields {
1196 task.fields = fields;
1197 }
1198 if let Some(description) = description {
1199 task.description = description;
1200 }
1201 task.finished_time = None;
1202 drop(state);
1203 let _ = self.refresh();
1204 }
1205
1206 pub fn track<'a, I>(
1207 &'a self,
1208 iter: I,
1209 task_id: TaskID,
1210 update_period: Duration,
1211 ) -> ProgressIterator<'a, I::IntoIter>
1212 where
1213 I: IntoIterator,
1214 {
1215 ProgressIterator {
1216 iter: iter.into_iter(),
1217 progress: self,
1218 task_id,
1219 track_thread: TrackThread::new(
1220 self.auto_refresh && !self.disable,
1221 self.state.clone(),
1222 self.live.started_flag(),
1223 task_id,
1224 update_period,
1225 ),
1226 pending_increment: false,
1227 }
1228 }
1229
1230 pub fn track_iter<'a, I>(
1232 &'a self,
1233 iter: I,
1234 description: &str,
1235 total: Option<f64>,
1236 completed: f64,
1237 update_period: Duration,
1238 ) -> ProgressIterator<'a, I::IntoIter>
1239 where
1240 I: IntoIterator,
1241 {
1242 let task_id = self.add_task(description, true, total, completed, true);
1243 self.track(iter, task_id, update_period)
1244 }
1245
1246 pub fn track_sequence<'a, I>(
1247 &'a self,
1248 sequence: I,
1249 config: TrackConfig,
1250 ) -> ProgressIterator<'a, I::IntoIter>
1251 where
1252 I: IntoIterator,
1253 {
1254 let iter = sequence.into_iter();
1255 let inferred_total = config.total.or_else(|| {
1256 let (lower, upper) = iter.size_hint();
1257 let hint = upper.or(Some(lower)).unwrap_or(0);
1258 if hint == 0 { None } else { Some(hint as f64) }
1259 });
1260
1261 let task_id = if let Some(task_id) = config.task_id {
1262 self.update(
1263 task_id,
1264 Some(inferred_total),
1266 Some(config.completed),
1267 None,
1268 None,
1269 None,
1270 false,
1271 None,
1272 );
1273 task_id
1274 } else {
1275 self.add_task(
1276 &config.description,
1277 true,
1278 inferred_total,
1279 config.completed,
1280 true,
1281 )
1282 };
1283
1284 self.track(iter, task_id, config.update_period)
1285 }
1286
1287 fn render_to_deferred_segments<R: Renderable + ?Sized>(renderable: &R) -> Segments {
1288 let options = ConsoleOptions::default();
1289 let temp_console = Console::<Stdout>::with_options(options.clone());
1290 renderable.render(&temp_console, &options)
1291 }
1292
1293 fn flush_deferred_console_calls(&self) -> io::Result<()> {
1294 let calls = {
1295 let mut deferred = self
1296 .deferred_console_calls
1297 .lock()
1298 .expect("deferred console calls mutex poisoned");
1299 deferred.drain(..).collect::<Vec<_>>()
1300 };
1301
1302 for call in calls {
1303 match call {
1304 DeferredConsoleCall::Print {
1305 segments,
1306 style,
1307 justify,
1308 overflow,
1309 no_wrap,
1310 end,
1311 } => {
1312 let renderable = SegmentsCell::new(segments);
1313 self.live
1314 .print(&renderable, style, justify, overflow, no_wrap, &end)?;
1315 }
1316 DeferredConsoleCall::Log {
1317 segments,
1318 file,
1319 line,
1320 } => {
1321 let renderable = SegmentsCell::new(segments);
1322 self.live.log(&renderable, file.as_deref(), line)?;
1323 }
1324 }
1325 }
1326
1327 Ok(())
1328 }
1329
1330 pub fn print<R: Renderable + ?Sized>(
1331 &self,
1332 renderable: &R,
1333 style: Option<Style>,
1334 justify: Option<JustifyMethod>,
1335 overflow: Option<OverflowMethod>,
1336 no_wrap: bool,
1337 end: &str,
1338 ) -> io::Result<()> {
1339 if self.disable {
1340 return Ok(());
1341 }
1342
1343 if self.started.load(Ordering::SeqCst) {
1344 return self
1345 .live
1346 .print(renderable, style, justify, overflow, no_wrap, end);
1347 }
1348
1349 let mut deferred = self
1350 .deferred_console_calls
1351 .lock()
1352 .expect("deferred console calls mutex poisoned");
1353 deferred.push(DeferredConsoleCall::Print {
1354 segments: Self::render_to_deferred_segments(renderable),
1355 style,
1356 justify,
1357 overflow,
1358 no_wrap,
1359 end: end.to_string(),
1360 });
1361 Ok(())
1362 }
1363
1364 pub fn log<R: Renderable + ?Sized>(
1365 &self,
1366 renderable: &R,
1367 file: Option<&str>,
1368 line: Option<u32>,
1369 ) -> io::Result<()> {
1370 if self.disable {
1371 return Ok(());
1372 }
1373
1374 if self.started.load(Ordering::SeqCst) {
1375 return self.live.log(renderable, file, line);
1376 }
1377
1378 let mut deferred = self
1379 .deferred_console_calls
1380 .lock()
1381 .expect("deferred console calls mutex poisoned");
1382 deferred.push(DeferredConsoleCall::Log {
1383 segments: Self::render_to_deferred_segments(renderable),
1384 file: file.map(ToString::to_string),
1385 line,
1386 });
1387 Ok(())
1388 }
1389}
1390
1391impl Progress {
1392 pub fn tasks(&self) -> Vec<ProgressTask> {
1394 let state = self.state.lock().expect("progress state mutex poisoned");
1395 state
1396 .order
1397 .iter()
1398 .filter_map(|id| state.tasks.get(id).cloned())
1399 .collect()
1400 }
1401
1402 pub fn task_ids(&self) -> Vec<TaskID> {
1404 let state = self.state.lock().expect("progress state mutex poisoned");
1405 state.order.clone()
1406 }
1407
1408 pub fn finished(&self) -> bool {
1410 let state = self.state.lock().expect("progress state mutex poisoned");
1411 state.tasks.values().all(|t| t.finished())
1412 }
1413}
1414
1415impl Drop for Progress {
1416 fn drop(&mut self) {
1417 let _ = self.stop();
1418 }
1419}
1420
1421impl Renderable for Progress {
1422 fn render(&self, console: &Console, options: &ConsoleOptions) -> Segments {
1423 let renderable = ProgressRenderable {
1425 state: self.state.clone(),
1426 columns: self.columns.clone(),
1427 };
1428 renderable.render(console, options)
1429 }
1430}
1431
1432pub struct ProgressReader<'a, R: Read> {
1459 inner: R,
1460 progress: &'a Progress,
1461 task_id: TaskID,
1462 #[allow(dead_code)]
1466 close_handle: bool,
1467}
1468
1469impl<'a, R: Read> ProgressReader<'a, R> {
1470 pub fn new(inner: R, progress: &'a Progress, task_id: TaskID, close_handle: bool) -> Self {
1475 Self {
1476 inner,
1477 progress,
1478 task_id,
1479 close_handle,
1480 }
1481 }
1482
1483 pub fn task_id(&self) -> TaskID {
1485 self.task_id
1486 }
1487
1488 pub fn inner(&self) -> &R {
1490 &self.inner
1491 }
1492
1493 pub fn inner_mut(&mut self) -> &mut R {
1495 &mut self.inner
1496 }
1497
1498 pub fn into_inner(self) -> R {
1500 self.inner
1501 }
1502}
1503
1504impl<R: Read> Read for ProgressReader<'_, R> {
1505 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
1506 let n = self.inner.read(buf)?;
1507 self.progress.advance(self.task_id, n as f64);
1508 Ok(n)
1509 }
1510}
1511
1512impl<R: Read + BufRead> BufRead for ProgressReader<'_, R> {
1513 fn fill_buf(&mut self) -> io::Result<&[u8]> {
1514 self.inner.fill_buf()
1515 }
1516
1517 fn consume(&mut self, amt: usize) {
1518 self.inner.consume(amt);
1519 self.progress.advance(self.task_id, amt as f64);
1520 }
1521}
1522
1523impl<R: Read + Seek> Seek for ProgressReader<'_, R> {
1524 fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
1525 let new_pos = self.inner.seek(pos)?;
1526 self.progress.update(
1528 self.task_id,
1529 None,
1530 Some(new_pos as f64),
1531 None,
1532 None,
1533 None,
1534 false,
1535 None,
1536 );
1537 Ok(new_pos)
1538 }
1539}
1540
1541pub struct WrapFileBuilder<'a, R: Read> {
1560 progress: &'a Progress,
1561 reader: R,
1562 total: Option<u64>,
1563 task_id: Option<TaskID>,
1564 description: String,
1565 close_handle: bool,
1566}
1567
1568impl<'a, R: Read> WrapFileBuilder<'a, R> {
1569 pub fn new(progress: &'a Progress, reader: R) -> Self {
1571 Self {
1572 progress,
1573 reader,
1574 total: None,
1575 task_id: None,
1576 description: "Reading...".to_string(),
1577 close_handle: false,
1578 }
1579 }
1580
1581 pub fn total(mut self, total: u64) -> Self {
1583 self.total = Some(total);
1584 self
1585 }
1586
1587 pub fn total_opt(mut self, total: Option<u64>) -> Self {
1589 self.total = total;
1590 self
1591 }
1592
1593 pub fn task_id(mut self, task_id: TaskID) -> Self {
1595 self.task_id = Some(task_id);
1596 self
1597 }
1598
1599 pub fn description(mut self, description: impl Into<String>) -> Self {
1601 self.description = description.into();
1602 self
1603 }
1604
1605 pub fn close_handle(mut self, close: bool) -> Self {
1607 self.close_handle = close;
1608 self
1609 }
1610
1611 pub fn build(self) -> ProgressReader<'a, R> {
1616 let task_id = if let Some(id) = self.task_id {
1617 if let Some(total) = self.total {
1619 self.progress.update(
1620 id,
1621 Some(Some(total as f64)),
1622 None,
1623 None,
1624 None,
1625 None,
1626 false,
1627 None,
1628 );
1629 }
1630 id
1631 } else {
1632 self.progress.add_task(
1634 &self.description,
1635 true,
1636 self.total.map(|t| t as f64),
1637 0.0,
1638 true,
1639 )
1640 };
1641
1642 ProgressReader::new(self.reader, self.progress, task_id, self.close_handle)
1643 }
1644}
1645
1646impl Progress {
1647 pub fn wrap_file<'a, R: Read>(
1674 &'a self,
1675 reader: R,
1676 total: Option<u64>,
1677 description: &str,
1678 ) -> ProgressReader<'a, R> {
1679 WrapFileBuilder::new(self, reader)
1680 .total_opt(total)
1681 .description(description)
1682 .close_handle(false)
1683 .build()
1684 }
1685
1686 pub fn wrap_file_with_task<'a, R: Read>(
1697 &'a self,
1698 reader: R,
1699 task_id: TaskID,
1700 total: Option<u64>,
1701 ) -> ProgressReader<'a, R> {
1702 WrapFileBuilder::new(self, reader)
1703 .task_id(task_id)
1704 .total_opt(total)
1705 .close_handle(false)
1706 .build()
1707 }
1708
1709 pub fn open<'a, P: AsRef<Path>>(
1741 &'a self,
1742 path: P,
1743 description: &str,
1744 ) -> io::Result<ProgressReader<'a, File>> {
1745 let path = path.as_ref();
1746 let file = File::open(path)?;
1747 let metadata = file.metadata()?;
1748 let total = metadata.len();
1749
1750 Ok(WrapFileBuilder::new(self, file)
1751 .total(total)
1752 .description(description)
1753 .close_handle(true)
1754 .build())
1755 }
1756
1757 pub fn open_with_task<'a, P: AsRef<Path>>(
1772 &'a self,
1773 path: P,
1774 task_id: TaskID,
1775 ) -> io::Result<ProgressReader<'a, File>> {
1776 let path = path.as_ref();
1777 let file = File::open(path)?;
1778 let metadata = file.metadata()?;
1779 let total = metadata.len();
1780
1781 Ok(WrapFileBuilder::new(self, file)
1782 .task_id(task_id)
1783 .total(total)
1784 .close_handle(true)
1785 .build())
1786 }
1787}
1788
1789pub struct ProgressIterator<'a, I> {
1790 iter: I,
1791 progress: &'a Progress,
1792 task_id: TaskID,
1793 track_thread: TrackThread,
1794 pending_increment: bool,
1795}
1796
1797struct TrackThread {
1798 enabled: bool,
1799 completed: Arc<AtomicUsize>,
1800 done: Arc<AtomicBool>,
1801 handle: Option<thread::JoinHandle<()>>,
1802}
1803
1804impl TrackThread {
1805 fn new(
1806 enabled: bool,
1807 state: Arc<Mutex<ProgressState>>,
1808 live_started: Arc<AtomicBool>,
1809 task_id: TaskID,
1810 update_period: Duration,
1811 ) -> Self {
1812 if !enabled {
1813 return Self {
1814 enabled: false,
1815 completed: Arc::new(AtomicUsize::new(0)),
1816 done: Arc::new(AtomicBool::new(false)),
1817 handle: None,
1818 };
1819 }
1820
1821 let update_period = update_period.max(Duration::from_millis(1));
1822
1823 let completed = Arc::new(AtomicUsize::new(0));
1824 let done = Arc::new(AtomicBool::new(false));
1825 let completed_thread = completed.clone();
1826 let done_thread = done.clone();
1827
1828 let handle = thread::spawn(move || {
1829 let mut last_completed: usize = 0;
1830 while !done_thread.load(Ordering::SeqCst) && live_started.load(Ordering::SeqCst) {
1831 thread::sleep(update_period);
1832 if done_thread.load(Ordering::SeqCst) || !live_started.load(Ordering::SeqCst) {
1833 break;
1834 }
1835 let current = completed_thread.load(Ordering::SeqCst);
1836 if current != last_completed {
1837 let delta = (current - last_completed) as f64;
1838 last_completed = current;
1839 advance_state(&state, task_id, delta);
1840 }
1841 }
1842 });
1843
1844 Self {
1845 enabled: true,
1846 completed,
1847 done,
1848 handle: Some(handle),
1849 }
1850 }
1851
1852 fn increment(&self) {
1853 if self.enabled {
1854 self.completed.fetch_add(1, Ordering::SeqCst);
1855 }
1856 }
1857
1858 fn take_completed(&self) -> usize {
1859 self.completed.load(Ordering::SeqCst)
1860 }
1861
1862 fn stop(&mut self) {
1863 if !self.enabled {
1864 return;
1865 }
1866 self.done.store(true, Ordering::SeqCst);
1867 if let Some(handle) = self.handle.take() {
1868 let _ = handle.join();
1869 }
1870 }
1871}
1872
1873impl Drop for TrackThread {
1874 fn drop(&mut self) {
1875 self.stop();
1876 }
1877}
1878
1879impl<'a, I> Iterator for ProgressIterator<'a, I>
1880where
1881 I: Iterator,
1882{
1883 type Item = I::Item;
1884
1885 fn next(&mut self) -> Option<Self::Item> {
1886 if !self.progress.disable && self.pending_increment {
1887 if self.progress.auto_refresh {
1888 self.track_thread.increment();
1889 } else {
1890 self.progress.advance(self.task_id, 1.0);
1891 let _ = self.progress.refresh();
1892 }
1893 }
1894
1895 let item = self.iter.next();
1896 self.pending_increment = item.is_some();
1897 item
1898 }
1899}
1900
1901impl<'a, I> Drop for ProgressIterator<'a, I> {
1902 fn drop(&mut self) {
1903 if self.progress.disable {
1904 return;
1905 }
1906 if self.progress.auto_refresh {
1907 let completed = self.track_thread.take_completed() as f64;
1908 self.track_thread.stop();
1909 self.progress.update(
1910 self.task_id,
1911 None,
1912 Some(completed),
1913 None,
1914 None,
1915 None,
1916 true,
1917 None,
1918 );
1919 }
1920 }
1921}
1922
1923#[derive(Debug, Clone)]
1924pub struct TrackConfig {
1925 pub total: Option<f64>,
1926 pub completed: f64,
1927 pub task_id: Option<TaskID>,
1928 pub description: String,
1929 pub update_period: Duration,
1930}
1931
1932impl TrackConfig {
1933 pub fn new(description: impl Into<String>) -> Self {
1934 Self {
1935 total: None,
1936 completed: 0.0,
1937 task_id: None,
1938 description: description.into(),
1939 update_period: Duration::from_millis(100),
1940 }
1941 }
1942
1943 pub fn with_total(mut self, total: Option<f64>) -> Self {
1944 self.total = total;
1945 self
1946 }
1947
1948 pub fn with_completed(mut self, completed: f64) -> Self {
1949 self.completed = completed;
1950 self
1951 }
1952
1953 pub fn with_task_id(mut self, task_id: TaskID) -> Self {
1954 self.task_id = Some(task_id);
1955 self
1956 }
1957
1958 pub fn with_update_period(mut self, update_period: Duration) -> Self {
1959 self.update_period = update_period;
1960 self
1961 }
1962}
1963
1964fn advance_state(state: &Arc<Mutex<ProgressState>>, task_id: TaskID, advance: f64) {
1965 let mut state = state.lock().expect("progress state mutex poisoned");
1966 let now = state.now();
1967 let speed_estimate_period = state.speed_estimate_period;
1968 let Some(task) = state.tasks.get_mut(&task_id) else {
1969 return;
1970 };
1971
1972 let completed_start = task.completed;
1973 task.completed += advance;
1974 let update_completed = task.completed - completed_start;
1975
1976 let old_sample_time = now - speed_estimate_period;
1977 while let Some(front) = task.progress.front() {
1978 if front.timestamp < old_sample_time {
1979 task.progress.pop_front();
1980 } else {
1981 break;
1982 }
1983 }
1984 while task.progress.len() > 1000 {
1985 task.progress.pop_front();
1986 }
1987 task.progress.push_back(ProgressSample {
1988 timestamp: now,
1989 completed: update_completed,
1990 });
1991
1992 if let Some(total) = task.total {
1993 if task.completed >= total && task.finished_time.is_none() {
1994 task.finished_time = task.elapsed(now);
1995 task.finished_speed = task.speed();
1996 }
1997 }
1998}
1999
2000fn format_task_template(template: &str, task: &ProgressTask, now: f64) -> String {
2001 let mut out = String::new();
2012 let mut rest = template;
2013 while let Some(start) = rest.find("{task.") {
2014 out.push_str(&rest[..start]);
2015 let after = &rest[start + 6..];
2016 let Some(end) = after.find('}') else {
2017 out.push_str(rest);
2018 return out;
2019 };
2020 let inside = &after[..end];
2021 let (field, spec) = inside.split_once(':').unwrap_or((inside, ""));
2022 let formatted = format_task_field(field, spec, task, now);
2023 out.push_str(&formatted);
2024 rest = &after[end + 1..];
2025 }
2026 out.push_str(rest);
2027 out
2028}
2029
2030#[derive(Debug, Default, Clone, Copy)]
2031struct FormatSpec {
2032 align: Option<char>,
2033 width: Option<usize>,
2034 precision: Option<usize>,
2035 ty: Option<char>,
2036}
2037
2038fn parse_format_spec(spec: &str) -> FormatSpec {
2039 let mut s = spec;
2040 let mut out = FormatSpec::default();
2041
2042 if let Some(first) = s.chars().next() {
2043 if matches!(first, '<' | '>' | '^') {
2044 out.align = Some(first);
2045 s = &s[first.len_utf8()..];
2046 }
2047 }
2048
2049 let mut width_end = 0;
2051 for (i, ch) in s.char_indices() {
2052 if ch.is_ascii_digit() {
2053 width_end = i + 1;
2054 } else {
2055 break;
2056 }
2057 }
2058 if width_end > 0 {
2059 if let Ok(w) = s[..width_end].parse::<usize>() {
2060 out.width = Some(w);
2061 }
2062 s = &s[width_end..];
2063 }
2064
2065 if let Some(rest) = s.strip_prefix('.') {
2067 let mut prec_end = 0;
2068 for (i, ch) in rest.char_indices() {
2069 if ch.is_ascii_digit() {
2070 prec_end = i + 1;
2071 } else {
2072 break;
2073 }
2074 }
2075 if prec_end > 0 {
2076 if let Ok(p) = rest[..prec_end].parse::<usize>() {
2077 out.precision = Some(p);
2078 }
2079 s = &rest[prec_end..];
2080 }
2081 }
2082
2083 if let Some(last) = s.chars().next() {
2085 if matches!(last, 'f' | 'd') {
2086 out.ty = Some(last);
2087 }
2088 }
2089
2090 out
2091}
2092
2093fn pad_aligned(text: &str, width: usize, align: char) -> String {
2094 let current = crate::cells::cell_len(text);
2095 if current >= width {
2096 return text.to_string();
2097 }
2098 let pad = width - current;
2099 match align {
2100 '<' => format!("{text}{}", " ".repeat(pad)),
2101 '^' => {
2102 let left = pad / 2;
2103 let right = pad - left;
2104 format!("{}{}{}", " ".repeat(left), text, " ".repeat(right))
2105 }
2106 _ => format!("{}{}", " ".repeat(pad), text),
2107 }
2108}
2109
2110fn apply_format_spec_value(text: String, spec: &FormatSpec) -> String {
2111 let Some(width) = spec.width else {
2112 return text;
2113 };
2114 let align = spec.align.unwrap_or('>');
2115 pad_aligned(&text, width, align)
2116}
2117
2118fn format_task_field(field: &str, spec: &str, task: &ProgressTask, now: f64) -> String {
2119 let spec = parse_format_spec(spec);
2120
2121 let value = match field {
2122 "description" => return apply_format_spec_value(task.description.clone(), &spec),
2123 "percentage" => Some(task.percentage()),
2124 "completed" => Some(task.completed),
2125 "total" => task.total,
2126 "elapsed" => task.elapsed(now),
2127 "remaining" => task.remaining(),
2128 "speed" => task.speed(),
2129 "time_remaining" => task.time_remaining(),
2130 _ => None,
2131 };
2132
2133 let Some(value) = value else {
2134 return apply_format_spec_value(String::new(), &spec);
2135 };
2136
2137 let rendered = match spec.ty {
2138 Some('d') => format!("{}", value as i64),
2139 Some('f') => {
2140 let precision = spec.precision.unwrap_or(0);
2141 format!("{value:.precision$}", precision = precision)
2142 }
2143 _ => {
2144 if let Some(precision) = spec.precision {
2145 format!("{value:.precision$}", precision = precision)
2146 } else {
2147 if value.fract() == 0.0 {
2149 format!("{:.0}", value)
2150 } else {
2151 format!("{value}")
2152 }
2153 }
2154 }
2155 };
2156
2157 apply_format_spec_value(rendered, &spec)
2158}
2159
2160#[cfg(test)]
2161mod tests {
2162 use super::*;
2163
2164 #[test]
2165 fn test_format_task_template_description() {
2166 let task = ProgressTask {
2167 id: TaskID(1),
2168 description: "Hello".to_string(),
2169 total: Some(10.0),
2170 completed: 3.0,
2171 visible: true,
2172 fields: HashMap::new(),
2173 finished_time: None,
2174 finished_speed: None,
2175 start_time: Some(0.0),
2176 stop_time: None,
2177 progress: VecDeque::new(),
2178 };
2179
2180 let s = format_task_template("x {task.description} y", &task, 1.0);
2181 assert_eq!(s, "x Hello y");
2182 }
2183
2184 #[test]
2185 fn test_apply_format_spec_right_align() {
2186 let spec = parse_format_spec(">3");
2187 assert_eq!(apply_format_spec_value("7".to_string(), &spec), " 7");
2188 assert_eq!(apply_format_spec_value("1234".to_string(), &spec), "1234");
2189 }
2190
2191 #[test]
2192 fn test_progress_update_appends_samples_only_on_positive_change() {
2193 let live_options = LiveOptions {
2194 auto_refresh: true,
2195 ..Default::default()
2196 };
2197 let progress = Progress::new_default(live_options, true, false, false);
2198
2199 let task_id = progress.add_task("t", true, Some(10.0), 0.0, true);
2200
2201 {
2202 let state = progress
2203 .state
2204 .lock()
2205 .expect("progress state mutex poisoned");
2206 let task = state.tasks.get(&task_id).unwrap();
2207 assert_eq!(task.progress.len(), 0);
2208 }
2209
2210 progress.update(task_id, None, None, Some(1.0), None, None, false, None);
2211 {
2212 let state = progress
2213 .state
2214 .lock()
2215 .expect("progress state mutex poisoned");
2216 let task = state.tasks.get(&task_id).unwrap();
2217 assert_eq!(task.completed, 1.0);
2218 assert_eq!(task.progress.len(), 1);
2219 }
2220
2221 progress.update(task_id, None, Some(1.0), None, None, None, false, None);
2223 {
2224 let state = progress
2225 .state
2226 .lock()
2227 .expect("progress state mutex poisoned");
2228 let task = state.tasks.get(&task_id).unwrap();
2229 assert_eq!(task.progress.len(), 1);
2230 }
2231 }
2232
2233 #[test]
2234 fn test_progress_update_total_change_resets_speed_samples() {
2235 let live_options = LiveOptions {
2236 auto_refresh: true,
2237 ..Default::default()
2238 };
2239 let progress = Progress::new_default(live_options, true, false, false);
2240
2241 let task_id = progress.add_task("t", true, Some(10.0), 0.0, true);
2242 progress.update(task_id, None, None, Some(2.0), None, None, false, None);
2243 {
2244 let state = progress
2245 .state
2246 .lock()
2247 .expect("progress state mutex poisoned");
2248 let task = state.tasks.get(&task_id).unwrap();
2249 assert_eq!(task.progress.len(), 1);
2250 }
2251
2252 progress.update(
2254 task_id,
2255 Some(Some(20.0)),
2256 None,
2257 None,
2258 None,
2259 None,
2260 false,
2261 None,
2262 );
2263 {
2264 let state = progress
2265 .state
2266 .lock()
2267 .expect("progress state mutex poisoned");
2268 let task = state.tasks.get(&task_id).unwrap();
2269 assert_eq!(task.total, Some(20.0));
2270 assert_eq!(task.progress.len(), 0);
2271 }
2272 }
2273
2274 #[test]
2275 fn test_text_column_defaults_to_no_wrap() {
2276 let col = TextColumn::new("{task.description}").table_column();
2277 assert!(col.no_wrap);
2278 }
2279
2280 #[test]
2281 fn test_task_progress_column_empty_when_total_unknown() {
2282 let task = ProgressTask {
2283 id: TaskID(0),
2284 description: "t".to_string(),
2285 total: None,
2286 completed: 0.0,
2287 visible: true,
2288 fields: HashMap::new(),
2289 finished_time: None,
2290 finished_speed: None,
2291 start_time: Some(0.0),
2292 stop_time: None,
2293 progress: VecDeque::new(),
2294 };
2295
2296 let console = Console::new();
2297 let options = ConsoleOptions::default();
2298 let col = TaskProgressColumn::new(false);
2299 let rendered = col.render(&task, 0.0, &options);
2300 let segs = rendered.render(&console, &options);
2301 let text: String = segs.iter().map(|s| s.text.as_ref()).collect();
2302 assert_eq!(text, "");
2303 }
2304
2305 #[test]
2306 fn test_bar_column_defaults_to_width_40() {
2307 let task = ProgressTask {
2308 id: TaskID(0),
2309 description: "t".to_string(),
2310 total: Some(100.0),
2311 completed: 50.0,
2312 visible: true,
2313 fields: HashMap::new(),
2314 finished_time: None,
2315 finished_speed: None,
2316 start_time: Some(0.0),
2317 stop_time: None,
2318 progress: VecDeque::new(),
2319 };
2320
2321 let console = Console::new();
2322 let options = ConsoleOptions {
2323 max_width: 120,
2324 ..ConsoleOptions::default()
2325 };
2326 let col = BarColumn::new();
2327 let rendered = col.render(&task, 0.0, &options);
2328 let measurement = rendered.measure(&console, &options);
2329 assert_eq!(measurement.minimum, 40);
2330 assert_eq!(measurement.maximum, 40);
2331 }
2332
2333 #[test]
2334 fn test_track_sequence_updates_total_to_none_for_existing_task() {
2335 let live_options = LiveOptions {
2336 auto_refresh: true,
2337 ..Default::default()
2338 };
2339 let progress = Progress::new_default(live_options, true, false, false);
2340 let task_id = progress.add_task("t", true, Some(10.0), 0.0, true);
2341
2342 let config = TrackConfig {
2343 total: None,
2344 completed: 0.0,
2345 task_id: Some(task_id),
2346 description: "t".to_string(),
2347 update_period: Duration::from_millis(10),
2348 };
2349
2350 let _iter = progress.track_sequence(std::iter::empty::<usize>(), config);
2351 let state = progress
2352 .state
2353 .lock()
2354 .expect("progress state mutex poisoned");
2355 let task = state.tasks.get(&task_id).unwrap();
2356 assert_eq!(task.total, None);
2357 }
2358
2359 #[test]
2360 fn test_progress_reader_advances_on_read() {
2361 use std::io::Cursor;
2362
2363 let live_options = LiveOptions {
2364 auto_refresh: true,
2365 ..Default::default()
2366 };
2367 let progress = Progress::new_default(live_options, true, false, false);
2368
2369 let data = b"Hello, World!";
2370 let cursor = Cursor::new(data.to_vec());
2371
2372 let mut reader = progress.wrap_file(cursor, Some(data.len() as u64), "Reading");
2373 let task_id = reader.task_id();
2374
2375 {
2377 let state = progress.state.lock().expect("mutex poisoned");
2378 let task = state.tasks.get(&task_id).unwrap();
2379 assert_eq!(task.completed, 0.0);
2380 assert_eq!(task.total, Some(data.len() as f64));
2381 }
2382
2383 let mut buf = [0u8; 5];
2385 let n = reader.read(&mut buf).unwrap();
2386 assert_eq!(n, 5);
2387
2388 {
2390 let state = progress.state.lock().expect("mutex poisoned");
2391 let task = state.tasks.get(&task_id).unwrap();
2392 assert_eq!(task.completed, 5.0);
2393 }
2394
2395 let mut buf = Vec::new();
2397 reader.read_to_end(&mut buf).unwrap();
2398
2399 {
2401 let state = progress.state.lock().expect("mutex poisoned");
2402 let task = state.tasks.get(&task_id).unwrap();
2403 assert_eq!(task.completed, data.len() as f64);
2404 }
2405 }
2406
2407 #[test]
2408 fn test_progress_reader_with_existing_task() {
2409 use std::io::Cursor;
2410
2411 let live_options = LiveOptions {
2412 auto_refresh: true,
2413 ..Default::default()
2414 };
2415 let progress = Progress::new_default(live_options, true, false, false);
2416
2417 let task_id = progress.add_task("Existing task", true, None, 0.0, true);
2419
2420 let data = b"Test data";
2421 let cursor = Cursor::new(data.to_vec());
2422
2423 let mut reader = progress.wrap_file_with_task(cursor, task_id, Some(data.len() as u64));
2425
2426 {
2428 let state = progress.state.lock().expect("mutex poisoned");
2429 let task = state.tasks.get(&task_id).unwrap();
2430 assert_eq!(task.total, Some(data.len() as f64));
2431 }
2432
2433 let mut buf = Vec::new();
2435 reader.read_to_end(&mut buf).unwrap();
2436
2437 {
2439 let state = progress.state.lock().expect("mutex poisoned");
2440 let task = state.tasks.get(&task_id).unwrap();
2441 assert_eq!(task.completed, data.len() as f64);
2442 }
2443 }
2444
2445 #[test]
2446 fn test_progress_reader_seek_updates_completed() {
2447 use std::io::Cursor;
2448
2449 let live_options = LiveOptions {
2450 auto_refresh: true,
2451 ..Default::default()
2452 };
2453 let progress = Progress::new_default(live_options, true, false, false);
2454
2455 let data = b"0123456789";
2456 let cursor = Cursor::new(data.to_vec());
2457
2458 let mut reader = progress.wrap_file(cursor, Some(data.len() as u64), "Seeking");
2459 let task_id = reader.task_id();
2460
2461 let mut buf = [0u8; 3];
2463 reader.read(&mut buf).unwrap();
2464
2465 {
2466 let state = progress.state.lock().expect("mutex poisoned");
2467 let task = state.tasks.get(&task_id).unwrap();
2468 assert_eq!(task.completed, 3.0);
2469 }
2470
2471 reader.seek(SeekFrom::Start(7)).unwrap();
2473
2474 {
2476 let state = progress.state.lock().expect("mutex poisoned");
2477 let task = state.tasks.get(&task_id).unwrap();
2478 assert_eq!(task.completed, 7.0);
2479 }
2480 }
2481
2482 #[test]
2483 fn test_wrap_file_builder() {
2484 use std::io::Cursor;
2485
2486 let live_options = LiveOptions {
2487 auto_refresh: true,
2488 ..Default::default()
2489 };
2490 let progress = Progress::new_default(live_options, true, false, false);
2491
2492 let data = b"Builder test";
2493 let cursor = Cursor::new(data.to_vec());
2494
2495 let reader = WrapFileBuilder::new(&progress, cursor)
2496 .total(data.len() as u64)
2497 .description("Custom description")
2498 .build();
2499
2500 let task_id = reader.task_id();
2501
2502 let state = progress.state.lock().expect("mutex poisoned");
2503 let task = state.tasks.get(&task_id).unwrap();
2504 assert_eq!(task.description, "Custom description");
2505 assert_eq!(task.total, Some(data.len() as f64));
2506 }
2507
2508 #[test]
2509 fn test_progress_reader_indeterminate() {
2510 use std::io::Cursor;
2511
2512 let live_options = LiveOptions {
2513 auto_refresh: true,
2514 ..Default::default()
2515 };
2516 let progress = Progress::new_default(live_options, true, false, false);
2517
2518 let data = b"Unknown size stream";
2519 let cursor = Cursor::new(data.to_vec());
2520
2521 let mut reader = progress.wrap_file(cursor, None, "Streaming");
2523 let task_id = reader.task_id();
2524
2525 {
2526 let state = progress.state.lock().expect("mutex poisoned");
2527 let task = state.tasks.get(&task_id).unwrap();
2528 assert_eq!(task.total, None);
2529 }
2530
2531 let mut buf = Vec::new();
2533 reader.read_to_end(&mut buf).unwrap();
2534
2535 {
2537 let state = progress.state.lock().expect("mutex poisoned");
2538 let task = state.tasks.get(&task_id).unwrap();
2539 assert_eq!(task.completed, data.len() as f64);
2540 }
2541 }
2542
2543 #[test]
2544 fn test_progress_print_and_log_are_deferred_until_start() {
2545 let mut console = Console::new();
2546 console.set_quiet(true);
2547
2548 let live_options = LiveOptions {
2549 auto_refresh: false,
2550 ..Default::default()
2551 };
2552 let mut progress = Progress::with_console(
2553 vec![Box::new(TextColumn::new("{task.description}"))],
2554 console,
2555 live_options,
2556 false,
2557 false,
2558 );
2559
2560 progress
2561 .print(&Text::plain("queued print"), None, None, None, false, "\n")
2562 .unwrap();
2563 progress
2564 .log(&Text::plain("queued log"), Some("queued.rs"), Some(12))
2565 .unwrap();
2566
2567 {
2568 let deferred = progress
2569 .deferred_console_calls
2570 .lock()
2571 .expect("deferred console calls mutex poisoned");
2572 assert_eq!(deferred.len(), 2);
2573 }
2574
2575 progress.start().unwrap();
2576
2577 let deferred = progress
2578 .deferred_console_calls
2579 .lock()
2580 .expect("deferred console calls mutex poisoned");
2581 assert!(deferred.is_empty());
2582 }
2583
2584 #[test]
2585 fn test_progress_print_and_log_do_not_queue_after_start() {
2586 let mut console = Console::new();
2587 console.set_quiet(true);
2588
2589 let live_options = LiveOptions {
2590 auto_refresh: false,
2591 ..Default::default()
2592 };
2593 let mut progress = Progress::with_console(
2594 vec![Box::new(TextColumn::new("{task.description}"))],
2595 console,
2596 live_options,
2597 false,
2598 false,
2599 );
2600
2601 progress.start().unwrap();
2602
2603 progress
2604 .print(&Text::plain("live print"), None, None, None, false, "\n")
2605 .unwrap();
2606 progress
2607 .log(&Text::plain("live log"), Some("live.rs"), Some(34))
2608 .unwrap();
2609
2610 let deferred = progress
2611 .deferred_console_calls
2612 .lock()
2613 .expect("deferred console calls mutex poisoned");
2614 assert!(deferred.is_empty());
2615 }
2616}