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 let key = (task.id, col_index);
789 if let Some((last_ts, cached)) = new_cache.get(&key) {
790 if now - *last_ts < max_refresh.as_secs_f64() {
791 cached.clone()
792 } else {
793 let renderable = col.render(task, now, options);
794 let segs = renderable.render(console, options);
795 new_cache.insert(key, (now, segs.clone()));
796 segs
797 }
798 } else {
799 let renderable = col.render(task, now, options);
800 let segs = renderable.render(console, options);
801 new_cache.insert(key, (now, segs.clone()));
802 segs
803 }
804 } else {
805 let renderable = col.render(task, now, options);
806 renderable.render(console, options)
807 };
808 row_cells.push(Box::new(SegmentsCell::new(segs)));
809 }
810 table.add_row(Row::new(row_cells));
811 }
812
813 {
815 let mut state = self.state.lock().expect("progress state mutex poisoned");
816 state.cell_cache = new_cache;
817 }
818
819 table.render(console, options)
820 }
821}
822
823#[derive(Clone)]
824struct SegmentsCell {
825 segments: Segments,
826}
827
828impl SegmentsCell {
829 fn new(segments: Segments) -> Self {
830 Self { segments }
831 }
832}
833
834impl Renderable for SegmentsCell {
835 fn render(&self, _console: &Console, _options: &ConsoleOptions) -> Segments {
836 self.segments.clone()
837 }
838
839 fn measure(&self, _console: &Console, _options: &ConsoleOptions) -> crate::Measurement {
840 let mut max_width: usize = 0;
842 let mut current_width: usize = 0;
843 for seg in self.segments.iter() {
844 if seg.text.as_ref() == "\n" {
845 max_width = max_width.max(current_width);
846 current_width = 0;
847 continue;
848 }
849 current_width += seg.cell_len();
850 }
851 max_width = max_width.max(current_width);
852 crate::Measurement::new(max_width, max_width)
853 }
854}
855
856enum DeferredConsoleCall {
857 Print {
858 segments: Segments,
859 style: Option<Style>,
860 justify: Option<JustifyMethod>,
861 overflow: Option<OverflowMethod>,
862 no_wrap: bool,
863 end: String,
864 },
865 Log {
866 segments: Segments,
867 file: Option<String>,
868 line: Option<u32>,
869 },
870}
871
872pub struct Progress {
873 state: Arc<Mutex<ProgressState>>,
874 columns: Arc<Vec<Box<dyn ProgressColumn>>>,
875 live: Live,
876 disable: bool,
877 auto_refresh: bool,
878 started: Arc<AtomicBool>,
879 deferred_console_calls: Arc<Mutex<Vec<DeferredConsoleCall>>>,
880}
881
882impl Progress {
883 pub fn new(
884 columns: Vec<Box<dyn ProgressColumn>>,
885 live_options: LiveOptions,
886 disable: bool,
887 expand: bool,
888 ) -> Self {
889 Self::with_console(columns, Console::new(), live_options, disable, expand)
890 }
891
892 pub fn with_console(
893 columns: Vec<Box<dyn ProgressColumn>>,
894 console: Console<Stdout>,
895 live_options: LiveOptions,
896 disable: bool,
897 expand: bool,
898 ) -> Self {
899 let auto_refresh = live_options.auto_refresh;
900 let state = Arc::new(Mutex::new(ProgressState {
901 start: Instant::now(),
902 tasks: HashMap::new(),
903 order: Vec::new(),
904 next_id: 0,
905 speed_estimate_period: 30.0,
906 expand,
907 cell_cache: HashMap::new(),
908 }));
909 let columns = Arc::new(columns);
910 let renderable = ProgressRenderable {
911 state: state.clone(),
912 columns: columns.clone(),
913 };
914 let live = Live::with_console(Box::new(renderable), console, live_options);
915 Self {
916 state,
917 columns,
918 live,
919 disable,
920 auto_refresh,
921 started: Arc::new(AtomicBool::new(false)),
922 deferred_console_calls: Arc::new(Mutex::new(Vec::new())),
923 }
924 }
925
926 pub fn new_default(
929 live_options: LiveOptions,
930 disable: bool,
931 expand: bool,
932 show_speed: bool,
933 ) -> Self {
934 let columns: Vec<Box<dyn ProgressColumn>> = vec![
935 Box::new(TextColumn::new("[progress.description]{task.description}")),
936 Box::new(BarColumn::new()),
937 Box::new(TaskProgressColumn::new(show_speed)),
938 Box::new(TimeRemainingColumn::new(false)),
939 ];
940 Self::new(columns, live_options, disable, expand)
941 }
942
943 pub fn start(&mut self) -> io::Result<()> {
944 if self.disable {
945 return Ok(());
946 }
947 self.live.start(true)?;
948 self.started.store(true, Ordering::SeqCst);
949 self.flush_deferred_console_calls()
950 }
951
952 pub fn stop(&mut self) -> io::Result<()> {
953 if self.disable {
954 return Ok(());
955 }
956 self.started.store(false, Ordering::SeqCst);
957 self.live.stop()
958 }
959
960 pub fn refresh(&self) -> io::Result<()> {
961 if self.disable {
962 return Ok(());
963 }
964 self.live.refresh()
965 }
966
967 pub fn add_task(
968 &self,
969 description: &str,
970 start: bool,
971 total: Option<f64>,
972 completed: f64,
973 visible: bool,
974 ) -> TaskID {
975 let id = {
976 let mut state = self.state.lock().expect("progress state mutex poisoned");
977 let id = TaskID(state.next_id);
978 state.next_id += 1;
979
980 let task = ProgressTask {
981 id,
982 description: description.to_string(),
983 total,
984 completed,
985 visible,
986 fields: HashMap::new(),
987 finished_time: None,
988 finished_speed: None,
989 start_time: None,
990 stop_time: None,
991 progress: VecDeque::with_capacity(1000),
992 };
993
994 state.order.push(id);
995 state.tasks.insert(id, task);
996 id
997 };
998 if start {
999 self.start_task(id);
1000 }
1001 let _ = self.refresh();
1002 id
1003 }
1004
1005 pub fn remove_task(&self, task_id: TaskID) {
1006 let mut state = self.state.lock().expect("progress state mutex poisoned");
1007 state.tasks.remove(&task_id);
1008 state.order.retain(|&id| id != task_id);
1009 state.cell_cache.retain(|(id, _), _| *id != task_id);
1010 }
1011
1012 pub fn start_task(&self, task_id: TaskID) {
1013 let mut state = self.state.lock().expect("progress state mutex poisoned");
1014 let now = state.now();
1015 if let Some(task) = state.tasks.get_mut(&task_id) {
1016 if task.start_time.is_none() {
1017 task.start_time = Some(now);
1018 }
1019 }
1020 }
1021
1022 pub fn stop_task(&self, task_id: TaskID) {
1023 let mut state = self.state.lock().expect("progress state mutex poisoned");
1024 let now = state.now();
1025 if let Some(task) = state.tasks.get_mut(&task_id) {
1026 if task.start_time.is_none() {
1027 task.start_time = Some(now);
1028 }
1029 task.stop_time = Some(now);
1030 }
1031 }
1032
1033 pub fn advance(&self, task_id: TaskID, advance: f64) {
1034 let mut state = self.state.lock().expect("progress state mutex poisoned");
1035 let now = state.now();
1036 let speed_estimate_period = state.speed_estimate_period;
1037 let Some(task) = state.tasks.get_mut(&task_id) else {
1038 return;
1039 };
1040
1041 let completed_start = task.completed;
1042 task.completed += advance;
1043 let update_completed = task.completed - completed_start;
1044
1045 let old_sample_time = now - speed_estimate_period;
1046 while let Some(front) = task.progress.front() {
1047 if front.timestamp < old_sample_time {
1048 task.progress.pop_front();
1049 } else {
1050 break;
1051 }
1052 }
1053 while task.progress.len() > 1000 {
1054 task.progress.pop_front();
1055 }
1056 task.progress.push_back(ProgressSample {
1057 timestamp: now,
1058 completed: update_completed,
1059 });
1060
1061 if let Some(total) = task.total {
1062 if task.completed >= total && task.finished_time.is_none() {
1063 task.finished_time = task.elapsed(now);
1064 task.finished_speed = task.speed();
1065 }
1066 }
1067 }
1068
1069 pub fn update(
1070 &self,
1071 task_id: TaskID,
1072 total: Option<Option<f64>>,
1073 completed: Option<f64>,
1074 advance: Option<f64>,
1075 description: Option<String>,
1076 visible: Option<bool>,
1077 refresh: bool,
1078 fields: Option<HashMap<String, String>>,
1079 ) {
1080 let mut state = self.state.lock().expect("progress state mutex poisoned");
1081 let now = state.now();
1082 let speed_estimate_period = state.speed_estimate_period;
1083 let Some(task) = state.tasks.get_mut(&task_id) else {
1084 return;
1085 };
1086 let completed_start = task.completed;
1087
1088 if let Some(total) = total {
1089 if task.total != total {
1090 task.total = total;
1091 task.progress.clear();
1092 task.finished_time = None;
1093 task.finished_speed = None;
1094 }
1095 }
1096 if let Some(advance) = advance {
1097 task.completed += advance;
1098 }
1099 if let Some(completed) = completed {
1100 task.completed = completed;
1101 }
1102 if let Some(description) = description {
1103 task.description = description;
1104 }
1105 if let Some(visible) = visible {
1106 task.visible = visible;
1107 }
1108 if let Some(fields) = fields {
1109 task.fields.extend(fields);
1110 }
1111
1112 let update_completed = task.completed - completed_start;
1113 let old_sample_time = now - speed_estimate_period;
1114 while let Some(front) = task.progress.front() {
1115 if front.timestamp < old_sample_time {
1116 task.progress.pop_front();
1117 } else {
1118 break;
1119 }
1120 }
1121 while task.progress.len() > 1000 {
1122 task.progress.pop_front();
1123 }
1124 if update_completed > 0.0 {
1125 task.progress.push_back(ProgressSample {
1126 timestamp: now,
1127 completed: update_completed,
1128 });
1129 }
1130
1131 if let Some(total) = task.total {
1132 if task.completed >= total && task.finished_time.is_none() {
1133 task.finished_time = task.elapsed(now);
1134 }
1135 }
1136 drop(state);
1137 if refresh {
1138 let _ = self.refresh();
1139 }
1140 }
1141
1142 pub fn update_task(
1143 &self,
1144 task_id: TaskID,
1145 total: Option<Option<f64>>,
1146 completed: Option<f64>,
1147 description: Option<String>,
1148 visible: Option<bool>,
1149 ) {
1150 self.update(
1151 task_id,
1152 total,
1153 completed,
1154 None,
1155 description,
1156 visible,
1157 false,
1158 None,
1159 );
1160 }
1161
1162 pub fn reset(
1163 &self,
1164 task_id: TaskID,
1165 start: bool,
1166 total: Option<Option<f64>>,
1167 completed: f64,
1168 visible: Option<bool>,
1169 description: Option<String>,
1170 fields: Option<HashMap<String, String>>,
1171 ) {
1172 let mut state = self.state.lock().expect("progress state mutex poisoned");
1173 let now = state.now();
1174 let Some(task) = state.tasks.get_mut(&task_id) else {
1175 return;
1176 };
1177 task.progress.clear();
1178 task.finished_time = None;
1179 task.finished_speed = None;
1180 task.start_time = if start { Some(now) } else { None };
1181 if let Some(total) = total {
1182 task.total = total;
1183 }
1184 task.completed = completed;
1185 if let Some(visible) = visible {
1186 task.visible = visible;
1187 }
1188 if let Some(fields) = fields {
1189 task.fields = fields;
1190 }
1191 if let Some(description) = description {
1192 task.description = description;
1193 }
1194 task.finished_time = None;
1195 drop(state);
1196 let _ = self.refresh();
1197 }
1198
1199 pub fn track<'a, I>(
1200 &'a self,
1201 iter: I,
1202 task_id: TaskID,
1203 update_period: Duration,
1204 ) -> ProgressIterator<'a, I::IntoIter>
1205 where
1206 I: IntoIterator,
1207 {
1208 ProgressIterator {
1209 iter: iter.into_iter(),
1210 progress: self,
1211 task_id,
1212 track_thread: TrackThread::new(
1213 self.auto_refresh && !self.disable,
1214 self.state.clone(),
1215 self.live.started_flag(),
1216 task_id,
1217 update_period,
1218 ),
1219 pending_increment: false,
1220 }
1221 }
1222
1223 pub fn track_iter<'a, I>(
1225 &'a self,
1226 iter: I,
1227 description: &str,
1228 total: Option<f64>,
1229 completed: f64,
1230 update_period: Duration,
1231 ) -> ProgressIterator<'a, I::IntoIter>
1232 where
1233 I: IntoIterator,
1234 {
1235 let task_id = self.add_task(description, true, total, completed, true);
1236 self.track(iter, task_id, update_period)
1237 }
1238
1239 pub fn track_sequence<'a, I>(
1240 &'a self,
1241 sequence: I,
1242 config: TrackConfig,
1243 ) -> ProgressIterator<'a, I::IntoIter>
1244 where
1245 I: IntoIterator,
1246 {
1247 let iter = sequence.into_iter();
1248 let inferred_total = config.total.or_else(|| {
1249 let (lower, upper) = iter.size_hint();
1250 let hint = upper.or(Some(lower)).unwrap_or(0);
1251 if hint == 0 { None } else { Some(hint as f64) }
1252 });
1253
1254 let task_id = if let Some(task_id) = config.task_id {
1255 self.update(
1256 task_id,
1257 Some(inferred_total),
1259 Some(config.completed),
1260 None,
1261 None,
1262 None,
1263 false,
1264 None,
1265 );
1266 task_id
1267 } else {
1268 self.add_task(
1269 &config.description,
1270 true,
1271 inferred_total,
1272 config.completed,
1273 true,
1274 )
1275 };
1276
1277 self.track(iter, task_id, config.update_period)
1278 }
1279
1280 fn render_to_deferred_segments<R: Renderable + ?Sized>(renderable: &R) -> Segments {
1281 let options = ConsoleOptions::default();
1282 let temp_console = Console::<Stdout>::with_options(options.clone());
1283 renderable.render(&temp_console, &options)
1284 }
1285
1286 fn flush_deferred_console_calls(&self) -> io::Result<()> {
1287 let calls = {
1288 let mut deferred = self
1289 .deferred_console_calls
1290 .lock()
1291 .expect("deferred console calls mutex poisoned");
1292 deferred.drain(..).collect::<Vec<_>>()
1293 };
1294
1295 for call in calls {
1296 match call {
1297 DeferredConsoleCall::Print {
1298 segments,
1299 style,
1300 justify,
1301 overflow,
1302 no_wrap,
1303 end,
1304 } => {
1305 let renderable = SegmentsCell::new(segments);
1306 self.live
1307 .print(&renderable, style, justify, overflow, no_wrap, &end)?;
1308 }
1309 DeferredConsoleCall::Log {
1310 segments,
1311 file,
1312 line,
1313 } => {
1314 let renderable = SegmentsCell::new(segments);
1315 self.live.log(&renderable, file.as_deref(), line)?;
1316 }
1317 }
1318 }
1319
1320 Ok(())
1321 }
1322
1323 pub fn print<R: Renderable + ?Sized>(
1324 &self,
1325 renderable: &R,
1326 style: Option<Style>,
1327 justify: Option<JustifyMethod>,
1328 overflow: Option<OverflowMethod>,
1329 no_wrap: bool,
1330 end: &str,
1331 ) -> io::Result<()> {
1332 if self.disable {
1333 return Ok(());
1334 }
1335
1336 if self.started.load(Ordering::SeqCst) {
1337 return self
1338 .live
1339 .print(renderable, style, justify, overflow, no_wrap, end);
1340 }
1341
1342 let mut deferred = self
1343 .deferred_console_calls
1344 .lock()
1345 .expect("deferred console calls mutex poisoned");
1346 deferred.push(DeferredConsoleCall::Print {
1347 segments: Self::render_to_deferred_segments(renderable),
1348 style,
1349 justify,
1350 overflow,
1351 no_wrap,
1352 end: end.to_string(),
1353 });
1354 Ok(())
1355 }
1356
1357 pub fn log<R: Renderable + ?Sized>(
1358 &self,
1359 renderable: &R,
1360 file: Option<&str>,
1361 line: Option<u32>,
1362 ) -> io::Result<()> {
1363 if self.disable {
1364 return Ok(());
1365 }
1366
1367 if self.started.load(Ordering::SeqCst) {
1368 return self.live.log(renderable, file, line);
1369 }
1370
1371 let mut deferred = self
1372 .deferred_console_calls
1373 .lock()
1374 .expect("deferred console calls mutex poisoned");
1375 deferred.push(DeferredConsoleCall::Log {
1376 segments: Self::render_to_deferred_segments(renderable),
1377 file: file.map(ToString::to_string),
1378 line,
1379 });
1380 Ok(())
1381 }
1382}
1383
1384impl Progress {
1385 pub fn tasks(&self) -> Vec<ProgressTask> {
1387 let state = self.state.lock().expect("progress state mutex poisoned");
1388 state
1389 .order
1390 .iter()
1391 .filter_map(|id| state.tasks.get(id).cloned())
1392 .collect()
1393 }
1394
1395 pub fn task_ids(&self) -> Vec<TaskID> {
1397 let state = self.state.lock().expect("progress state mutex poisoned");
1398 state.order.clone()
1399 }
1400
1401 pub fn finished(&self) -> bool {
1403 let state = self.state.lock().expect("progress state mutex poisoned");
1404 state.tasks.values().all(|t| t.finished())
1405 }
1406}
1407
1408impl Drop for Progress {
1409 fn drop(&mut self) {
1410 let _ = self.stop();
1411 }
1412}
1413
1414impl Renderable for Progress {
1415 fn render(&self, console: &Console, options: &ConsoleOptions) -> Segments {
1416 let renderable = ProgressRenderable {
1418 state: self.state.clone(),
1419 columns: self.columns.clone(),
1420 };
1421 renderable.render(console, options)
1422 }
1423}
1424
1425pub struct ProgressReader<'a, R: Read> {
1452 inner: R,
1453 progress: &'a Progress,
1454 task_id: TaskID,
1455 #[allow(dead_code)]
1459 close_handle: bool,
1460}
1461
1462impl<'a, R: Read> ProgressReader<'a, R> {
1463 pub fn new(inner: R, progress: &'a Progress, task_id: TaskID, close_handle: bool) -> Self {
1468 Self {
1469 inner,
1470 progress,
1471 task_id,
1472 close_handle,
1473 }
1474 }
1475
1476 pub fn task_id(&self) -> TaskID {
1478 self.task_id
1479 }
1480
1481 pub fn inner(&self) -> &R {
1483 &self.inner
1484 }
1485
1486 pub fn inner_mut(&mut self) -> &mut R {
1488 &mut self.inner
1489 }
1490
1491 pub fn into_inner(self) -> R {
1493 self.inner
1494 }
1495}
1496
1497impl<R: Read> Read for ProgressReader<'_, R> {
1498 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
1499 let n = self.inner.read(buf)?;
1500 self.progress.advance(self.task_id, n as f64);
1501 Ok(n)
1502 }
1503}
1504
1505impl<R: Read + BufRead> BufRead for ProgressReader<'_, R> {
1506 fn fill_buf(&mut self) -> io::Result<&[u8]> {
1507 self.inner.fill_buf()
1508 }
1509
1510 fn consume(&mut self, amt: usize) {
1511 self.inner.consume(amt);
1512 self.progress.advance(self.task_id, amt as f64);
1513 }
1514}
1515
1516impl<R: Read + Seek> Seek for ProgressReader<'_, R> {
1517 fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
1518 let new_pos = self.inner.seek(pos)?;
1519 self.progress.update(
1521 self.task_id,
1522 None,
1523 Some(new_pos as f64),
1524 None,
1525 None,
1526 None,
1527 false,
1528 None,
1529 );
1530 Ok(new_pos)
1531 }
1532}
1533
1534pub struct WrapFileBuilder<'a, R: Read> {
1553 progress: &'a Progress,
1554 reader: R,
1555 total: Option<u64>,
1556 task_id: Option<TaskID>,
1557 description: String,
1558 close_handle: bool,
1559}
1560
1561impl<'a, R: Read> WrapFileBuilder<'a, R> {
1562 pub fn new(progress: &'a Progress, reader: R) -> Self {
1564 Self {
1565 progress,
1566 reader,
1567 total: None,
1568 task_id: None,
1569 description: "Reading...".to_string(),
1570 close_handle: false,
1571 }
1572 }
1573
1574 pub fn total(mut self, total: u64) -> Self {
1576 self.total = Some(total);
1577 self
1578 }
1579
1580 pub fn total_opt(mut self, total: Option<u64>) -> Self {
1582 self.total = total;
1583 self
1584 }
1585
1586 pub fn task_id(mut self, task_id: TaskID) -> Self {
1588 self.task_id = Some(task_id);
1589 self
1590 }
1591
1592 pub fn description(mut self, description: impl Into<String>) -> Self {
1594 self.description = description.into();
1595 self
1596 }
1597
1598 pub fn close_handle(mut self, close: bool) -> Self {
1600 self.close_handle = close;
1601 self
1602 }
1603
1604 pub fn build(self) -> ProgressReader<'a, R> {
1609 let task_id = if let Some(id) = self.task_id {
1610 if let Some(total) = self.total {
1612 self.progress.update(
1613 id,
1614 Some(Some(total as f64)),
1615 None,
1616 None,
1617 None,
1618 None,
1619 false,
1620 None,
1621 );
1622 }
1623 id
1624 } else {
1625 self.progress.add_task(
1627 &self.description,
1628 true,
1629 self.total.map(|t| t as f64),
1630 0.0,
1631 true,
1632 )
1633 };
1634
1635 ProgressReader::new(self.reader, self.progress, task_id, self.close_handle)
1636 }
1637}
1638
1639impl Progress {
1640 pub fn wrap_file<'a, R: Read>(
1667 &'a self,
1668 reader: R,
1669 total: Option<u64>,
1670 description: &str,
1671 ) -> ProgressReader<'a, R> {
1672 WrapFileBuilder::new(self, reader)
1673 .total_opt(total)
1674 .description(description)
1675 .close_handle(false)
1676 .build()
1677 }
1678
1679 pub fn wrap_file_with_task<'a, R: Read>(
1690 &'a self,
1691 reader: R,
1692 task_id: TaskID,
1693 total: Option<u64>,
1694 ) -> ProgressReader<'a, R> {
1695 WrapFileBuilder::new(self, reader)
1696 .task_id(task_id)
1697 .total_opt(total)
1698 .close_handle(false)
1699 .build()
1700 }
1701
1702 pub fn open<'a, P: AsRef<Path>>(
1734 &'a self,
1735 path: P,
1736 description: &str,
1737 ) -> io::Result<ProgressReader<'a, File>> {
1738 let path = path.as_ref();
1739 let file = File::open(path)?;
1740 let metadata = file.metadata()?;
1741 let total = metadata.len();
1742
1743 Ok(WrapFileBuilder::new(self, file)
1744 .total(total)
1745 .description(description)
1746 .close_handle(true)
1747 .build())
1748 }
1749
1750 pub fn open_with_task<'a, P: AsRef<Path>>(
1765 &'a self,
1766 path: P,
1767 task_id: TaskID,
1768 ) -> io::Result<ProgressReader<'a, File>> {
1769 let path = path.as_ref();
1770 let file = File::open(path)?;
1771 let metadata = file.metadata()?;
1772 let total = metadata.len();
1773
1774 Ok(WrapFileBuilder::new(self, file)
1775 .task_id(task_id)
1776 .total(total)
1777 .close_handle(true)
1778 .build())
1779 }
1780}
1781
1782pub struct ProgressIterator<'a, I> {
1783 iter: I,
1784 progress: &'a Progress,
1785 task_id: TaskID,
1786 track_thread: TrackThread,
1787 pending_increment: bool,
1788}
1789
1790struct TrackThread {
1791 enabled: bool,
1792 completed: Arc<AtomicUsize>,
1793 done: Arc<AtomicBool>,
1794 handle: Option<thread::JoinHandle<()>>,
1795}
1796
1797impl TrackThread {
1798 fn new(
1799 enabled: bool,
1800 state: Arc<Mutex<ProgressState>>,
1801 live_started: Arc<AtomicBool>,
1802 task_id: TaskID,
1803 update_period: Duration,
1804 ) -> Self {
1805 if !enabled {
1806 return Self {
1807 enabled: false,
1808 completed: Arc::new(AtomicUsize::new(0)),
1809 done: Arc::new(AtomicBool::new(false)),
1810 handle: None,
1811 };
1812 }
1813
1814 let update_period = update_period.max(Duration::from_millis(1));
1815
1816 let completed = Arc::new(AtomicUsize::new(0));
1817 let done = Arc::new(AtomicBool::new(false));
1818 let completed_thread = completed.clone();
1819 let done_thread = done.clone();
1820
1821 let handle = thread::spawn(move || {
1822 let mut last_completed: usize = 0;
1823 while !done_thread.load(Ordering::SeqCst) && live_started.load(Ordering::SeqCst) {
1824 thread::sleep(update_period);
1825 if done_thread.load(Ordering::SeqCst) || !live_started.load(Ordering::SeqCst) {
1826 break;
1827 }
1828 let current = completed_thread.load(Ordering::SeqCst);
1829 if current != last_completed {
1830 let delta = (current - last_completed) as f64;
1831 last_completed = current;
1832 advance_state(&state, task_id, delta);
1833 }
1834 }
1835 });
1836
1837 Self {
1838 enabled: true,
1839 completed,
1840 done,
1841 handle: Some(handle),
1842 }
1843 }
1844
1845 fn increment(&self) {
1846 if self.enabled {
1847 self.completed.fetch_add(1, Ordering::SeqCst);
1848 }
1849 }
1850
1851 fn take_completed(&self) -> usize {
1852 self.completed.load(Ordering::SeqCst)
1853 }
1854
1855 fn stop(&mut self) {
1856 if !self.enabled {
1857 return;
1858 }
1859 self.done.store(true, Ordering::SeqCst);
1860 if let Some(handle) = self.handle.take() {
1861 let _ = handle.join();
1862 }
1863 }
1864}
1865
1866impl Drop for TrackThread {
1867 fn drop(&mut self) {
1868 self.stop();
1869 }
1870}
1871
1872impl<'a, I> Iterator for ProgressIterator<'a, I>
1873where
1874 I: Iterator,
1875{
1876 type Item = I::Item;
1877
1878 fn next(&mut self) -> Option<Self::Item> {
1879 if !self.progress.disable && self.pending_increment {
1880 if self.progress.auto_refresh {
1881 self.track_thread.increment();
1882 } else {
1883 self.progress.advance(self.task_id, 1.0);
1884 let _ = self.progress.refresh();
1885 }
1886 }
1887
1888 let item = self.iter.next();
1889 self.pending_increment = item.is_some();
1890 item
1891 }
1892}
1893
1894impl<'a, I> Drop for ProgressIterator<'a, I> {
1895 fn drop(&mut self) {
1896 if self.progress.disable {
1897 return;
1898 }
1899 if self.progress.auto_refresh {
1900 let completed = self.track_thread.take_completed() as f64;
1901 self.track_thread.stop();
1902 self.progress.update(
1903 self.task_id,
1904 None,
1905 Some(completed),
1906 None,
1907 None,
1908 None,
1909 true,
1910 None,
1911 );
1912 }
1913 }
1914}
1915
1916#[derive(Debug, Clone)]
1917pub struct TrackConfig {
1918 pub total: Option<f64>,
1919 pub completed: f64,
1920 pub task_id: Option<TaskID>,
1921 pub description: String,
1922 pub update_period: Duration,
1923}
1924
1925impl TrackConfig {
1926 pub fn new(description: impl Into<String>) -> Self {
1927 Self {
1928 total: None,
1929 completed: 0.0,
1930 task_id: None,
1931 description: description.into(),
1932 update_period: Duration::from_millis(100),
1933 }
1934 }
1935
1936 pub fn with_total(mut self, total: Option<f64>) -> Self {
1937 self.total = total;
1938 self
1939 }
1940
1941 pub fn with_completed(mut self, completed: f64) -> Self {
1942 self.completed = completed;
1943 self
1944 }
1945
1946 pub fn with_task_id(mut self, task_id: TaskID) -> Self {
1947 self.task_id = Some(task_id);
1948 self
1949 }
1950
1951 pub fn with_update_period(mut self, update_period: Duration) -> Self {
1952 self.update_period = update_period;
1953 self
1954 }
1955}
1956
1957fn advance_state(state: &Arc<Mutex<ProgressState>>, task_id: TaskID, advance: f64) {
1958 let mut state = state.lock().expect("progress state mutex poisoned");
1959 let now = state.now();
1960 let speed_estimate_period = state.speed_estimate_period;
1961 let Some(task) = state.tasks.get_mut(&task_id) else {
1962 return;
1963 };
1964
1965 let completed_start = task.completed;
1966 task.completed += advance;
1967 let update_completed = task.completed - completed_start;
1968
1969 let old_sample_time = now - speed_estimate_period;
1970 while let Some(front) = task.progress.front() {
1971 if front.timestamp < old_sample_time {
1972 task.progress.pop_front();
1973 } else {
1974 break;
1975 }
1976 }
1977 while task.progress.len() > 1000 {
1978 task.progress.pop_front();
1979 }
1980 task.progress.push_back(ProgressSample {
1981 timestamp: now,
1982 completed: update_completed,
1983 });
1984
1985 if let Some(total) = task.total {
1986 if task.completed >= total && task.finished_time.is_none() {
1987 task.finished_time = task.elapsed(now);
1988 task.finished_speed = task.speed();
1989 }
1990 }
1991}
1992
1993fn format_task_template(template: &str, task: &ProgressTask, now: f64) -> String {
1994 let mut out = String::new();
2005 let mut rest = template;
2006 while let Some(start) = rest.find("{task.") {
2007 out.push_str(&rest[..start]);
2008 let after = &rest[start + 6..];
2009 let Some(end) = after.find('}') else {
2010 out.push_str(rest);
2011 return out;
2012 };
2013 let inside = &after[..end];
2014 let (field, spec) = inside.split_once(':').unwrap_or((inside, ""));
2015 let formatted = format_task_field(field, spec, task, now);
2016 out.push_str(&formatted);
2017 rest = &after[end + 1..];
2018 }
2019 out.push_str(rest);
2020 out
2021}
2022
2023#[derive(Debug, Default, Clone, Copy)]
2024struct FormatSpec {
2025 align: Option<char>,
2026 width: Option<usize>,
2027 precision: Option<usize>,
2028 ty: Option<char>,
2029}
2030
2031fn parse_format_spec(spec: &str) -> FormatSpec {
2032 let mut s = spec;
2033 let mut out = FormatSpec::default();
2034
2035 if let Some(first) = s.chars().next() {
2036 if matches!(first, '<' | '>' | '^') {
2037 out.align = Some(first);
2038 s = &s[first.len_utf8()..];
2039 }
2040 }
2041
2042 let mut width_end = 0;
2044 for (i, ch) in s.char_indices() {
2045 if ch.is_ascii_digit() {
2046 width_end = i + 1;
2047 } else {
2048 break;
2049 }
2050 }
2051 if width_end > 0 {
2052 if let Ok(w) = s[..width_end].parse::<usize>() {
2053 out.width = Some(w);
2054 }
2055 s = &s[width_end..];
2056 }
2057
2058 if let Some(rest) = s.strip_prefix('.') {
2060 let mut prec_end = 0;
2061 for (i, ch) in rest.char_indices() {
2062 if ch.is_ascii_digit() {
2063 prec_end = i + 1;
2064 } else {
2065 break;
2066 }
2067 }
2068 if prec_end > 0 {
2069 if let Ok(p) = rest[..prec_end].parse::<usize>() {
2070 out.precision = Some(p);
2071 }
2072 s = &rest[prec_end..];
2073 }
2074 }
2075
2076 if let Some(last) = s.chars().next() {
2078 if matches!(last, 'f' | 'd') {
2079 out.ty = Some(last);
2080 }
2081 }
2082
2083 out
2084}
2085
2086fn pad_aligned(text: &str, width: usize, align: char) -> String {
2087 let current = crate::cells::cell_len(text);
2088 if current >= width {
2089 return text.to_string();
2090 }
2091 let pad = width - current;
2092 match align {
2093 '<' => format!("{text}{}", " ".repeat(pad)),
2094 '^' => {
2095 let left = pad / 2;
2096 let right = pad - left;
2097 format!("{}{}{}", " ".repeat(left), text, " ".repeat(right))
2098 }
2099 _ => format!("{}{}", " ".repeat(pad), text),
2100 }
2101}
2102
2103fn apply_format_spec_value(text: String, spec: &FormatSpec) -> String {
2104 let Some(width) = spec.width else {
2105 return text;
2106 };
2107 let align = spec.align.unwrap_or('>');
2108 pad_aligned(&text, width, align)
2109}
2110
2111fn format_task_field(field: &str, spec: &str, task: &ProgressTask, now: f64) -> String {
2112 let spec = parse_format_spec(spec);
2113
2114 let value = match field {
2115 "description" => return apply_format_spec_value(task.description.clone(), &spec),
2116 "percentage" => Some(task.percentage()),
2117 "completed" => Some(task.completed),
2118 "total" => task.total,
2119 "elapsed" => task.elapsed(now),
2120 "remaining" => task.remaining(),
2121 "speed" => task.speed(),
2122 "time_remaining" => task.time_remaining(),
2123 _ => None,
2124 };
2125
2126 let Some(value) = value else {
2127 return apply_format_spec_value(String::new(), &spec);
2128 };
2129
2130 let rendered = match spec.ty {
2131 Some('d') => format!("{}", value as i64),
2132 Some('f') => {
2133 let precision = spec.precision.unwrap_or(0);
2134 format!("{value:.precision$}", precision = precision)
2135 }
2136 _ => {
2137 if let Some(precision) = spec.precision {
2138 format!("{value:.precision$}", precision = precision)
2139 } else {
2140 if value.fract() == 0.0 {
2142 format!("{:.0}", value)
2143 } else {
2144 format!("{value}")
2145 }
2146 }
2147 }
2148 };
2149
2150 apply_format_spec_value(rendered, &spec)
2151}
2152
2153#[cfg(test)]
2154mod tests {
2155 use super::*;
2156
2157 #[test]
2158 fn test_format_task_template_description() {
2159 let task = ProgressTask {
2160 id: TaskID(1),
2161 description: "Hello".to_string(),
2162 total: Some(10.0),
2163 completed: 3.0,
2164 visible: true,
2165 fields: HashMap::new(),
2166 finished_time: None,
2167 finished_speed: None,
2168 start_time: Some(0.0),
2169 stop_time: None,
2170 progress: VecDeque::new(),
2171 };
2172
2173 let s = format_task_template("x {task.description} y", &task, 1.0);
2174 assert_eq!(s, "x Hello y");
2175 }
2176
2177 #[test]
2178 fn test_apply_format_spec_right_align() {
2179 let spec = parse_format_spec(">3");
2180 assert_eq!(apply_format_spec_value("7".to_string(), &spec), " 7");
2181 assert_eq!(apply_format_spec_value("1234".to_string(), &spec), "1234");
2182 }
2183
2184 #[test]
2185 fn test_progress_update_appends_samples_only_on_positive_change() {
2186 let live_options = LiveOptions {
2187 auto_refresh: true,
2188 ..Default::default()
2189 };
2190 let progress = Progress::new_default(live_options, true, false, false);
2191
2192 let task_id = progress.add_task("t", true, Some(10.0), 0.0, true);
2193
2194 {
2195 let state = progress
2196 .state
2197 .lock()
2198 .expect("progress state mutex poisoned");
2199 let task = state.tasks.get(&task_id).unwrap();
2200 assert_eq!(task.progress.len(), 0);
2201 }
2202
2203 progress.update(task_id, None, None, Some(1.0), None, None, false, None);
2204 {
2205 let state = progress
2206 .state
2207 .lock()
2208 .expect("progress state mutex poisoned");
2209 let task = state.tasks.get(&task_id).unwrap();
2210 assert_eq!(task.completed, 1.0);
2211 assert_eq!(task.progress.len(), 1);
2212 }
2213
2214 progress.update(task_id, None, Some(1.0), None, None, None, false, None);
2216 {
2217 let state = progress
2218 .state
2219 .lock()
2220 .expect("progress state mutex poisoned");
2221 let task = state.tasks.get(&task_id).unwrap();
2222 assert_eq!(task.progress.len(), 1);
2223 }
2224 }
2225
2226 #[test]
2227 fn test_progress_update_total_change_resets_speed_samples() {
2228 let live_options = LiveOptions {
2229 auto_refresh: true,
2230 ..Default::default()
2231 };
2232 let progress = Progress::new_default(live_options, true, false, false);
2233
2234 let task_id = progress.add_task("t", true, Some(10.0), 0.0, true);
2235 progress.update(task_id, None, None, Some(2.0), None, None, false, None);
2236 {
2237 let state = progress
2238 .state
2239 .lock()
2240 .expect("progress state mutex poisoned");
2241 let task = state.tasks.get(&task_id).unwrap();
2242 assert_eq!(task.progress.len(), 1);
2243 }
2244
2245 progress.update(
2247 task_id,
2248 Some(Some(20.0)),
2249 None,
2250 None,
2251 None,
2252 None,
2253 false,
2254 None,
2255 );
2256 {
2257 let state = progress
2258 .state
2259 .lock()
2260 .expect("progress state mutex poisoned");
2261 let task = state.tasks.get(&task_id).unwrap();
2262 assert_eq!(task.total, Some(20.0));
2263 assert_eq!(task.progress.len(), 0);
2264 }
2265 }
2266
2267 #[test]
2268 fn test_text_column_defaults_to_no_wrap() {
2269 let col = TextColumn::new("{task.description}").table_column();
2270 assert!(col.no_wrap);
2271 }
2272
2273 #[test]
2274 fn test_task_progress_column_empty_when_total_unknown() {
2275 let task = ProgressTask {
2276 id: TaskID(0),
2277 description: "t".to_string(),
2278 total: None,
2279 completed: 0.0,
2280 visible: true,
2281 fields: HashMap::new(),
2282 finished_time: None,
2283 finished_speed: None,
2284 start_time: Some(0.0),
2285 stop_time: None,
2286 progress: VecDeque::new(),
2287 };
2288
2289 let console = Console::new();
2290 let options = ConsoleOptions::default();
2291 let col = TaskProgressColumn::new(false);
2292 let rendered = col.render(&task, 0.0, &options);
2293 let segs = rendered.render(&console, &options);
2294 let text: String = segs.iter().map(|s| s.text.as_ref()).collect();
2295 assert_eq!(text, "");
2296 }
2297
2298 #[test]
2299 fn test_bar_column_defaults_to_width_40() {
2300 let task = ProgressTask {
2301 id: TaskID(0),
2302 description: "t".to_string(),
2303 total: Some(100.0),
2304 completed: 50.0,
2305 visible: true,
2306 fields: HashMap::new(),
2307 finished_time: None,
2308 finished_speed: None,
2309 start_time: Some(0.0),
2310 stop_time: None,
2311 progress: VecDeque::new(),
2312 };
2313
2314 let console = Console::new();
2315 let options = ConsoleOptions {
2316 max_width: 120,
2317 ..ConsoleOptions::default()
2318 };
2319 let col = BarColumn::new();
2320 let rendered = col.render(&task, 0.0, &options);
2321 let measurement = rendered.measure(&console, &options);
2322 assert_eq!(measurement.minimum, 40);
2323 assert_eq!(measurement.maximum, 40);
2324 }
2325
2326 #[test]
2327 fn test_track_sequence_updates_total_to_none_for_existing_task() {
2328 let live_options = LiveOptions {
2329 auto_refresh: true,
2330 ..Default::default()
2331 };
2332 let progress = Progress::new_default(live_options, true, false, false);
2333 let task_id = progress.add_task("t", true, Some(10.0), 0.0, true);
2334
2335 let config = TrackConfig {
2336 total: None,
2337 completed: 0.0,
2338 task_id: Some(task_id),
2339 description: "t".to_string(),
2340 update_period: Duration::from_millis(10),
2341 };
2342
2343 let _iter = progress.track_sequence(std::iter::empty::<usize>(), config);
2344 let state = progress
2345 .state
2346 .lock()
2347 .expect("progress state mutex poisoned");
2348 let task = state.tasks.get(&task_id).unwrap();
2349 assert_eq!(task.total, None);
2350 }
2351
2352 #[test]
2353 fn test_progress_reader_advances_on_read() {
2354 use std::io::Cursor;
2355
2356 let live_options = LiveOptions {
2357 auto_refresh: true,
2358 ..Default::default()
2359 };
2360 let progress = Progress::new_default(live_options, true, false, false);
2361
2362 let data = b"Hello, World!";
2363 let cursor = Cursor::new(data.to_vec());
2364
2365 let mut reader = progress.wrap_file(cursor, Some(data.len() as u64), "Reading");
2366 let task_id = reader.task_id();
2367
2368 {
2370 let state = progress.state.lock().expect("mutex poisoned");
2371 let task = state.tasks.get(&task_id).unwrap();
2372 assert_eq!(task.completed, 0.0);
2373 assert_eq!(task.total, Some(data.len() as f64));
2374 }
2375
2376 let mut buf = [0u8; 5];
2378 let n = reader.read(&mut buf).unwrap();
2379 assert_eq!(n, 5);
2380
2381 {
2383 let state = progress.state.lock().expect("mutex poisoned");
2384 let task = state.tasks.get(&task_id).unwrap();
2385 assert_eq!(task.completed, 5.0);
2386 }
2387
2388 let mut buf = Vec::new();
2390 reader.read_to_end(&mut buf).unwrap();
2391
2392 {
2394 let state = progress.state.lock().expect("mutex poisoned");
2395 let task = state.tasks.get(&task_id).unwrap();
2396 assert_eq!(task.completed, data.len() as f64);
2397 }
2398 }
2399
2400 #[test]
2401 fn test_progress_reader_with_existing_task() {
2402 use std::io::Cursor;
2403
2404 let live_options = LiveOptions {
2405 auto_refresh: true,
2406 ..Default::default()
2407 };
2408 let progress = Progress::new_default(live_options, true, false, false);
2409
2410 let task_id = progress.add_task("Existing task", true, None, 0.0, true);
2412
2413 let data = b"Test data";
2414 let cursor = Cursor::new(data.to_vec());
2415
2416 let mut reader = progress.wrap_file_with_task(cursor, task_id, Some(data.len() as u64));
2418
2419 {
2421 let state = progress.state.lock().expect("mutex poisoned");
2422 let task = state.tasks.get(&task_id).unwrap();
2423 assert_eq!(task.total, Some(data.len() as f64));
2424 }
2425
2426 let mut buf = Vec::new();
2428 reader.read_to_end(&mut buf).unwrap();
2429
2430 {
2432 let state = progress.state.lock().expect("mutex poisoned");
2433 let task = state.tasks.get(&task_id).unwrap();
2434 assert_eq!(task.completed, data.len() as f64);
2435 }
2436 }
2437
2438 #[test]
2439 fn test_progress_reader_seek_updates_completed() {
2440 use std::io::Cursor;
2441
2442 let live_options = LiveOptions {
2443 auto_refresh: true,
2444 ..Default::default()
2445 };
2446 let progress = Progress::new_default(live_options, true, false, false);
2447
2448 let data = b"0123456789";
2449 let cursor = Cursor::new(data.to_vec());
2450
2451 let mut reader = progress.wrap_file(cursor, Some(data.len() as u64), "Seeking");
2452 let task_id = reader.task_id();
2453
2454 let mut buf = [0u8; 3];
2456 reader.read(&mut buf).unwrap();
2457
2458 {
2459 let state = progress.state.lock().expect("mutex poisoned");
2460 let task = state.tasks.get(&task_id).unwrap();
2461 assert_eq!(task.completed, 3.0);
2462 }
2463
2464 reader.seek(SeekFrom::Start(7)).unwrap();
2466
2467 {
2469 let state = progress.state.lock().expect("mutex poisoned");
2470 let task = state.tasks.get(&task_id).unwrap();
2471 assert_eq!(task.completed, 7.0);
2472 }
2473 }
2474
2475 #[test]
2476 fn test_wrap_file_builder() {
2477 use std::io::Cursor;
2478
2479 let live_options = LiveOptions {
2480 auto_refresh: true,
2481 ..Default::default()
2482 };
2483 let progress = Progress::new_default(live_options, true, false, false);
2484
2485 let data = b"Builder test";
2486 let cursor = Cursor::new(data.to_vec());
2487
2488 let reader = WrapFileBuilder::new(&progress, cursor)
2489 .total(data.len() as u64)
2490 .description("Custom description")
2491 .build();
2492
2493 let task_id = reader.task_id();
2494
2495 let state = progress.state.lock().expect("mutex poisoned");
2496 let task = state.tasks.get(&task_id).unwrap();
2497 assert_eq!(task.description, "Custom description");
2498 assert_eq!(task.total, Some(data.len() as f64));
2499 }
2500
2501 #[test]
2502 fn test_progress_reader_indeterminate() {
2503 use std::io::Cursor;
2504
2505 let live_options = LiveOptions {
2506 auto_refresh: true,
2507 ..Default::default()
2508 };
2509 let progress = Progress::new_default(live_options, true, false, false);
2510
2511 let data = b"Unknown size stream";
2512 let cursor = Cursor::new(data.to_vec());
2513
2514 let mut reader = progress.wrap_file(cursor, None, "Streaming");
2516 let task_id = reader.task_id();
2517
2518 {
2519 let state = progress.state.lock().expect("mutex poisoned");
2520 let task = state.tasks.get(&task_id).unwrap();
2521 assert_eq!(task.total, None);
2522 }
2523
2524 let mut buf = Vec::new();
2526 reader.read_to_end(&mut buf).unwrap();
2527
2528 {
2530 let state = progress.state.lock().expect("mutex poisoned");
2531 let task = state.tasks.get(&task_id).unwrap();
2532 assert_eq!(task.completed, data.len() as f64);
2533 }
2534 }
2535
2536 #[test]
2537 fn test_progress_print_and_log_are_deferred_until_start() {
2538 let mut console = Console::new();
2539 console.set_quiet(true);
2540
2541 let live_options = LiveOptions {
2542 auto_refresh: false,
2543 ..Default::default()
2544 };
2545 let mut progress = Progress::with_console(
2546 vec![Box::new(TextColumn::new("{task.description}"))],
2547 console,
2548 live_options,
2549 false,
2550 false,
2551 );
2552
2553 progress
2554 .print(&Text::plain("queued print"), None, None, None, false, "\n")
2555 .unwrap();
2556 progress
2557 .log(&Text::plain("queued log"), Some("queued.rs"), Some(12))
2558 .unwrap();
2559
2560 {
2561 let deferred = progress
2562 .deferred_console_calls
2563 .lock()
2564 .expect("deferred console calls mutex poisoned");
2565 assert_eq!(deferred.len(), 2);
2566 }
2567
2568 progress.start().unwrap();
2569
2570 let deferred = progress
2571 .deferred_console_calls
2572 .lock()
2573 .expect("deferred console calls mutex poisoned");
2574 assert!(deferred.is_empty());
2575 }
2576
2577 #[test]
2578 fn test_progress_print_and_log_do_not_queue_after_start() {
2579 let mut console = Console::new();
2580 console.set_quiet(true);
2581
2582 let live_options = LiveOptions {
2583 auto_refresh: false,
2584 ..Default::default()
2585 };
2586 let mut progress = Progress::with_console(
2587 vec![Box::new(TextColumn::new("{task.description}"))],
2588 console,
2589 live_options,
2590 false,
2591 false,
2592 );
2593
2594 progress.start().unwrap();
2595
2596 progress
2597 .print(&Text::plain("live print"), None, None, None, false, "\n")
2598 .unwrap();
2599 progress
2600 .log(&Text::plain("live log"), Some("live.rs"), Some(34))
2601 .unwrap();
2602
2603 let deferred = progress
2604 .deferred_console_calls
2605 .lock()
2606 .expect("deferred console calls mutex poisoned");
2607 assert!(deferred.is_empty());
2608 }
2609}