Skip to main content

rich/
progress.rs

1//! Progress displays.
2//!
3//! Port of `rich/progress.py`'s display and task model: a grid of tasks, one
4//! row each, whose cells come from a list of [`ProgressColumn`]s, laid out as
5//! upstream's `make_tasks_table` does: a [`Table::grid`] with `padding=(0, 1)`,
6//! each column's table-column options, and `expand`.
7//!
8//! Time is read from an injectable clock ([`Progress::clock`], upstream's
9//! `get_time`), so elapsed time, speed, ETA and spinner frames are
10//! deterministic under test. The default clock is monotonic.
11//!
12//! [`Progress::start`] runs the display live on the [`Live`](crate::live::Live)
13//! refresh thread ([`LiveProgress`]); [`LiveProgress::track`] and [`track`]
14//! port `track()`. `TextColumn` format strings use [`pyformat`].
15//!
16//! `transient` and `disable` apply to the live display; [`LiveProgress::wrap_read`]
17//! and [`LiveProgress::open`] port `wrap_file` and `open`.
18
19use std::cell::RefCell;
20use std::collections::{BTreeMap, HashMap, VecDeque};
21use std::sync::Arc;
22
23use crate::console::{Console, ConsoleOptions, Justify};
24use crate::filesize;
25use crate::progress_bar::ProgressBar;
26use crate::protocol::Renderable;
27use crate::pyformat::{self, FormatValue};
28use crate::segment::Segment;
29use crate::spinner::Spinner;
30use crate::style::{Style, StyleType};
31use crate::table::{Cell, ColumnOptions, Table};
32use crate::text::Text;
33
34/// Upstream keeps at most this many speed samples per task (`deque(maxlen=1000)`).
35const MAX_SAMPLES: usize = 1000;
36
37/// A source of the current time in seconds. Upstream's `GetTimeCallable`.
38pub use crate::console::GetTime;
39
40use crate::console::monotonic;
41
42/// Identifies a task within one [`Progress`]. Upstream's `TaskID`.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
44pub struct TaskId(pub usize);
45
46/// Estimated time remaining. Port of `TimeRemainingColumn`, including its
47/// half-second render cache (`max_refresh = 0.5`).
48pub struct TimeRemainingColumn {
49    compact: bool,
50    elapsed_when_finished: bool,
51    cache: RefCell<HashMap<TaskId, (f64, Text)>>,
52}
53
54impl TimeRemainingColumn {
55    /// `compact` drops the hours when there are none (`05:03`);
56    /// `elapsed_when_finished` shows the elapsed time once a task finishes.
57    pub fn new(compact: bool, elapsed_when_finished: bool) -> Self {
58        TimeRemainingColumn {
59            compact,
60            elapsed_when_finished,
61            cache: RefCell::new(HashMap::new()),
62        }
63    }
64}
65
66/// An animated spinner. Port of `SpinnerColumn`: one spinner shared by every
67/// row, whose animation starts at its first render.
68pub struct SpinnerColumn {
69    spinner: Spinner,
70    style: StyleType,
71    finished_text: String,
72}
73
74impl SpinnerColumn {
75    /// A spinner by name (e.g. `"dots"`), styled `progress.spinner`, showing
76    /// `finished_text` (console markup) once a task finishes.
77    pub fn new(name: &str, finished_text: impl Into<String>) -> Self {
78        SpinnerColumn {
79            spinner: Spinner::new(name),
80            style: StyleType::Name("progress.spinner".to_string()),
81            finished_text: finished_text.into(),
82        }
83    }
84
85    /// Animation speed multiplier (default 1.0).
86    pub fn speed(mut self, speed: f64) -> Self {
87        self.spinner = self.spinner.speed(speed);
88        self
89    }
90
91    /// Style of the spinner frame (default `progress.spinner`).
92    pub fn style(mut self, style: impl Into<StyleType>) -> Self {
93        self.style = style.into();
94        self
95    }
96}
97
98/// A text cell built from a format string. Port of `TextColumn`: the format
99/// is expanded against the task as `text_format.format(task=task)`, so it can
100/// use any task attribute (`{task.completed}`, `{task.percentage:>3.0f}`) and
101/// per-task fields (`{task.fields[name]}`).
102pub struct TextColumn {
103    text_format: String,
104    style: StyleType,
105    justify: Justify,
106    markup: bool,
107}
108
109impl TextColumn {
110    /// `TextColumn(text_format)` with upstream's defaults: no style, left
111    /// justified, console markup on.
112    pub fn new(text_format: impl Into<String>) -> Self {
113        TextColumn {
114            text_format: text_format.into(),
115            style: StyleType::default(),
116            justify: Justify::Left,
117            markup: true,
118        }
119    }
120
121    /// The style of the whole cell (upstream `style`).
122    pub fn style(mut self, style: impl Into<StyleType>) -> Self {
123        self.style = style.into();
124        self
125    }
126
127    /// Justify the text within the column (upstream `justify`).
128    pub fn justify(mut self, justify: Justify) -> Self {
129        self.justify = justify;
130        self
131    }
132
133    /// Parse the expanded text as console markup (upstream `markup`, default on).
134    pub fn markup(mut self, markup: bool) -> Self {
135        self.markup = markup;
136        self
137    }
138
139    fn render(&self, task: &Task) -> Text {
140        let expanded = pyformat::format(&self.text_format, |name| task.format_field(name));
141        let mut text = if self.markup {
142            Text::from_markup(&expanded).unwrap_or_else(|_| Text::new(expanded.clone()))
143        } else {
144            Text::new(expanded)
145        };
146        text.set_base_style(self.style.clone());
147        text.set_justify(self.justify);
148        text
149    }
150}
151
152/// A column in a [`Progress`] display. Mirrors upstream's `ProgressColumn`s.
153pub enum ProgressColumn {
154    /// The task description as console markup
155    /// (`TextColumn("[progress.description]{task.description}")`).
156    Description,
157    /// A static text cell with an explicit style (a simplified `TextColumn`).
158    Text(String, Style),
159    /// A text cell formatted from the task (`TextColumn`).
160    TextFormat(TextColumn),
161    /// The same renderable in every row (`RenderableColumn`). It may span
162    /// several lines; the row grows to fit it.
163    Renderable(Arc<dyn Renderable + Send + Sync>),
164    /// The flexing progress bar (`BarColumn`).
165    Bar,
166    /// The completion percentage `"{pct:>3}%"` (default `TaskProgressColumn`).
167    Percentage,
168    /// `TaskProgressColumn(show_speed=…)`: the percentage, or for a task with no
169    /// total and `show_speed`, the rate in `it/s`.
170    TaskProgress { show_speed: bool },
171    /// `"{completed}/{total}"` (`MofNCompleteColumn`, `progress.download`).
172    MofN,
173    /// `"{completed}/{total} {unit}"` in shared SI byte units, e.g. `0.5/1.0 kB`
174    /// (`DownloadColumn`, `progress.download`).
175    Download,
176    /// As [`Download`](Self::Download) in binary units (`DownloadColumn(binary_units=True)`).
177    BinaryDownload,
178    /// Elapsed time `H:MM:SS` (`TimeElapsedColumn`, `progress.elapsed`).
179    TimeElapsed,
180    /// Estimated time remaining (`TimeRemainingColumn`, `progress.remaining`).
181    TimeRemaining(TimeRemainingColumn),
182    /// Data speed, e.g. `1.2 MB/s` (`TransferSpeedColumn`, `progress.data.speed`).
183    TransferSpeed,
184    /// Completed size in decimal units (`FileSizeColumn`, `progress.filesize`).
185    FileSize,
186    /// Total size in decimal units (`TotalFileSizeColumn`, `progress.filesize.total`).
187    TotalFileSize,
188    /// An animated spinner (`SpinnerColumn`).
189    Spinner(SpinnerColumn),
190    /// A bar with its width and styles set (`BarColumn(bar_width=…, style=…)`).
191    /// A `bar_width` of `None` lets the bar fill its column.
192    BarWith(BarColumn),
193    /// A column with explicit table-column options (upstream's `table_column=`
194    /// argument). See [`ProgressColumn::with_table_column`].
195    WithTableColumn(Box<ProgressColumn>, ColumnOptions),
196}
197
198/// A progress bar column's width and styles. Port of `BarColumn`'s arguments.
199#[derive(Clone, Debug)]
200pub struct BarColumn {
201    bar_width: Option<usize>,
202    style: StyleType,
203    complete_style: StyleType,
204    finished_style: StyleType,
205    pulse_style: StyleType,
206}
207
208impl Default for BarColumn {
209    fn default() -> Self {
210        BarColumn {
211            bar_width: Some(40),
212            style: "bar.back".into(),
213            complete_style: "bar.complete".into(),
214            finished_style: "bar.finished".into(),
215            pulse_style: "bar.pulse".into(),
216        }
217    }
218}
219
220impl BarColumn {
221    /// Upstream's defaults: 40 cells wide, `bar.*` styles.
222    pub fn new() -> Self {
223        BarColumn::default()
224    }
225
226    /// The bar width, or `None` to fill the column (upstream `bar_width`).
227    pub fn bar_width(mut self, width: Option<usize>) -> Self {
228        self.bar_width = width;
229        self
230    }
231
232    /// The background style (upstream `style`).
233    pub fn style(mut self, style: impl Into<StyleType>) -> Self {
234        self.style = style.into();
235        self
236    }
237
238    /// The completed-part style (upstream `complete_style`).
239    pub fn complete_style(mut self, style: impl Into<StyleType>) -> Self {
240        self.complete_style = style.into();
241        self
242    }
243
244    /// The finished style (upstream `finished_style`).
245    pub fn finished_style(mut self, style: impl Into<StyleType>) -> Self {
246        self.finished_style = style.into();
247        self
248    }
249
250    /// The pulse style (upstream `pulse_style`).
251    pub fn pulse_style(mut self, style: impl Into<StyleType>) -> Self {
252        self.pulse_style = style.into();
253        self
254    }
255
256    /// Port of `BarColumn.render`.
257    fn render(&self, task: &Task) -> ProgressBar {
258        let bar = match task.total {
259            Some(total) => ProgressBar::new(total.max(0.0), task.completed.max(0.0)),
260            None => ProgressBar::indeterminate(),
261        };
262        let bar = match self.bar_width {
263            Some(width) => bar.width(width.max(1)),
264            None => bar,
265        };
266        bar.pulse(!task.started())
267            .animation_time(task.now())
268            .style(self.style.clone())
269            .complete_style(self.complete_style.clone())
270            .finished_style(self.finished_style.clone())
271            .pulse_style(self.pulse_style.clone())
272    }
273}
274
275impl ProgressColumn {
276    /// `TimeRemainingColumn()` with upstream's defaults.
277    pub fn time_remaining() -> Self {
278        ProgressColumn::TimeRemaining(TimeRemainingColumn::new(false, false))
279    }
280
281    /// `SpinnerColumn()` with upstream's defaults (`dots`, finished text `" "`).
282    pub fn spinner() -> Self {
283        ProgressColumn::Spinner(SpinnerColumn::new("dots", " "))
284    }
285
286    /// This column with explicit table-column options, as upstream's
287    /// `table_column=Column(...)` argument sets them: width, ratio, justify,
288    /// wrapping and style of the grid column.
289    pub fn with_table_column(self, options: ColumnOptions) -> Self {
290        let inner = match self {
291            ProgressColumn::WithTableColumn(inner, _) => *inner,
292            column => column,
293        };
294        ProgressColumn::WithTableColumn(Box::new(inner), options)
295    }
296
297    /// Port of `get_table_column()`: text columns default to
298    /// `Column(no_wrap=True)`, the rest to `Column()`.
299    fn table_column(&self) -> ColumnOptions {
300        match self {
301            ProgressColumn::WithTableColumn(_, options) => options.clone(),
302            ProgressColumn::Description
303            | ProgressColumn::Text(..)
304            | ProgressColumn::TextFormat(_)
305            | ProgressColumn::Percentage
306            | ProgressColumn::TaskProgress { .. } => ColumnOptions {
307                no_wrap: true,
308                ..ColumnOptions::default()
309            },
310            _ => ColumnOptions::default(),
311        }
312    }
313
314    /// The grid cell for `task`: the column's `__call__(task)`.
315    fn table_cell(&self, task: &Task) -> Cell {
316        match self {
317            ProgressColumn::WithTableColumn(inner, _) => inner.table_cell(task),
318            ProgressColumn::Bar => Cell::Renderable(Arc::new(BarColumn::default().render(task))),
319            ProgressColumn::BarWith(column) => Cell::Renderable(Arc::new(column.render(task))),
320            ProgressColumn::Renderable(renderable) => Cell::Renderable(renderable.clone()),
321            column => Cell::Text(column.cell(task)),
322        }
323    }
324
325    /// The cell for `task` (never called on [`ProgressColumn::Bar`]).
326    fn cell(&self, task: &Task) -> Text {
327        let named = |plain: String, style: &str| Text::styled(plain, style);
328        match self {
329            // `TextColumn`s hand their `justify` (default left) to the text, so
330            // it overrides the table column's.
331            ProgressColumn::Description => {
332                let markup = format!("[progress.description]{}", task.description);
333                Text::from_markup(&markup)
334                    .unwrap_or_else(|_| Text::new(task.description.clone()))
335                    .justify(Justify::Left)
336            }
337            ProgressColumn::Text(text, style) => {
338                Text::styled(text.clone(), style.clone()).justify(Justify::Left)
339            }
340            ProgressColumn::TextFormat(column) => column.render(task),
341            ProgressColumn::Bar
342            | ProgressColumn::BarWith(_)
343            | ProgressColumn::Renderable(_)
344            | ProgressColumn::WithTableColumn(..) => {
345                unreachable!("bar, renderable and wrapped columns have no text cell")
346            }
347            ProgressColumn::Percentage => task.percentage_cell().justify(Justify::Left),
348            ProgressColumn::TaskProgress { show_speed } => {
349                if task.total.is_none() && *show_speed {
350                    render_speed(
351                        task.finished_speed
352                            .filter(|s| *s != 0.0)
353                            .or_else(|| task.speed()),
354                    )
355                } else {
356                    task.percentage_cell().justify(Justify::Left)
357                }
358            }
359            ProgressColumn::MofN => named(task.mofn_text(), "progress.download"),
360            ProgressColumn::Download => named(task.download_text(false), "progress.download"),
361            ProgressColumn::BinaryDownload => named(task.download_text(true), "progress.download"),
362            ProgressColumn::TimeElapsed => {
363                let elapsed = if task.finished() {
364                    task.finished_time
365                } else {
366                    task.elapsed()
367                };
368                let text = match elapsed {
369                    None => "-:--:--".to_string(),
370                    Some(elapsed) => timedelta(elapsed.max(0.0) as i64),
371                };
372                named(text, "progress.elapsed")
373            }
374            ProgressColumn::TimeRemaining(column) => column.render(task),
375            ProgressColumn::TransferSpeed => {
376                let speed = task
377                    .finished_speed
378                    .filter(|s| *s != 0.0)
379                    .or_else(|| task.speed());
380                let text = match speed {
381                    None => "?".to_string(),
382                    Some(speed) => format!("{}/s", filesize::decimal_signed(speed as i64)),
383                };
384                named(text, "progress.data.speed")
385            }
386            // `filesize.decimal(int(task.completed))`: `int()` truncates
387            // toward zero and keeps the sign.
388            ProgressColumn::FileSize => named(
389                filesize::decimal_signed(task.completed as i64),
390                "progress.filesize",
391            ),
392            ProgressColumn::TotalFileSize => named(
393                task.total
394                    .map_or_else(String::new, |total| filesize::decimal_signed(total as i64)),
395                "progress.filesize.total",
396            ),
397            ProgressColumn::Spinner(column) => {
398                if task.finished() {
399                    Text::from_markup(&column.finished_text)
400                        .unwrap_or_else(|_| Text::new(column.finished_text.clone()))
401                } else {
402                    // Upstream's `self.spinner.render(task.get_time())`: the
403                    // spinner itself starts its animation at the first render.
404                    let mut frame = column.spinner.render(task.now());
405                    frame.set_base_style(column.style.clone());
406                    frame
407                }
408            }
409        }
410    }
411}
412
413impl TimeRemainingColumn {
414    fn render(&self, task: &Task) -> Text {
415        // `ProgressColumn.__call__`: reuse a render younger than max_refresh,
416        // but only while the task has completed nothing (`not task.completed`).
417        let now = task.now();
418        if task.completed == 0.0 {
419            if let Some((timestamp, text)) = self.cache.borrow().get(&task.id) {
420                if timestamp + 0.5 > now {
421                    return text.clone();
422                }
423            }
424        }
425        let (task_time, style) = if self.elapsed_when_finished && task.finished() {
426            (task.finished_time, "progress.elapsed")
427        } else {
428            (task.time_remaining(), "progress.remaining")
429        };
430        let text = if task.total.is_none() {
431            Text::styled("", style)
432        } else {
433            match task_time {
434                None => Text::styled(if self.compact { "--:--" } else { "-:--:--" }, style),
435                Some(task_time) => {
436                    let whole = task_time as i64;
437                    let (minutes, seconds) = (whole.div_euclid(60), whole.rem_euclid(60));
438                    let (hours, minutes) = (minutes.div_euclid(60), minutes.rem_euclid(60));
439                    let formatted = if self.compact && hours == 0 {
440                        format!("{minutes:02}:{seconds:02}")
441                    } else {
442                        format!("{hours}:{minutes:02}:{seconds:02}")
443                    };
444                    Text::styled(formatted, style)
445                }
446            }
447        };
448        self.cache.borrow_mut().insert(task.id, (now, text.clone()));
449        text
450    }
451}
452
453/// `TaskProgressColumn.render_speed`: iterations per second with a power-of-ten
454/// suffix, e.g. `2.5×10³ it/s`.
455fn render_speed(speed: Option<f64>) -> Text {
456    let Some(speed) = speed else {
457        return Text::styled("", "progress.percentage");
458    };
459    let (unit, suffix) = filesize::pick_unit_and_suffix_signed(
460        speed as i64,
461        &["", "×10³", "×10⁶", "×10⁹", "×10¹²"],
462        1000,
463    );
464    let data_speed = speed / unit as f64;
465    Text::styled(
466        format!("{data_speed:.1}{suffix} it/s"),
467        "progress.percentage",
468    )
469}
470
471/// Python's `str(timedelta(seconds=n))` for `n >= 0`: `H:MM:SS`, prefixed by
472/// `N day(s), ` from a day upward.
473fn timedelta(total_seconds: i64) -> String {
474    let days = total_seconds / 86_400;
475    let rest = total_seconds % 86_400;
476    let clock = format!("{}:{:02}:{:02}", rest / 3600, rest % 3600 / 60, rest % 60);
477    match days {
478        0 => clock,
479        1 => format!("1 day, {clock}"),
480        days => format!("{days} days, {clock}"),
481    }
482}
483
484/// Python's `f"{value:,.{precision}f}"`: fixed precision with `,` grouping.
485fn grouped(value: f64, precision: usize) -> String {
486    let formatted = format!("{value:.precision$}");
487    let (sign, digits) = match formatted.strip_prefix('-') {
488        Some(rest) => ("-", rest),
489        None => ("", formatted.as_str()),
490    };
491    let (integer, fraction) = match digits.split_once('.') {
492        Some((integer, fraction)) => (integer, Some(fraction)),
493        None => (digits, None),
494    };
495    let mut grouped = String::new();
496    for (index, digit) in integer.chars().enumerate() {
497        if index > 0 && (integer.len() - index) % 3 == 0 {
498            grouped.push(',');
499        }
500        grouped.push(digit);
501    }
502    match fraction {
503        Some(fraction) => format!("{sign}{grouped}.{fraction}"),
504        None => format!("{sign}{grouped}"),
505    }
506}
507
508/// A single tracked task. Mirrors `rich.progress.Task`; read-only outside
509/// [`Progress`].
510pub struct Task {
511    id: TaskId,
512    description: String,
513    total: Option<f64>,
514    completed: f64,
515    visible: bool,
516    start_time: Option<f64>,
517    stop_time: Option<f64>,
518    finished_time: Option<f64>,
519    finished_speed: Option<f64>,
520    /// `(timestamp, completed)` speed samples (upstream `ProgressSample`).
521    samples: VecDeque<(f64, f64)>,
522    /// Arbitrary per-task values for format strings (upstream `fields`).
523    fields: BTreeMap<String, FormatValue>,
524    get_time: GetTime,
525}
526
527impl Task {
528    /// This task's custom fields (upstream `Task.fields`).
529    pub fn fields(&self) -> &BTreeMap<String, FormatValue> {
530        &self.fields
531    }
532
533    /// Resolve a `str.format` field name against this task, as
534    /// `text_format.format(task=task)` does: `task.<attribute>` or
535    /// `task.fields[<name>]`.
536    fn format_field(&self, name: &str) -> Option<FormatValue> {
537        let attribute = name.strip_prefix("task.")?;
538        if let Some(key) = attribute
539            .strip_prefix("fields[")
540            .and_then(|rest| rest.strip_suffix(']'))
541        {
542            return self.fields.get(key).cloned();
543        }
544        Some(match attribute {
545            "id" => FormatValue::Int(self.id.0 as i64),
546            "description" => FormatValue::Str(self.description.clone()),
547            // Python keeps the ints a caller passes (`total=200` prints `200`);
548            // this API takes floats, so a whole number formats as an int.
549            "total" => self.total.map_or(FormatValue::None, whole_number),
550            "completed" => whole_number(self.completed),
551            "visible" => FormatValue::Bool(self.visible),
552            "started" => FormatValue::Bool(self.started()),
553            "finished" => FormatValue::Bool(self.finished()),
554            "percentage" => FormatValue::Float(self.percentage()),
555            "remaining" => self.remaining().into(),
556            "elapsed" => self.elapsed().into(),
557            "speed" => self.speed().into(),
558            "time_remaining" => self.time_remaining().into(),
559            "start_time" => self.start_time.into(),
560            "stop_time" => self.stop_time.into(),
561            "finished_time" => self.finished_time.into(),
562            "finished_speed" => self.finished_speed.into(),
563            _ => return None,
564        })
565    }
566    fn now(&self) -> f64 {
567        (self.get_time)()
568    }
569
570    /// This task's id.
571    pub fn id(&self) -> TaskId {
572        self.id
573    }
574
575    /// The description (console markup).
576    pub fn description(&self) -> &str {
577        &self.description
578    }
579
580    /// The total number of steps, or `None` when indeterminate.
581    pub fn total(&self) -> Option<f64> {
582        self.total
583    }
584
585    /// The number of steps completed.
586    pub fn completed(&self) -> f64 {
587        self.completed
588    }
589
590    /// Whether the task is shown.
591    pub fn visible(&self) -> bool {
592        self.visible
593    }
594
595    /// Whether the task has been started.
596    pub fn started(&self) -> bool {
597        self.start_time.is_some()
598    }
599
600    /// Steps left, or `None` when indeterminate.
601    pub fn remaining(&self) -> Option<f64> {
602        self.total.map(|total| total - self.completed)
603    }
604
605    /// Seconds since the task started (to its stop time, if stopped).
606    pub fn elapsed(&self) -> Option<f64> {
607        let start = self.start_time?;
608        Some(self.stop_time.unwrap_or_else(|| self.now()) - start)
609    }
610
611    /// Whether the task has reached its total.
612    pub fn finished(&self) -> bool {
613        self.finished_time.is_some()
614    }
615
616    /// The elapsed time recorded when the task finished.
617    pub fn finished_time(&self) -> Option<f64> {
618        self.finished_time
619    }
620
621    /// The completion percentage, clamped to 0–100 (0 without a total).
622    pub fn percentage(&self) -> f64 {
623        match self.total {
624            Some(total) if total != 0.0 => (self.completed / total * 100.0).clamp(0.0, 100.0),
625            _ => 0.0,
626        }
627    }
628
629    /// Steps per second over the sample window, or `None` without enough samples.
630    pub fn speed(&self) -> Option<f64> {
631        self.start_time?;
632        let (first, _) = *self.samples.front()?;
633        let (last, _) = *self.samples.back()?;
634        let total_time = last - first;
635        if total_time == 0.0 {
636            return None;
637        }
638        let total_completed: f64 = self.samples.iter().skip(1).map(|(_, done)| done).sum();
639        Some(total_completed / total_time)
640    }
641
642    /// Estimated seconds remaining (rounded up), 0 once finished.
643    pub fn time_remaining(&self) -> Option<f64> {
644        if self.finished() {
645            return Some(0.0);
646        }
647        let speed = self.speed().filter(|speed| *speed != 0.0)?;
648        let remaining = self.remaining()?;
649        Some((remaining / speed).ceil())
650    }
651
652    /// `Task._reset`.
653    fn clear_progress(&mut self) {
654        self.samples.clear();
655        self.finished_time = None;
656        self.finished_speed = None;
657    }
658
659    /// The percentage cell: `[progress.percentage]{percentage:>3.0f}%`, empty
660    /// without a total (`text_format_no_percentage`).
661    fn percentage_cell(&self) -> Text {
662        if self.total.is_none() {
663            return Text::new("");
664        }
665        let mut text = Text::new(format!("{:>3.0}%", self.percentage()));
666        let len = text.plain().len();
667        text.stylize("progress.percentage", 0, len);
668        text
669    }
670
671    /// The M-of-N cell text: `completed` right-justified to the width of `total`
672    /// (`?` when indeterminate), then `/total`. Port of `MofNCompleteColumn.render`.
673    fn mofn_text(&self) -> String {
674        let completed = self.completed as i64;
675        let total = self
676            .total
677            .map_or_else(|| "?".to_string(), |total| (total as i64).to_string());
678        let total_width = total.chars().count();
679        format!("{completed:>total_width$}/{total}")
680    }
681
682    /// The download cell text: `completed`/`total` in a shared byte unit, e.g.
683    /// `0.5/1.0 kB`. Port of `DownloadColumn.render`.
684    fn download_text(&self, binary: bool) -> String {
685        const DECIMAL: &[&str] = &["bytes", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
686        const BINARY: &[&str] = &[
687            "bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB",
688        ];
689        // `int(task.completed)` / `int(task.total)`: truncated, sign kept.
690        let completed = self.completed as i64;
691        let base_size = self.total.map_or(completed, |total| total as i64);
692        let (unit, suffix) = if binary {
693            filesize::pick_unit_and_suffix_signed(base_size, BINARY, 1024)
694        } else {
695            filesize::pick_unit_and_suffix_signed(base_size, DECIMAL, 1000)
696        };
697        let precision = if unit == 1 { 0 } else { 1 };
698        let completed_str = grouped(completed as f64 / unit as f64, precision);
699        let total_str = self.total.map_or_else(
700            || "?".to_string(),
701            |total| grouped((total as i64) as f64 / unit as f64, precision),
702        );
703        format!("{completed_str}/{total_str} {suffix}")
704    }
705}
706
707/// Changes for [`Progress::update`]; unset fields are left alone. Upstream's
708/// keyword arguments to `Progress.update`.
709#[derive(Debug, Clone, Default)]
710pub struct TaskUpdate {
711    pub total: Option<f64>,
712    pub completed: Option<f64>,
713    pub advance: Option<f64>,
714    pub description: Option<String>,
715    pub visible: Option<bool>,
716    /// Custom fields to set (upstream `**fields`).
717    pub fields: Vec<(String, FormatValue)>,
718    /// Redraw a live display right after the update (upstream `refresh=True`).
719    pub refresh: bool,
720}
721
722impl TaskUpdate {
723    pub fn total(mut self, total: f64) -> Self {
724        self.total = Some(total);
725        self
726    }
727
728    pub fn completed(mut self, completed: f64) -> Self {
729        self.completed = Some(completed);
730        self
731    }
732
733    pub fn advance(mut self, advance: f64) -> Self {
734        self.advance = Some(advance);
735        self
736    }
737
738    pub fn description(mut self, description: impl Into<String>) -> Self {
739        self.description = Some(description.into());
740        self
741    }
742
743    pub fn visible(mut self, visible: bool) -> Self {
744        self.visible = Some(visible);
745        self
746    }
747
748    /// Redraw a [`LiveProgress`] right after this update (upstream
749    /// `refresh=True`).
750    pub fn refresh(mut self, refresh: bool) -> Self {
751        self.refresh = refresh;
752        self
753    }
754
755    /// Set a custom field (upstream `update(task_id, **fields)`).
756    pub fn field(mut self, name: impl Into<String>, value: impl Into<FormatValue>) -> Self {
757        self.fields.push((name.into(), value.into()));
758        self
759    }
760}
761
762/// A progress display over one or more [`Task`]s. Mirrors `rich.progress.Progress`.
763pub struct Progress {
764    tasks: Vec<Task>,
765    next_id: usize,
766    columns: Vec<ProgressColumn>,
767    get_time: GetTime,
768    speed_estimate_period: f64,
769    expand: bool,
770    transient: bool,
771    disable: bool,
772}
773
774impl Default for Progress {
775    fn default() -> Self {
776        Progress {
777            tasks: Vec::new(),
778            next_id: 0,
779            columns: Progress::default_columns(),
780            get_time: Arc::new(monotonic),
781            speed_estimate_period: 30.0,
782            expand: false,
783            transient: false,
784            disable: false,
785        }
786    }
787}
788
789impl Progress {
790    pub fn new() -> Self {
791        Progress::default()
792    }
793
794    /// Upstream's `Progress.get_default_columns()`: description, bar,
795    /// percentage and time remaining.
796    pub fn default_columns() -> Vec<ProgressColumn> {
797        vec![
798            ProgressColumn::Description,
799            ProgressColumn::Bar,
800            ProgressColumn::Percentage,
801            ProgressColumn::time_remaining(),
802        ]
803    }
804
805    /// Replace the column list (default: [`default_columns`](Self::default_columns)).
806    pub fn columns(mut self, columns: Vec<ProgressColumn>) -> Self {
807        self.columns = columns;
808        self
809    }
810
811    /// Read time from `clock` (seconds) instead of the monotonic clock.
812    /// Upstream's `get_time`; makes time-based columns deterministic.
813    pub fn clock(mut self, clock: impl Fn() -> f64 + Send + Sync + 'static) -> Self {
814        self.get_time = Arc::new(clock);
815        for task in &mut self.tasks {
816            task.get_time = self.get_time.clone();
817        }
818        self
819    }
820
821    /// Stretch the task grid to the full width (upstream `expand`).
822    pub fn expand(mut self, expand: bool) -> Self {
823        self.expand = expand;
824        self
825    }
826
827    /// Erase the display when it stops (upstream `transient`).
828    pub fn transient(mut self, transient: bool) -> Self {
829        self.transient = transient;
830        self
831    }
832
833    /// Show nothing: [`start`](Self::start) draws no display, while tasks
834    /// still update (upstream `disable`).
835    pub fn disable(mut self, disable: bool) -> Self {
836        self.disable = disable;
837        self
838    }
839
840    /// Seconds of history used for speed estimates (default 30).
841    pub fn speed_estimate_period(mut self, seconds: f64) -> Self {
842        self.speed_estimate_period = seconds;
843        self
844    }
845
846    fn now(&self) -> f64 {
847        (self.get_time)()
848    }
849
850    fn task_mut(&mut self, id: TaskId) -> Option<&mut Task> {
851        self.tasks.iter_mut().find(|task| task.id == id)
852    }
853
854    /// Add a started task and return its id. Port of `Progress.add_task`
855    /// (`start=True`); `total` of `None` is an indeterminate task.
856    pub fn add_task(
857        &mut self,
858        description: impl Into<String>,
859        total: impl Into<Option<f64>>,
860        completed: f64,
861    ) -> TaskId {
862        let id = self.push_task(description.into(), total.into(), completed);
863        self.start_task(id);
864        id
865    }
866
867    /// Add a task that has not started (`add_task(start=False)`): it shows no
868    /// elapsed time until [`start_task`](Self::start_task).
869    pub fn add_unstarted_task(
870        &mut self,
871        description: impl Into<String>,
872        total: impl Into<Option<f64>>,
873        completed: f64,
874    ) -> TaskId {
875        self.push_task(description.into(), total.into(), completed)
876    }
877
878    /// Add a task with custom fields for format strings. Port of
879    /// `add_task(description, total=…, completed=…, start=…, **fields)`.
880    pub fn add_task_with<K: Into<String>, V: Into<FormatValue>>(
881        &mut self,
882        description: impl Into<String>,
883        total: impl Into<Option<f64>>,
884        completed: f64,
885        start: bool,
886        fields: impl IntoIterator<Item = (K, V)>,
887    ) -> TaskId {
888        let id = self.push_task(description.into(), total.into(), completed);
889        if let Some(task) = self.task_mut(id) {
890            task.fields = fields
891                .into_iter()
892                .map(|(name, value)| (name.into(), value.into()))
893                .collect();
894        }
895        if start {
896            self.start_task(id);
897        }
898        id
899    }
900
901    fn push_task(&mut self, description: String, total: Option<f64>, completed: f64) -> TaskId {
902        let id = TaskId(self.next_id);
903        self.next_id += 1;
904        self.tasks.push(Task {
905            id,
906            description,
907            total,
908            completed,
909            visible: true,
910            start_time: None,
911            stop_time: None,
912            finished_time: None,
913            finished_speed: None,
914            samples: VecDeque::new(),
915            fields: BTreeMap::new(),
916            get_time: self.get_time.clone(),
917        });
918        id
919    }
920
921    /// The task with this id, if it has not been removed.
922    pub fn task(&self, id: TaskId) -> Option<&Task> {
923        self.tasks.iter().find(|task| task.id == id)
924    }
925
926    /// Every task, in the order added.
927    pub fn tasks(&self) -> &[Task] {
928        &self.tasks
929    }
930
931    /// Whether every task has finished. Port of `Progress.finished`.
932    pub fn finished(&self) -> bool {
933        self.tasks.iter().all(Task::finished)
934    }
935
936    /// Start a task's clock if it has not started. Port of `start_task`.
937    pub fn start_task(&mut self, id: TaskId) {
938        let now = self.now();
939        if let Some(task) = self.task_mut(id) {
940            task.start_time.get_or_insert(now);
941        }
942    }
943
944    /// Stop a task's clock; its elapsed time freezes. Port of `stop_task`.
945    pub fn stop_task(&mut self, id: TaskId) {
946        let now = self.now();
947        if let Some(task) = self.task_mut(id) {
948            task.start_time.get_or_insert(now);
949            task.stop_time = Some(now);
950        }
951    }
952
953    /// Update a task. Port of `Progress.update`: a new total clears the speed
954    /// samples; positive progress adds a sample; reaching the total records the
955    /// finish time.
956    pub fn update(&mut self, id: TaskId, update: TaskUpdate) {
957        let now = self.now();
958        let period = self.speed_estimate_period;
959        let Some(task) = self.task_mut(id) else {
960            return;
961        };
962        let completed_start = task.completed;
963        if let Some(total) = update.total {
964            if Some(total) != task.total {
965                task.total = Some(total);
966                task.clear_progress();
967            }
968        }
969        if let Some(advance) = update.advance {
970            task.completed += advance;
971        }
972        if let Some(completed) = update.completed {
973            task.completed = completed;
974        }
975        if let Some(description) = update.description {
976            task.description = description;
977        }
978        if let Some(visible) = update.visible {
979            task.visible = visible;
980        }
981        task.fields.extend(update.fields);
982        let update_completed = task.completed - completed_start;
983        let old_sample_time = now - period;
984        while task
985            .samples
986            .front()
987            .is_some_and(|(time, _)| *time < old_sample_time)
988        {
989            task.samples.pop_front();
990        }
991        if update_completed > 0.0 {
992            task.samples.push_back((now, update_completed));
993            if task.samples.len() > MAX_SAMPLES {
994                task.samples.pop_front();
995            }
996        }
997        if task.total.is_some_and(|total| task.completed >= total) && task.finished_time.is_none() {
998            task.finished_time = task.elapsed();
999        }
1000    }
1001
1002    /// Advance a task by `amount` steps. Port of `Progress.advance`, which
1003    /// (unlike `update`) always records a sample and the finish speed.
1004    pub fn advance(&mut self, id: TaskId, amount: f64) {
1005        let now = self.now();
1006        let period = self.speed_estimate_period;
1007        let Some(task) = self.task_mut(id) else {
1008            return;
1009        };
1010        let completed_start = task.completed;
1011        task.completed += amount;
1012        let update_completed = task.completed - completed_start;
1013        let old_sample_time = now - period;
1014        while task
1015            .samples
1016            .front()
1017            .is_some_and(|(time, _)| *time < old_sample_time)
1018        {
1019            task.samples.pop_front();
1020        }
1021        while task.samples.len() > MAX_SAMPLES {
1022            task.samples.pop_front();
1023        }
1024        task.samples.push_back((now, update_completed));
1025        if task.samples.len() > MAX_SAMPLES {
1026            task.samples.pop_front();
1027        }
1028        if task.total.is_some_and(|total| task.completed >= total) && task.finished_time.is_none() {
1029            task.finished_time = task.elapsed();
1030            task.finished_speed = task.speed();
1031        }
1032    }
1033
1034    /// Reset a task to `completed`, optionally restarting its clock and
1035    /// changing its total. Port of `Progress.reset`. Like upstream, a stop
1036    /// time set earlier is kept.
1037    pub fn reset(&mut self, id: TaskId, start: bool, total: Option<f64>, completed: f64) {
1038        let now = self.now();
1039        let Some(task) = self.task_mut(id) else {
1040            return;
1041        };
1042        task.clear_progress();
1043        task.start_time = start.then_some(now);
1044        if let Some(total) = total {
1045            task.total = Some(total);
1046        }
1047        task.completed = completed;
1048        task.finished_time = None;
1049    }
1050
1051    /// Remove a task. Port of `Progress.remove_task`.
1052    pub fn remove_task(&mut self, id: TaskId) {
1053        self.tasks.retain(|task| task.id != id);
1054    }
1055}
1056
1057impl Progress {
1058    /// The grid the display renders. Port of `Progress.make_tasks_table`: a
1059    /// `Table.grid` with one column per [`ProgressColumn`] (its table column
1060    /// options), `padding=(0, 1)` and the progress's `expand`, and one row per
1061    /// visible task.
1062    pub fn make_tasks_table(&self) -> Table {
1063        let mut table = Table::grid().padding(0, 1, 0, 1).expand(self.expand);
1064        for column in &self.columns {
1065            table.add_column_with(Text::new(""), column.table_column());
1066        }
1067        for task in self.tasks.iter().filter(|task| task.visible) {
1068            // Each column is called once per row, in order: spinners and the
1069            // remaining-time cache are stateful, as upstream's columns are.
1070            let cells = self
1071                .columns
1072                .iter()
1073                .map(|column| column.table_cell(task))
1074                .collect();
1075            table.add_row_cells(cells);
1076        }
1077        table
1078    }
1079}
1080
1081impl Renderable for Progress {
1082    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
1083        self.make_tasks_table().rich_render(console, options)
1084    }
1085}
1086
1087/// A [`Progress`] shared with the auto-refresh thread of a live display.
1088struct ProgressView(Arc<std::sync::Mutex<Progress>>);
1089
1090impl Renderable for ProgressView {
1091    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
1092        match self.0.lock() {
1093            Ok(progress) => progress.rich_render(console, options),
1094            Err(poisoned) => poisoned.into_inner().rich_render(console, options),
1095        }
1096    }
1097}
1098
1099impl Progress {
1100    /// Start an auto-refreshing live display of this progress, redrawn
1101    /// `refresh_per_second` times a second on a background thread. Port of
1102    /// `Progress.start` with `auto_refresh=True`; stop it with
1103    /// [`LiveProgress::stop`] (upstream's `with progress:` block).
1104    pub fn start<W: std::io::Write + Send + 'static>(
1105        self,
1106        console: Console,
1107        writer: W,
1108        refresh_per_second: f64,
1109    ) -> LiveProgress<W> {
1110        // `disable` draws nothing: upstream skips `live.start()` and `stop()`.
1111        if self.disable {
1112            return LiveProgress {
1113                progress: Arc::new(std::sync::Mutex::new(self)),
1114                live: None,
1115                writer: Some(writer),
1116                interactive: true,
1117                holder: std::sync::Mutex::new(None),
1118            };
1119        }
1120        let transient = self.transient;
1121        let interactive = console.is_terminal();
1122        let shared = Arc::new(std::sync::Mutex::new(self));
1123        let live = crate::live::Live::spawn_with(
1124            Box::new(ProgressView(shared.clone())),
1125            console,
1126            writer,
1127            refresh_per_second,
1128            transient,
1129        );
1130        LiveProgress {
1131            progress: shared,
1132            live: Some(live),
1133            writer: None,
1134            interactive,
1135            holder: std::sync::Mutex::new(None),
1136        }
1137    }
1138}
1139
1140/// A running [`Progress`] display. Task changes made through it are picked up
1141/// by the next refresh, as with upstream's `refresh=False` updates.
1142pub struct LiveProgress<W: std::io::Write + Send + 'static> {
1143    progress: Arc<std::sync::Mutex<Progress>>,
1144    live: Option<crate::live::AutoLive<W>>,
1145    /// The sink of a disabled display, which never reaches a live thread.
1146    writer: Option<W>,
1147    /// Whether the console is a terminal; `stop` ends a file with a newline.
1148    interactive: bool,
1149    /// The thread inside [`with`](LiveProgress::with), if any. Upstream's lock
1150    /// is an `RLock`; ours is not, so re-entry is detected rather than left to
1151    /// deadlock on the mutex or on the refresh thread.
1152    holder: std::sync::Mutex<Option<std::thread::ThreadId>>,
1153}
1154
1155/// Clears [`LiveProgress`]'s `holder` when a `with` block ends, unwinding
1156/// included.
1157struct HolderGuard<'a>(&'a std::sync::Mutex<Option<std::thread::ThreadId>>);
1158
1159impl Drop for HolderGuard<'_> {
1160    fn drop(&mut self) {
1161        *self
1162            .0
1163            .lock()
1164            .unwrap_or_else(|poisoned| poisoned.into_inner()) = None;
1165    }
1166}
1167
1168impl<W: std::io::Write + Send + 'static> LiveProgress<W> {
1169    /// Run `f` with the progress locked, for any change not wrapped below.
1170    ///
1171    /// `f` may call [`refresh`](Self::refresh) (the frame is drawn as soon as
1172    /// the lock is released), but not `with` or a method built on it: upstream
1173    /// re-enters its `RLock`, which a `&mut Progress` cannot express, so a
1174    /// nested call panics instead of deadlocking.
1175    pub fn with<R>(&self, f: impl FnOnce(&mut Progress) -> R) -> R {
1176        let current = std::thread::current().id();
1177        assert!(
1178            !self.held_by(current),
1179            "LiveProgress::with re-entered from inside a `with` closure; \
1180             use the `&mut Progress` it was given instead"
1181        );
1182        let mut progress = match self.progress.lock() {
1183            Ok(progress) => progress,
1184            Err(poisoned) => poisoned.into_inner(),
1185        };
1186        *self
1187            .holder
1188            .lock()
1189            .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(current);
1190        let _holder = HolderGuard(&self.holder);
1191        f(&mut progress)
1192    }
1193
1194    /// Whether `thread` is inside [`with`](Self::with) right now.
1195    fn held_by(&self, thread: std::thread::ThreadId) -> bool {
1196        *self
1197            .holder
1198            .lock()
1199            .unwrap_or_else(|poisoned| poisoned.into_inner())
1200            == Some(thread)
1201    }
1202
1203    /// [`Progress::add_task`], then redraw (upstream's `add_task` refreshes).
1204    pub fn add_task(
1205        &self,
1206        description: impl Into<String>,
1207        total: impl Into<Option<f64>>,
1208        completed: f64,
1209    ) -> TaskId {
1210        let id = self.with(|progress| progress.add_task(description, total, completed));
1211        self.refresh();
1212        id
1213    }
1214
1215    /// [`Progress::reset`], then redraw (upstream's `reset` refreshes).
1216    pub fn reset(&self, id: TaskId, start: bool, total: Option<f64>, completed: f64) {
1217        self.with(|progress| progress.reset(id, start, total, completed));
1218        self.refresh();
1219    }
1220
1221    /// [`Progress::advance`].
1222    pub fn advance(&self, id: TaskId, amount: f64) {
1223        self.with(|progress| progress.advance(id, amount));
1224    }
1225
1226    /// [`Progress::update`]; redraws when the update asks to
1227    /// ([`TaskUpdate::refresh`]).
1228    pub fn update(&self, id: TaskId, update: TaskUpdate) {
1229        let refresh = update.refresh;
1230        self.with(|progress| progress.update(id, update));
1231        if refresh {
1232            self.refresh();
1233        }
1234    }
1235
1236    /// Redraw now rather than at the next tick, returning once the frame is
1237    /// written. Port of `Progress.refresh`.
1238    ///
1239    /// Called from inside [`with`](Self::with), the redraw is queued instead:
1240    /// the refresh thread needs the lock this thread holds to render, so
1241    /// waiting for it would deadlock. The frame is drawn when `with` returns.
1242    pub fn refresh(&self) {
1243        if let Some(live) = &self.live {
1244            if self.held_by(std::thread::current().id()) {
1245                live.refresh();
1246            } else {
1247                live.refresh_wait();
1248            }
1249        }
1250    }
1251
1252    /// Iterate `iter`, advancing a new task by one after each item is
1253    /// processed. Port of `Progress.track`: the total defaults to the
1254    /// iterator's exact length, else the task is indeterminate.
1255    pub fn track<I: IntoIterator>(
1256        &self,
1257        iter: I,
1258        total: Option<f64>,
1259        description: impl Into<String>,
1260    ) -> Track<'_, I::IntoIter, W> {
1261        let iter = iter.into_iter();
1262        let total = total.or_else(|| match iter.size_hint() {
1263            (lower, Some(upper)) if lower == upper && lower > 0 => Some(lower as f64),
1264            _ => None,
1265        });
1266        let task = self.add_task(description, total, 0.0);
1267        Track {
1268            iter,
1269            progress: self,
1270            task,
1271            pending: false,
1272        }
1273    }
1274
1275    /// Track reading from `reader`: each read advances the task by the bytes
1276    /// read. Port of `Progress.wrap_file`: `total` is the byte count, or else
1277    /// the total of `task`; a new task named `description` is added when
1278    /// `task` is `None`, otherwise `task`'s total is set.
1279    pub fn wrap_read<R: std::io::Read>(
1280        &self,
1281        reader: R,
1282        total: Option<u64>,
1283        task: Option<TaskId>,
1284        description: impl Into<String>,
1285    ) -> std::io::Result<ProgressReader<'_, R, W>> {
1286        let total = total.map(|total| total as f64).or_else(|| {
1287            task.and_then(|task| self.with(|progress| progress.task(task).and_then(Task::total)))
1288        });
1289        let Some(total) = total else {
1290            return Err(std::io::Error::new(
1291                std::io::ErrorKind::InvalidInput,
1292                "unable to get the total number of bytes, please specify 'total'",
1293            ));
1294        };
1295        let task = self.task_for(task, total, description);
1296        Ok(ProgressReader {
1297            reader,
1298            progress: self,
1299            task,
1300        })
1301    }
1302
1303    /// Open `path` for reading and track it. Port of `Progress.open` in
1304    /// binary mode: `total` defaults to the file's size.
1305    pub fn open(
1306        &self,
1307        path: impl AsRef<std::path::Path>,
1308        total: Option<u64>,
1309        task: Option<TaskId>,
1310        description: impl Into<String>,
1311    ) -> std::io::Result<ProgressReader<'_, std::fs::File, W>> {
1312        let file = std::fs::File::open(path)?;
1313        let total = match total {
1314            Some(total) => total,
1315            None => file.metadata()?.len(),
1316        };
1317        let task = self.task_for(task, total as f64, description);
1318        Ok(ProgressReader {
1319            reader: file,
1320            progress: self,
1321            task,
1322        })
1323    }
1324
1325    /// A new task with `total`, or `task` with its total set to it.
1326    fn task_for(&self, task: Option<TaskId>, total: f64, description: impl Into<String>) -> TaskId {
1327        match task {
1328            Some(task) => {
1329                self.update(task, TaskUpdate::default().total(total));
1330                task
1331            }
1332            None => self.add_task(description, total, 0.0),
1333        }
1334    }
1335
1336    /// Commit the final frame, stop the refresh thread, and return the
1337    /// progress and the output sink. Port of `Progress.stop`.
1338    pub fn stop(mut self) -> (Progress, W) {
1339        let writer = match self.live.take() {
1340            Some(live) => {
1341                let mut writer = live.stop();
1342                // `Progress.stop`: `console.print()` when not interactive.
1343                if !self.interactive {
1344                    let _ = writer.write_all(b"\n");
1345                }
1346                writer
1347            }
1348            None => self
1349                .writer
1350                .take()
1351                .expect("a disabled display keeps its writer"),
1352        };
1353        let progress = match Arc::try_unwrap(std::mem::replace(
1354            &mut self.progress,
1355            Arc::new(std::sync::Mutex::new(Progress::new())),
1356        )) {
1357            Ok(mutex) => mutex
1358                .into_inner()
1359                .unwrap_or_else(|poisoned| poisoned.into_inner()),
1360            // The refresh thread has exited, so this is the only owner left.
1361            Err(_) => unreachable!("progress still shared after the live display stopped"),
1362        };
1363        (progress, writer)
1364    }
1365}
1366
1367/// A reader that advances a task by the bytes read through it. Returned by
1368/// [`LiveProgress::wrap_read`] and [`LiveProgress::open`] (upstream `_Reader`).
1369pub struct ProgressReader<'a, R, W: std::io::Write + Send + 'static> {
1370    reader: R,
1371    progress: &'a LiveProgress<W>,
1372    task: TaskId,
1373}
1374
1375impl<R, W: std::io::Write + Send + 'static> ProgressReader<'_, R, W> {
1376    /// The task this reader advances.
1377    pub fn task(&self) -> TaskId {
1378        self.task
1379    }
1380
1381    /// The wrapped reader.
1382    pub fn into_inner(self) -> R {
1383        self.reader
1384    }
1385}
1386
1387impl<R: std::io::Read, W: std::io::Write + Send + 'static> std::io::Read
1388    for ProgressReader<'_, R, W>
1389{
1390    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1391        let count = self.reader.read(buf)?;
1392        self.progress.advance(self.task, count as f64);
1393        Ok(count)
1394    }
1395}
1396
1397impl<R: std::io::BufRead, W: std::io::Write + Send + 'static> std::io::BufRead
1398    for ProgressReader<'_, R, W>
1399{
1400    fn fill_buf(&mut self) -> std::io::Result<&[u8]> {
1401        self.reader.fill_buf()
1402    }
1403
1404    fn consume(&mut self, amount: usize) {
1405        self.reader.consume(amount);
1406        self.progress.advance(self.task, amount as f64);
1407    }
1408}
1409
1410/// The iterator [`LiveProgress::track`] returns.
1411pub struct Track<'a, I: Iterator, W: std::io::Write + Send + 'static> {
1412    iter: I,
1413    progress: &'a LiveProgress<W>,
1414    task: TaskId,
1415    /// Whether an item has been handed out but not yet counted: upstream
1416    /// advances after the loop body, when the next item is requested.
1417    pending: bool,
1418}
1419
1420impl<I: Iterator, W: std::io::Write + Send + 'static> Track<'_, I, W> {
1421    /// The task this iterator advances.
1422    pub fn task(&self) -> TaskId {
1423        self.task
1424    }
1425}
1426
1427impl<I: Iterator, W: std::io::Write + Send + 'static> Iterator for Track<'_, I, W> {
1428    type Item = I::Item;
1429
1430    fn next(&mut self) -> Option<I::Item> {
1431        if std::mem::take(&mut self.pending) {
1432            self.progress.advance(self.task, 1.0);
1433        }
1434        let item = self.iter.next();
1435        if item.is_some() {
1436            self.pending = true;
1437        } else {
1438            self.progress.refresh();
1439        }
1440        item
1441    }
1442
1443    fn size_hint(&self) -> (usize, Option<usize>) {
1444        self.iter.size_hint()
1445    }
1446}
1447
1448/// Track progress over `iter` with a live display on stdout. Port of the
1449/// module-level `rich.progress.track`: the description, bar, progress and
1450/// time-remaining columns, refreshed ten times a second, stopped when the
1451/// iterator is exhausted or dropped.
1452pub fn track<I: IntoIterator>(iter: I, description: &str) -> TrackStdout<I::IntoIter> {
1453    let mut columns = Vec::new();
1454    if !description.is_empty() {
1455        columns.push(ProgressColumn::Description);
1456    }
1457    columns.extend([
1458        ProgressColumn::Bar,
1459        ProgressColumn::TaskProgress { show_speed: true },
1460        ProgressColumn::TimeRemaining(TimeRemainingColumn::new(false, true)),
1461    ]);
1462    let iter = iter.into_iter();
1463    let total = match iter.size_hint() {
1464        (lower, Some(upper)) if lower == upper && lower > 0 => Some(lower as f64),
1465        _ => None,
1466    };
1467    let live = Progress::new()
1468        .columns(columns)
1469        .start(Console::new(), std::io::stdout(), 10.0);
1470    let task = live.add_task(description, total, 0.0);
1471    TrackStdout {
1472        iter,
1473        live: Some(live),
1474        task,
1475        pending: false,
1476    }
1477}
1478
1479/// The iterator [`track`] returns; it owns its live display.
1480pub struct TrackStdout<I: Iterator> {
1481    iter: I,
1482    live: Option<LiveProgress<std::io::Stdout>>,
1483    task: TaskId,
1484    pending: bool,
1485}
1486
1487impl<I: Iterator> Iterator for TrackStdout<I> {
1488    type Item = I::Item;
1489
1490    fn next(&mut self) -> Option<I::Item> {
1491        let live = self.live.as_ref()?;
1492        if std::mem::take(&mut self.pending) {
1493            live.advance(self.task, 1.0);
1494        }
1495        match self.iter.next() {
1496            Some(item) => {
1497                self.pending = true;
1498                Some(item)
1499            }
1500            None => {
1501                if let Some(live) = self.live.take() {
1502                    live.stop();
1503                }
1504                None
1505            }
1506        }
1507    }
1508}
1509
1510impl<I: Iterator> Drop for TrackStdout<I> {
1511    fn drop(&mut self) {
1512        if let Some(live) = self.live.take() {
1513            live.stop();
1514        }
1515    }
1516}
1517
1518/// A whole, exactly representable count as an int, else the float.
1519fn whole_number(value: f64) -> FormatValue {
1520    if value.fract() == 0.0 && value.abs() < 9_007_199_254_740_992.0 {
1521        FormatValue::Int(value as i64)
1522    } else {
1523        FormatValue::Float(value)
1524    }
1525}
1526
1527#[cfg(test)]
1528mod tests {
1529    use super::*;
1530    use crate::color::ColorSystem;
1531
1532    fn render(progress: &Progress) -> String {
1533        Console::builder()
1534            .force_terminal(true)
1535            .color_system(Some(ColorSystem::Truecolor))
1536            .width(50)
1537            .no_color(false)
1538            .build()
1539            .render_to_string(progress)
1540    }
1541
1542    #[test]
1543    fn three_tasks_match_upstream() {
1544        // Captured from real rich 15.0.0 (default columns, width 50).
1545        let mut progress = Progress::new().columns(vec![
1546            ProgressColumn::Description,
1547            ProgressColumn::Bar,
1548            ProgressColumn::Percentage,
1549        ]);
1550        progress.add_task("Downloading", 100.0, 50.0);
1551        progress.add_task("Processing", 100.0, 100.0);
1552        progress.add_task("Waiting", 100.0, 0.0);
1553        let expected = concat!(
1554            "Downloading \x1b[38;2;249;38;114m━━━━━━━━━━━━━━━━\x1b[0m",
1555            "\x1b[38;2;249;38;114m╸\x1b[0m\x1b[38;5;237m━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 50%\x1b[0m\n",
1556            "Processing  \x1b[38;2;114;156;31m",
1557            "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m100%\x1b[0m\n",
1558            "Waiting     \x1b[38;5;237m",
1559            "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m  0%\x1b[0m",
1560        );
1561        assert_eq!(render(&progress), expected);
1562    }
1563
1564    #[test]
1565    fn download_text_matches_upstream() {
1566        // Captured from real rich 15.0.0 DownloadColumn.render (decimal units).
1567        let dl = |completed: f64, total: f64| {
1568            let mut progress = Progress::new();
1569            let id = progress.add_task("", total, completed);
1570            progress.task(id).unwrap().download_text(false)
1571        };
1572        assert_eq!(dl(500.0, 1000.0), "0.5/1.0 kB");
1573        assert_eq!(dl(500.0, 999.0), "500/999 bytes");
1574        assert_eq!(dl(1_500_000.0, 3_000_000.0), "1.5/3.0 MB");
1575        assert_eq!(dl(0.0, 1024.0), "0.0/1.0 kB");
1576        assert_eq!(dl(2_500_000_000.0, 10_000_000_000.0), "2.5/10.0 GB");
1577        assert_eq!(dl(250.0, 250.0), "250/250 bytes");
1578    }
1579
1580    #[test]
1581    fn download_column_in_grid_matches_upstream() {
1582        // Captured from real rich 15.0.0: description + bar + download at width 50.
1583        let mut progress = Progress::new().columns(vec![
1584            ProgressColumn::Description,
1585            ProgressColumn::Bar,
1586            ProgressColumn::Download,
1587        ]);
1588        progress.add_task("File", 1000.0, 500.0);
1589        let expected = concat!(
1590            "File \x1b[38;2;249;38;114m━━━━━━━━━━━━━━━━━\x1b[0m\x1b[38;5;237m╺\x1b[0m",
1591            "\x1b[38;5;237m━━━━━━━━━━━━━━━━\x1b[0m \x1b[32m0.5/1.0 kB\x1b[0m",
1592        );
1593        assert_eq!(render(&progress), expected);
1594    }
1595
1596    #[test]
1597    fn custom_columns_with_mofn_match_upstream() {
1598        // Captured from real rich 15.0.0: description + bar + M-of-N (differing
1599        // M-of-N widths → the narrower cell left-justifies with green padding).
1600        let mut progress = Progress::new().columns(vec![
1601            ProgressColumn::Description,
1602            ProgressColumn::Bar,
1603            ProgressColumn::MofN,
1604        ]);
1605        progress.add_task("A", 5.0, 3.0);
1606        progress.add_task("B", 100.0, 50.0);
1607        let console = Console::builder()
1608            .force_terminal(true)
1609            .color_system(Some(ColorSystem::Truecolor))
1610            .width(40)
1611            .no_color(false)
1612            .build();
1613        let expected = concat!(
1614            "A \x1b[38;2;249;38;114m━━━━━━━━━━━━━━━━━━\x1b[0m\x1b[38;5;237m╺\x1b[0m",
1615            "\x1b[38;5;237m━━━━━━━━━━━\x1b[0m \x1b[32m3/5    \x1b[0m\n",
1616            "B \x1b[38;2;249;38;114m━━━━━━━━━━━━━━━\x1b[0m\x1b[38;5;237m╺\x1b[0m",
1617            "\x1b[38;5;237m━━━━━━━━━━━━━━\x1b[0m \x1b[32m 50/100\x1b[0m",
1618        );
1619        assert_eq!(console.render_to_string(&progress), expected);
1620    }
1621
1622    fn live(columns: Vec<ProgressColumn>) -> LiveProgress<Vec<u8>> {
1623        let console = Console::builder()
1624            .force_terminal(true)
1625            .color_system(Some(ColorSystem::Truecolor))
1626            .width(40)
1627            .build();
1628        Progress::new()
1629            .columns(columns)
1630            .clock(|| 0.0)
1631            .start(console, Vec::new(), 1e-9)
1632    }
1633
1634    /// Run `f` on its own thread and fail (rather than hang the suite) if it
1635    /// has not finished within a few seconds.
1636    fn within_deadline<R: Send + 'static>(f: impl FnOnce() -> R + Send + 'static) -> R {
1637        let (done, wait) = std::sync::mpsc::channel();
1638        std::thread::spawn(move || {
1639            let _ = done.send(std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)));
1640        });
1641        match wait.recv_timeout(std::time::Duration::from_secs(10)) {
1642            Ok(Ok(value)) => value,
1643            Ok(Err(payload)) => std::panic::resume_unwind(payload),
1644            Err(_) => panic!("deadlocked: did not finish within 10s"),
1645        }
1646    }
1647
1648    #[test]
1649    fn refresh_inside_with_does_not_deadlock() {
1650        // Upstream's `Progress` lock is an `RLock` and `refresh()` renders in
1651        // the caller's thread, so refreshing while holding the lock is fine.
1652        let output = within_deadline(|| {
1653            let live = live(vec![ProgressColumn::Description, ProgressColumn::MofN]);
1654            live.with(|progress| {
1655                let task = progress.add_task("inside", Some(2.0), 1.0);
1656                live.refresh();
1657                task
1658            });
1659            live.refresh();
1660            String::from_utf8(live.stop().1).unwrap()
1661        });
1662        assert!(
1663            output.ends_with("inside \x1b[32m1/2\x1b[0m\n\x1b[?25h"),
1664            "{output:?}"
1665        );
1666    }
1667
1668    #[test]
1669    fn nested_with_panics_instead_of_deadlocking() {
1670        let result = within_deadline(|| {
1671            let live = live(vec![ProgressColumn::Description]);
1672            let nested = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1673                live.with(|_| live.add_task("nested", None, 0.0))
1674            }));
1675            // The display is still usable after the refused re-entry.
1676            let task = live.add_task("after", None, 0.0);
1677            live.stop();
1678            (nested.is_err(), task)
1679        });
1680        assert!(result.0, "a nested `with` must be refused, not deadlock");
1681    }
1682
1683    #[test]
1684    fn track_counts_each_item_after_its_loop_body() {
1685        let live = live(vec![ProgressColumn::Description, ProgressColumn::MofN]);
1686        let mut seen = Vec::new();
1687        let tracked = live.track(vec!['a', 'b', 'c'], None, "letters");
1688        let task = tracked.task();
1689        for item in tracked {
1690            // Upstream advances after the loop body, so the item being
1691            // processed is not yet counted.
1692            let completed = live.with(|progress| progress.task(task).unwrap().completed());
1693            seen.push((item, completed));
1694        }
1695        assert_eq!(seen, vec![('a', 0.0), ('b', 1.0), ('c', 2.0)]);
1696        let (progress, bytes) = live.stop();
1697        let task = progress.task(task).unwrap();
1698        assert_eq!((task.total(), task.completed()), (Some(3.0), 3.0));
1699        let output = String::from_utf8(bytes).unwrap();
1700        assert!(
1701            output.ends_with("letters \x1b[32m3/3\x1b[0m\n\x1b[?25h"),
1702            "{output:?}"
1703        );
1704    }
1705
1706    #[test]
1707    fn track_leaves_an_iterator_of_unknown_length_indeterminate() {
1708        let live = live(vec![ProgressColumn::MofN]);
1709        let task = {
1710            let mut tracked = live.track((0..10).filter(|n| n % 3 == 0), None, "");
1711            let task = tracked.task();
1712            assert_eq!(tracked.by_ref().count(), 4);
1713            task
1714        };
1715        let with_total = {
1716            let mut tracked = live.track(0..2, Some(5.0), "");
1717            tracked.by_ref().for_each(drop);
1718            tracked.task()
1719        };
1720        let (progress, _) = live.stop();
1721        assert_eq!(progress.task(task).unwrap().total(), None);
1722        assert_eq!(progress.task(task).unwrap().completed(), 4.0);
1723        assert_eq!(progress.task(with_total).unwrap().total(), Some(5.0));
1724    }
1725
1726    fn quiet_console() -> Console {
1727        Console::builder().force_terminal(false).width(40).build()
1728    }
1729
1730    #[test]
1731    fn wrap_read_advances_by_the_bytes_read() {
1732        use std::io::Read;
1733        let live = Progress::new()
1734            .disable(true)
1735            .start(quiet_console(), Vec::new(), 1.0);
1736        let mut reader = live
1737            .wrap_read(&b"hello world"[..], Some(11), None, "Reading...")
1738            .expect("total given");
1739        let task = reader.task();
1740        let mut buf = [0u8; 4];
1741        reader.read_exact(&mut buf).unwrap();
1742        assert_eq!(live.with(|p| p.task(task).unwrap().completed()), 4.0);
1743        let mut rest = Vec::new();
1744        reader.read_to_end(&mut rest).unwrap();
1745        assert!(live.with(|p| p.task(task).unwrap().finished()));
1746        let (_, out) = live.stop();
1747        assert!(out.is_empty(), "a disabled display writes nothing");
1748    }
1749
1750    #[test]
1751    fn wrap_read_needs_a_total() {
1752        let live = Progress::new()
1753            .disable(true)
1754            .start(quiet_console(), Vec::new(), 1.0);
1755        let err = live
1756            .wrap_read(&b""[..], None, None, "x")
1757            .err()
1758            .expect("no total");
1759        assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
1760        let task = live.add_task("sized", 5.0, 0.0);
1761        assert!(live.wrap_read(&b""[..], None, Some(task), "x").is_ok());
1762    }
1763
1764    #[test]
1765    fn open_takes_the_file_size_as_total() {
1766        use std::io::Read;
1767        let path = std::env::temp_dir().join(format!("rs-rich-open-{}", std::process::id()));
1768        std::fs::write(&path, b"0123456789").unwrap();
1769        let live = Progress::new()
1770            .disable(true)
1771            .start(quiet_console(), Vec::new(), 1.0);
1772        let mut reader = live.open(&path, None, None, "Reading...").unwrap();
1773        let task = reader.task();
1774        assert_eq!(live.with(|p| p.task(task).unwrap().total()), Some(10.0));
1775        std::io::copy(&mut reader, &mut std::io::sink()).unwrap();
1776        assert_eq!(live.with(|p| p.task(task).unwrap().completed()), 10.0);
1777        let _ = reader.read(&mut [0u8; 1]);
1778        drop(reader);
1779        std::fs::remove_file(path).unwrap();
1780    }
1781}