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    /// A user-defined column: upstream's subclass of `ProgressColumn`. See
197    /// [`CustomProgressColumn`] and [`ProgressColumn::custom`].
198    Custom(CustomColumn),
199}
200
201/// A user-defined progress column. Port of subclassing upstream's abstract
202/// `ProgressColumn`: implement [`render`](Self::render), and optionally the
203/// table column and `max_refresh`.
204pub trait CustomProgressColumn: Send + Sync {
205    /// The cell for `task`. Port of `ProgressColumn.render`.
206    fn render(&self, task: &Task) -> Cell;
207
208    /// The grid column this column occupies. Port of `get_table_column`
209    /// (upstream's default `Column()`).
210    fn table_column(&self) -> ColumnOptions {
211        ColumnOptions::default()
212    }
213
214    /// Reuse a render younger than this many seconds (upstream
215    /// `max_refresh`, default `None`: render every time). As upstream, the
216    /// cache is only consulted while the task has completed nothing.
217    fn max_refresh(&self) -> Option<f64> {
218        None
219    }
220}
221
222/// A [`CustomProgressColumn`] with upstream's `ProgressColumn.__call__`
223/// render cache.
224pub struct CustomColumn {
225    column: Arc<dyn CustomProgressColumn>,
226    cache: RefCell<HashMap<TaskId, (f64, Cell)>>,
227}
228
229impl CustomColumn {
230    /// Wrap a user-defined column.
231    pub fn new(column: Arc<dyn CustomProgressColumn>) -> Self {
232        CustomColumn {
233            column,
234            cache: RefCell::new(HashMap::new()),
235        }
236    }
237
238    /// The wrapped column.
239    pub fn column(&self) -> &Arc<dyn CustomProgressColumn> {
240        &self.column
241    }
242
243    /// Port of `ProgressColumn.__call__`.
244    fn call(&self, task: &Task) -> Cell {
245        let now = task.now();
246        let max_refresh = self.column.max_refresh();
247        if let Some(max_refresh) = max_refresh
248            && task.completed == 0.0
249            && let Some((timestamp, cell)) = self.cache.borrow().get(&task.id)
250            && timestamp + max_refresh > now
251        {
252            return cell.clone();
253        }
254        let cell = self.column.render(task);
255        self.cache.borrow_mut().insert(task.id, (now, cell.clone()));
256        cell
257    }
258}
259
260/// A progress bar column's width and styles. Port of `BarColumn`'s arguments.
261#[derive(Clone, Debug)]
262pub struct BarColumn {
263    bar_width: Option<usize>,
264    style: StyleType,
265    complete_style: StyleType,
266    finished_style: StyleType,
267    pulse_style: StyleType,
268}
269
270impl Default for BarColumn {
271    fn default() -> Self {
272        BarColumn {
273            bar_width: Some(40),
274            style: "bar.back".into(),
275            complete_style: "bar.complete".into(),
276            finished_style: "bar.finished".into(),
277            pulse_style: "bar.pulse".into(),
278        }
279    }
280}
281
282impl BarColumn {
283    /// Upstream's defaults: 40 cells wide, `bar.*` styles.
284    pub fn new() -> Self {
285        BarColumn::default()
286    }
287
288    /// The bar width, or `None` to fill the column (upstream `bar_width`).
289    pub fn bar_width(mut self, width: Option<usize>) -> Self {
290        self.bar_width = width;
291        self
292    }
293
294    /// The background style (upstream `style`).
295    pub fn style(mut self, style: impl Into<StyleType>) -> Self {
296        self.style = style.into();
297        self
298    }
299
300    /// The completed-part style (upstream `complete_style`).
301    pub fn complete_style(mut self, style: impl Into<StyleType>) -> Self {
302        self.complete_style = style.into();
303        self
304    }
305
306    /// The finished style (upstream `finished_style`).
307    pub fn finished_style(mut self, style: impl Into<StyleType>) -> Self {
308        self.finished_style = style.into();
309        self
310    }
311
312    /// The pulse style (upstream `pulse_style`).
313    pub fn pulse_style(mut self, style: impl Into<StyleType>) -> Self {
314        self.pulse_style = style.into();
315        self
316    }
317
318    /// Port of `BarColumn.render`.
319    fn render(&self, task: &Task) -> ProgressBar {
320        let bar = match task.total {
321            Some(total) => ProgressBar::new(total.max(0.0), task.completed.max(0.0)),
322            None => ProgressBar::indeterminate(),
323        };
324        let bar = match self.bar_width {
325            Some(width) => bar.width(width.max(1)),
326            None => bar,
327        };
328        bar.pulse(!task.started())
329            .animation_time(task.now())
330            .style(self.style.clone())
331            .complete_style(self.complete_style.clone())
332            .finished_style(self.finished_style.clone())
333            .pulse_style(self.pulse_style.clone())
334    }
335}
336
337impl ProgressColumn {
338    /// `TimeRemainingColumn()` with upstream's defaults.
339    pub fn time_remaining() -> Self {
340        ProgressColumn::TimeRemaining(TimeRemainingColumn::new(false, false))
341    }
342
343    /// `SpinnerColumn()` with upstream's defaults (`dots`, finished text `" "`).
344    pub fn spinner() -> Self {
345        ProgressColumn::Spinner(SpinnerColumn::new("dots", " "))
346    }
347
348    /// A user-defined column (upstream's `ProgressColumn` subclass).
349    pub fn custom(column: impl CustomProgressColumn + 'static) -> Self {
350        ProgressColumn::Custom(CustomColumn::new(Arc::new(column)))
351    }
352
353    /// This column with explicit table-column options, as upstream's
354    /// `table_column=Column(...)` argument sets them: width, ratio, justify,
355    /// wrapping and style of the grid column.
356    pub fn with_table_column(self, options: ColumnOptions) -> Self {
357        let inner = match self {
358            ProgressColumn::WithTableColumn(inner, _) => *inner,
359            column => column,
360        };
361        ProgressColumn::WithTableColumn(Box::new(inner), options)
362    }
363
364    /// Port of `get_table_column()`: text columns default to
365    /// `Column(no_wrap=True)`, the rest to `Column()`.
366    pub fn table_column(&self) -> ColumnOptions {
367        match self {
368            ProgressColumn::WithTableColumn(_, options) => options.clone(),
369            ProgressColumn::Custom(column) => column.column.table_column(),
370            ProgressColumn::Description
371            | ProgressColumn::Text(..)
372            | ProgressColumn::TextFormat(_)
373            | ProgressColumn::Percentage
374            | ProgressColumn::TaskProgress { .. } => ColumnOptions {
375                no_wrap: true,
376                ..ColumnOptions::default()
377            },
378            _ => ColumnOptions::default(),
379        }
380    }
381
382    /// The grid cell for `task`: the column's `__call__(task)`. Stateful
383    /// columns (spinners, the remaining-time and custom caches) advance as
384    /// upstream's do, so call it once per task per refresh.
385    pub fn table_cell(&self, task: &Task) -> Cell {
386        match self {
387            ProgressColumn::WithTableColumn(inner, _) => inner.table_cell(task),
388            ProgressColumn::Custom(column) => column.call(task),
389            ProgressColumn::Bar => Cell::Renderable(Arc::new(BarColumn::default().render(task))),
390            ProgressColumn::BarWith(column) => Cell::Renderable(Arc::new(column.render(task))),
391            ProgressColumn::Renderable(renderable) => Cell::Renderable(renderable.clone()),
392            column => Cell::Text(column.cell(task)),
393        }
394    }
395
396    /// The cell for `task` (never called on [`ProgressColumn::Bar`]).
397    fn cell(&self, task: &Task) -> Text {
398        let named = |plain: String, style: &str| Text::styled(plain, style);
399        match self {
400            // `TextColumn`s hand their `justify` (default left) to the text, so
401            // it overrides the table column's.
402            ProgressColumn::Description => {
403                let markup = format!("[progress.description]{}", task.description);
404                Text::from_markup(&markup)
405                    .unwrap_or_else(|_| Text::new(task.description.clone()))
406                    .justify(Justify::Left)
407            }
408            ProgressColumn::Text(text, style) => {
409                Text::styled(text.clone(), style.clone()).justify(Justify::Left)
410            }
411            ProgressColumn::TextFormat(column) => column.render(task),
412            ProgressColumn::Bar
413            | ProgressColumn::BarWith(_)
414            | ProgressColumn::Renderable(_)
415            | ProgressColumn::Custom(_)
416            | ProgressColumn::WithTableColumn(..) => {
417                unreachable!("bar, renderable and wrapped columns have no text cell")
418            }
419            ProgressColumn::Percentage => task.percentage_cell().justify(Justify::Left),
420            ProgressColumn::TaskProgress { show_speed } => {
421                if task.total.is_none() && *show_speed {
422                    render_speed(
423                        task.finished_speed
424                            .filter(|s| *s != 0.0)
425                            .or_else(|| task.speed()),
426                    )
427                } else {
428                    task.percentage_cell().justify(Justify::Left)
429                }
430            }
431            ProgressColumn::MofN => named(task.mofn_text(), "progress.download"),
432            ProgressColumn::Download => named(task.download_text(false), "progress.download"),
433            ProgressColumn::BinaryDownload => named(task.download_text(true), "progress.download"),
434            ProgressColumn::TimeElapsed => {
435                let elapsed = if task.finished() {
436                    task.finished_time
437                } else {
438                    task.elapsed()
439                };
440                let text = match elapsed {
441                    None => "-:--:--".to_string(),
442                    Some(elapsed) => timedelta(elapsed.max(0.0) as i64),
443                };
444                named(text, "progress.elapsed")
445            }
446            ProgressColumn::TimeRemaining(column) => column.render(task),
447            ProgressColumn::TransferSpeed => {
448                let speed = task
449                    .finished_speed
450                    .filter(|s| *s != 0.0)
451                    .or_else(|| task.speed());
452                let text = match speed {
453                    None => "?".to_string(),
454                    Some(speed) => format!("{}/s", filesize::decimal_signed(speed as i64)),
455                };
456                named(text, "progress.data.speed")
457            }
458            // `filesize.decimal(int(task.completed))`: `int()` truncates
459            // toward zero and keeps the sign.
460            ProgressColumn::FileSize => named(
461                filesize::decimal_signed(task.completed as i64),
462                "progress.filesize",
463            ),
464            ProgressColumn::TotalFileSize => named(
465                task.total
466                    .map_or_else(String::new, |total| filesize::decimal_signed(total as i64)),
467                "progress.filesize.total",
468            ),
469            ProgressColumn::Spinner(column) => {
470                if task.finished() {
471                    Text::from_markup(&column.finished_text)
472                        .unwrap_or_else(|_| Text::new(column.finished_text.clone()))
473                } else {
474                    // Upstream's `self.spinner.render(task.get_time())`: the
475                    // spinner itself starts its animation at the first render.
476                    let mut frame = column.spinner.render(task.now());
477                    frame.set_base_style(column.style.clone());
478                    frame
479                }
480            }
481        }
482    }
483}
484
485impl TimeRemainingColumn {
486    fn render(&self, task: &Task) -> Text {
487        // `ProgressColumn.__call__`: reuse a render younger than max_refresh,
488        // but only while the task has completed nothing (`not task.completed`).
489        let now = task.now();
490        if task.completed == 0.0
491            && let Some((timestamp, text)) = self.cache.borrow().get(&task.id)
492            && timestamp + 0.5 > now
493        {
494            return text.clone();
495        }
496        let (task_time, style) = if self.elapsed_when_finished && task.finished() {
497            (task.finished_time, "progress.elapsed")
498        } else {
499            (task.time_remaining(), "progress.remaining")
500        };
501        let text = if task.total.is_none() {
502            Text::styled("", style)
503        } else {
504            match task_time {
505                None => Text::styled(if self.compact { "--:--" } else { "-:--:--" }, style),
506                Some(task_time) => {
507                    let whole = task_time as i64;
508                    let (minutes, seconds) = (whole.div_euclid(60), whole.rem_euclid(60));
509                    let (hours, minutes) = (minutes.div_euclid(60), minutes.rem_euclid(60));
510                    let formatted = if self.compact && hours == 0 {
511                        format!("{minutes:02}:{seconds:02}")
512                    } else {
513                        format!("{hours}:{minutes:02}:{seconds:02}")
514                    };
515                    Text::styled(formatted, style)
516                }
517            }
518        };
519        self.cache.borrow_mut().insert(task.id, (now, text.clone()));
520        text
521    }
522}
523
524/// `TaskProgressColumn.render_speed`: iterations per second with a power-of-ten
525/// suffix, e.g. `2.5×10³ it/s`.
526fn render_speed(speed: Option<f64>) -> Text {
527    let Some(speed) = speed else {
528        return Text::styled("", "progress.percentage");
529    };
530    let (unit, suffix) = filesize::pick_unit_and_suffix_signed(
531        speed as i64,
532        &["", "×10³", "×10⁶", "×10⁹", "×10¹²"],
533        1000,
534    );
535    let data_speed = speed / unit as f64;
536    Text::styled(
537        format!("{data_speed:.1}{suffix} it/s"),
538        "progress.percentage",
539    )
540}
541
542/// Python's `str(timedelta(seconds=n))` for `n >= 0`: `H:MM:SS`, prefixed by
543/// `N day(s), ` from a day upward.
544fn timedelta(total_seconds: i64) -> String {
545    let days = total_seconds / 86_400;
546    let rest = total_seconds % 86_400;
547    let clock = format!("{}:{:02}:{:02}", rest / 3600, rest % 3600 / 60, rest % 60);
548    match days {
549        0 => clock,
550        1 => format!("1 day, {clock}"),
551        days => format!("{days} days, {clock}"),
552    }
553}
554
555/// Python's `f"{value:,.{precision}f}"`: fixed precision with `,` grouping.
556fn grouped(value: f64, precision: usize) -> String {
557    let formatted = format!("{value:.precision$}");
558    let (sign, digits) = match formatted.strip_prefix('-') {
559        Some(rest) => ("-", rest),
560        None => ("", formatted.as_str()),
561    };
562    let (integer, fraction) = match digits.split_once('.') {
563        Some((integer, fraction)) => (integer, Some(fraction)),
564        None => (digits, None),
565    };
566    let mut grouped = String::new();
567    for (index, digit) in integer.chars().enumerate() {
568        if index > 0 && (integer.len() - index) % 3 == 0 {
569            grouped.push(',');
570        }
571        grouped.push(digit);
572    }
573    match fraction {
574        Some(fraction) => format!("{sign}{grouped}.{fraction}"),
575        None => format!("{sign}{grouped}"),
576    }
577}
578
579/// A single tracked task. Mirrors `rich.progress.Task`; read-only outside
580/// [`Progress`].
581pub struct Task {
582    id: TaskId,
583    description: String,
584    total: Option<f64>,
585    completed: f64,
586    visible: bool,
587    start_time: Option<f64>,
588    stop_time: Option<f64>,
589    finished_time: Option<f64>,
590    finished_speed: Option<f64>,
591    /// `(timestamp, completed)` speed samples (upstream `ProgressSample`).
592    samples: VecDeque<(f64, f64)>,
593    /// Arbitrary per-task values for format strings (upstream `fields`).
594    fields: BTreeMap<String, FormatValue>,
595    get_time: GetTime,
596}
597
598impl Task {
599    /// This task's custom fields (upstream `Task.fields`).
600    pub fn fields(&self) -> &BTreeMap<String, FormatValue> {
601        &self.fields
602    }
603
604    /// Resolve a `str.format` field name against this task, as
605    /// `text_format.format(task=task)` does: `task.<attribute>` or
606    /// `task.fields[<name>]`.
607    fn format_field(&self, name: &str) -> Option<FormatValue> {
608        let attribute = name.strip_prefix("task.")?;
609        if let Some(key) = attribute
610            .strip_prefix("fields[")
611            .and_then(|rest| rest.strip_suffix(']'))
612        {
613            return self.fields.get(key).cloned();
614        }
615        Some(match attribute {
616            "id" => FormatValue::Int(self.id.0 as i64),
617            "description" => FormatValue::Str(self.description.clone()),
618            // Python keeps the ints a caller passes (`total=200` prints `200`);
619            // this API takes floats, so a whole number formats as an int.
620            "total" => self.total.map_or(FormatValue::None, whole_number),
621            "completed" => whole_number(self.completed),
622            "visible" => FormatValue::Bool(self.visible),
623            "started" => FormatValue::Bool(self.started()),
624            "finished" => FormatValue::Bool(self.finished()),
625            "percentage" => FormatValue::Float(self.percentage()),
626            "remaining" => self.remaining().into(),
627            "elapsed" => self.elapsed().into(),
628            "speed" => self.speed().into(),
629            "time_remaining" => self.time_remaining().into(),
630            "start_time" => self.start_time.into(),
631            "stop_time" => self.stop_time.into(),
632            "finished_time" => self.finished_time.into(),
633            "finished_speed" => self.finished_speed.into(),
634            _ => return None,
635        })
636    }
637    fn now(&self) -> f64 {
638        (self.get_time)()
639    }
640
641    /// The current time by the progress's clock. Port of `Task.get_time`.
642    pub fn get_time(&self) -> f64 {
643        self.now()
644    }
645
646    /// This task's id.
647    pub fn id(&self) -> TaskId {
648        self.id
649    }
650
651    /// The description (console markup).
652    pub fn description(&self) -> &str {
653        &self.description
654    }
655
656    /// The total number of steps, or `None` when indeterminate.
657    pub fn total(&self) -> Option<f64> {
658        self.total
659    }
660
661    /// The number of steps completed.
662    pub fn completed(&self) -> f64 {
663        self.completed
664    }
665
666    /// Whether the task is shown.
667    pub fn visible(&self) -> bool {
668        self.visible
669    }
670
671    /// Whether the task has been started.
672    pub fn started(&self) -> bool {
673        self.start_time.is_some()
674    }
675
676    /// Steps left, or `None` when indeterminate.
677    pub fn remaining(&self) -> Option<f64> {
678        self.total.map(|total| total - self.completed)
679    }
680
681    /// Seconds since the task started (to its stop time, if stopped).
682    pub fn elapsed(&self) -> Option<f64> {
683        let start = self.start_time?;
684        Some(self.stop_time.unwrap_or_else(|| self.now()) - start)
685    }
686
687    /// Whether the task has reached its total.
688    pub fn finished(&self) -> bool {
689        self.finished_time.is_some()
690    }
691
692    /// The elapsed time recorded when the task finished.
693    pub fn finished_time(&self) -> Option<f64> {
694        self.finished_time
695    }
696
697    /// The completion percentage, clamped to 0–100 (0 without a total).
698    pub fn percentage(&self) -> f64 {
699        match self.total {
700            Some(total) if total != 0.0 => (self.completed / total * 100.0).clamp(0.0, 100.0),
701            _ => 0.0,
702        }
703    }
704
705    /// Steps per second over the sample window, or `None` without enough samples.
706    pub fn speed(&self) -> Option<f64> {
707        self.start_time?;
708        let (first, _) = *self.samples.front()?;
709        let (last, _) = *self.samples.back()?;
710        let total_time = last - first;
711        if total_time == 0.0 {
712            return None;
713        }
714        let total_completed: f64 = self.samples.iter().skip(1).map(|(_, done)| done).sum();
715        Some(total_completed / total_time)
716    }
717
718    /// Estimated seconds remaining (rounded up), 0 once finished.
719    pub fn time_remaining(&self) -> Option<f64> {
720        if self.finished() {
721            return Some(0.0);
722        }
723        let speed = self.speed().filter(|speed| *speed != 0.0)?;
724        let remaining = self.remaining()?;
725        Some((remaining / speed).ceil())
726    }
727
728    /// `Task._reset`.
729    fn clear_progress(&mut self) {
730        self.samples.clear();
731        self.finished_time = None;
732        self.finished_speed = None;
733    }
734
735    /// The percentage cell: `[progress.percentage]{percentage:>3.0f}%`, empty
736    /// without a total (`text_format_no_percentage`).
737    fn percentage_cell(&self) -> Text {
738        if self.total.is_none() {
739            return Text::new("");
740        }
741        let mut text = Text::new(format!("{:>3.0}%", self.percentage()));
742        let len = text.plain().len();
743        text.stylize("progress.percentage", 0, len);
744        text
745    }
746
747    /// The M-of-N cell text: `completed` right-justified to the width of `total`
748    /// (`?` when indeterminate), then `/total`. Port of `MofNCompleteColumn.render`.
749    fn mofn_text(&self) -> String {
750        let completed = self.completed as i64;
751        let total = self
752            .total
753            .map_or_else(|| "?".to_string(), |total| (total as i64).to_string());
754        let total_width = total.chars().count();
755        format!("{completed:>total_width$}/{total}")
756    }
757
758    /// The download cell text: `completed`/`total` in a shared byte unit, e.g.
759    /// `0.5/1.0 kB`. Port of `DownloadColumn.render`.
760    fn download_text(&self, binary: bool) -> String {
761        const DECIMAL: &[&str] = &["bytes", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
762        const BINARY: &[&str] = &[
763            "bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB",
764        ];
765        // `int(task.completed)` / `int(task.total)`: truncated, sign kept.
766        let completed = self.completed as i64;
767        let base_size = self.total.map_or(completed, |total| total as i64);
768        let (unit, suffix) = if binary {
769            filesize::pick_unit_and_suffix_signed(base_size, BINARY, 1024)
770        } else {
771            filesize::pick_unit_and_suffix_signed(base_size, DECIMAL, 1000)
772        };
773        let precision = if unit == 1 { 0 } else { 1 };
774        let completed_str = grouped(completed as f64 / unit as f64, precision);
775        let total_str = self.total.map_or_else(
776            || "?".to_string(),
777            |total| grouped((total as i64) as f64 / unit as f64, precision),
778        );
779        format!("{completed_str}/{total_str} {suffix}")
780    }
781}
782
783/// Changes for [`Progress::update`]; unset fields are left alone. Upstream's
784/// keyword arguments to `Progress.update`.
785#[derive(Debug, Clone, Default)]
786pub struct TaskUpdate {
787    pub total: Option<f64>,
788    pub completed: Option<f64>,
789    pub advance: Option<f64>,
790    pub description: Option<String>,
791    pub visible: Option<bool>,
792    /// Custom fields to set (upstream `**fields`).
793    pub fields: Vec<(String, FormatValue)>,
794    /// Redraw a live display right after the update (upstream `refresh=True`).
795    pub refresh: bool,
796}
797
798impl TaskUpdate {
799    pub fn total(mut self, total: f64) -> Self {
800        self.total = Some(total);
801        self
802    }
803
804    pub fn completed(mut self, completed: f64) -> Self {
805        self.completed = Some(completed);
806        self
807    }
808
809    pub fn advance(mut self, advance: f64) -> Self {
810        self.advance = Some(advance);
811        self
812    }
813
814    pub fn description(mut self, description: impl Into<String>) -> Self {
815        self.description = Some(description.into());
816        self
817    }
818
819    pub fn visible(mut self, visible: bool) -> Self {
820        self.visible = Some(visible);
821        self
822    }
823
824    /// Redraw a [`LiveProgress`] right after this update (upstream
825    /// `refresh=True`).
826    pub fn refresh(mut self, refresh: bool) -> Self {
827        self.refresh = refresh;
828        self
829    }
830
831    /// Set a custom field (upstream `update(task_id, **fields)`).
832    pub fn field(mut self, name: impl Into<String>, value: impl Into<FormatValue>) -> Self {
833        self.fields.push((name.into(), value.into()));
834        self
835    }
836}
837
838/// A progress display over one or more [`Task`]s. Mirrors `rich.progress.Progress`.
839pub struct Progress {
840    tasks: Vec<Task>,
841    next_id: usize,
842    columns: Vec<ProgressColumn>,
843    get_time: GetTime,
844    speed_estimate_period: f64,
845    expand: bool,
846    transient: bool,
847    disable: bool,
848    redirect_stdout: bool,
849    redirect_stderr: bool,
850}
851
852impl Default for Progress {
853    fn default() -> Self {
854        Progress {
855            tasks: Vec::new(),
856            next_id: 0,
857            columns: Progress::default_columns(),
858            get_time: Arc::new(monotonic),
859            speed_estimate_period: 30.0,
860            expand: false,
861            transient: false,
862            disable: false,
863            redirect_stdout: true,
864            redirect_stderr: true,
865        }
866    }
867}
868
869impl Progress {
870    pub fn new() -> Self {
871        Progress::default()
872    }
873
874    /// Upstream's `Progress.get_default_columns()`: description, bar,
875    /// percentage and time remaining.
876    pub fn default_columns() -> Vec<ProgressColumn> {
877        vec![
878            ProgressColumn::Description,
879            ProgressColumn::Bar,
880            ProgressColumn::Percentage,
881            ProgressColumn::time_remaining(),
882        ]
883    }
884
885    /// Replace the column list (default: [`default_columns`](Self::default_columns)).
886    pub fn columns(mut self, columns: Vec<ProgressColumn>) -> Self {
887        self.columns = columns;
888        self
889    }
890
891    /// Read time from `clock` (seconds) instead of the monotonic clock.
892    /// Upstream's `get_time`; makes time-based columns deterministic.
893    pub fn clock(mut self, clock: impl Fn() -> f64 + Send + Sync + 'static) -> Self {
894        self.get_time = Arc::new(clock);
895        for task in &mut self.tasks {
896            task.get_time = self.get_time.clone();
897        }
898        self
899    }
900
901    /// Stretch the task grid to the full width (upstream `expand`).
902    pub fn expand(mut self, expand: bool) -> Self {
903        self.expand = expand;
904        self
905    }
906
907    /// Erase the display when it stops (upstream `transient`).
908    pub fn transient(mut self, transient: bool) -> Self {
909        self.transient = transient;
910        self
911    }
912
913    /// Print what is written to [`LiveProgress::stdout`] above the display
914    /// (upstream `redirect_stdout`, default true).
915    pub fn redirect_stdout(mut self, redirect: bool) -> Self {
916        self.redirect_stdout = redirect;
917        self
918    }
919
920    /// The same for [`LiveProgress::stderr`] (upstream `redirect_stderr`).
921    pub fn redirect_stderr(mut self, redirect: bool) -> Self {
922        self.redirect_stderr = redirect;
923        self
924    }
925
926    /// Show nothing: [`start`](Self::start) draws no display, while tasks
927    /// still update (upstream `disable`).
928    pub fn disable(mut self, disable: bool) -> Self {
929        self.disable = disable;
930        self
931    }
932
933    /// Seconds of history used for speed estimates (default 30).
934    pub fn speed_estimate_period(mut self, seconds: f64) -> Self {
935        self.speed_estimate_period = seconds;
936        self
937    }
938
939    fn now(&self) -> f64 {
940        (self.get_time)()
941    }
942
943    fn task_mut(&mut self, id: TaskId) -> Option<&mut Task> {
944        self.tasks.iter_mut().find(|task| task.id == id)
945    }
946
947    /// Add a started task and return its id. Port of `Progress.add_task`
948    /// (`start=True`); `total` of `None` is an indeterminate task.
949    pub fn add_task(
950        &mut self,
951        description: impl Into<String>,
952        total: impl Into<Option<f64>>,
953        completed: f64,
954    ) -> TaskId {
955        let id = self.push_task(description.into(), total.into(), completed);
956        self.start_task(id);
957        id
958    }
959
960    /// Add a task that has not started (`add_task(start=False)`): it shows no
961    /// elapsed time until [`start_task`](Self::start_task).
962    pub fn add_unstarted_task(
963        &mut self,
964        description: impl Into<String>,
965        total: impl Into<Option<f64>>,
966        completed: f64,
967    ) -> TaskId {
968        self.push_task(description.into(), total.into(), completed)
969    }
970
971    /// Add a task with custom fields for format strings. Port of
972    /// `add_task(description, total=…, completed=…, start=…, **fields)`.
973    pub fn add_task_with<K: Into<String>, V: Into<FormatValue>>(
974        &mut self,
975        description: impl Into<String>,
976        total: impl Into<Option<f64>>,
977        completed: f64,
978        start: bool,
979        fields: impl IntoIterator<Item = (K, V)>,
980    ) -> TaskId {
981        let id = self.push_task(description.into(), total.into(), completed);
982        if let Some(task) = self.task_mut(id) {
983            task.fields = fields
984                .into_iter()
985                .map(|(name, value)| (name.into(), value.into()))
986                .collect();
987        }
988        if start {
989            self.start_task(id);
990        }
991        id
992    }
993
994    fn push_task(&mut self, description: String, total: Option<f64>, completed: f64) -> TaskId {
995        let id = TaskId(self.next_id);
996        self.next_id += 1;
997        self.tasks.push(Task {
998            id,
999            description,
1000            total,
1001            completed,
1002            visible: true,
1003            start_time: None,
1004            stop_time: None,
1005            finished_time: None,
1006            finished_speed: None,
1007            samples: VecDeque::new(),
1008            fields: BTreeMap::new(),
1009            get_time: self.get_time.clone(),
1010        });
1011        id
1012    }
1013
1014    /// The task with this id, if it has not been removed.
1015    pub fn task(&self, id: TaskId) -> Option<&Task> {
1016        self.tasks.iter().find(|task| task.id == id)
1017    }
1018
1019    /// Every task, in the order added.
1020    pub fn tasks(&self) -> &[Task] {
1021        &self.tasks
1022    }
1023
1024    /// Whether every task has finished. Port of `Progress.finished`.
1025    pub fn finished(&self) -> bool {
1026        self.tasks.iter().all(Task::finished)
1027    }
1028
1029    /// Start a task's clock if it has not started. Port of `start_task`.
1030    pub fn start_task(&mut self, id: TaskId) {
1031        let now = self.now();
1032        if let Some(task) = self.task_mut(id) {
1033            task.start_time.get_or_insert(now);
1034        }
1035    }
1036
1037    /// Stop a task's clock; its elapsed time freezes. Port of `stop_task`.
1038    pub fn stop_task(&mut self, id: TaskId) {
1039        let now = self.now();
1040        if let Some(task) = self.task_mut(id) {
1041            task.start_time.get_or_insert(now);
1042            task.stop_time = Some(now);
1043        }
1044    }
1045
1046    /// Update a task. Port of `Progress.update`: a new total clears the speed
1047    /// samples; positive progress adds a sample; reaching the total records the
1048    /// finish time.
1049    pub fn update(&mut self, id: TaskId, update: TaskUpdate) {
1050        let now = self.now();
1051        let period = self.speed_estimate_period;
1052        let Some(task) = self.task_mut(id) else {
1053            return;
1054        };
1055        let completed_start = task.completed;
1056        if let Some(total) = update.total
1057            && Some(total) != task.total
1058        {
1059            task.total = Some(total);
1060            task.clear_progress();
1061        }
1062        if let Some(advance) = update.advance {
1063            task.completed += advance;
1064        }
1065        if let Some(completed) = update.completed {
1066            task.completed = completed;
1067        }
1068        if let Some(description) = update.description {
1069            task.description = description;
1070        }
1071        if let Some(visible) = update.visible {
1072            task.visible = visible;
1073        }
1074        task.fields.extend(update.fields);
1075        let update_completed = task.completed - completed_start;
1076        let old_sample_time = now - period;
1077        while task
1078            .samples
1079            .front()
1080            .is_some_and(|(time, _)| *time < old_sample_time)
1081        {
1082            task.samples.pop_front();
1083        }
1084        if update_completed > 0.0 {
1085            task.samples.push_back((now, update_completed));
1086            if task.samples.len() > MAX_SAMPLES {
1087                task.samples.pop_front();
1088            }
1089        }
1090        if task.total.is_some_and(|total| task.completed >= total) && task.finished_time.is_none() {
1091            task.finished_time = task.elapsed();
1092        }
1093    }
1094
1095    /// Advance a task by `amount` steps. Port of `Progress.advance`, which
1096    /// (unlike `update`) always records a sample and the finish speed.
1097    pub fn advance(&mut self, id: TaskId, amount: f64) {
1098        let now = self.now();
1099        let period = self.speed_estimate_period;
1100        let Some(task) = self.task_mut(id) else {
1101            return;
1102        };
1103        let completed_start = task.completed;
1104        task.completed += amount;
1105        let update_completed = task.completed - completed_start;
1106        let old_sample_time = now - period;
1107        while task
1108            .samples
1109            .front()
1110            .is_some_and(|(time, _)| *time < old_sample_time)
1111        {
1112            task.samples.pop_front();
1113        }
1114        while task.samples.len() > MAX_SAMPLES {
1115            task.samples.pop_front();
1116        }
1117        task.samples.push_back((now, update_completed));
1118        if task.samples.len() > MAX_SAMPLES {
1119            task.samples.pop_front();
1120        }
1121        if task.total.is_some_and(|total| task.completed >= total) && task.finished_time.is_none() {
1122            task.finished_time = task.elapsed();
1123            task.finished_speed = task.speed();
1124        }
1125    }
1126
1127    /// Reset a task to `completed`, optionally restarting its clock and
1128    /// changing its total. Port of `Progress.reset`. Like upstream, a stop
1129    /// time set earlier is kept.
1130    pub fn reset(&mut self, id: TaskId, start: bool, total: Option<f64>, completed: f64) {
1131        let now = self.now();
1132        let Some(task) = self.task_mut(id) else {
1133            return;
1134        };
1135        task.clear_progress();
1136        task.start_time = start.then_some(now);
1137        if let Some(total) = total {
1138            task.total = Some(total);
1139        }
1140        task.completed = completed;
1141        task.finished_time = None;
1142    }
1143
1144    /// Remove a task. Port of `Progress.remove_task`.
1145    pub fn remove_task(&mut self, id: TaskId) {
1146        self.tasks.retain(|task| task.id != id);
1147    }
1148}
1149
1150impl Progress {
1151    /// The grid the display renders. Port of `Progress.make_tasks_table`: a
1152    /// `Table.grid` with one column per [`ProgressColumn`] (its table column
1153    /// options), `padding=(0, 1)` and the progress's `expand`, and one row per
1154    /// visible task.
1155    pub fn make_tasks_table(&self) -> Table {
1156        let mut table = Table::grid().padding(0, 1, 0, 1).expand(self.expand);
1157        for column in &self.columns {
1158            table.add_column_with(Text::new(""), column.table_column());
1159        }
1160        for task in self.tasks.iter().filter(|task| task.visible) {
1161            // Each column is called once per row, in order: spinners and the
1162            // remaining-time cache are stateful, as upstream's columns are.
1163            let cells = self
1164                .columns
1165                .iter()
1166                .map(|column| column.table_cell(task))
1167                .collect();
1168            table.add_row_cells(cells);
1169        }
1170        table
1171    }
1172}
1173
1174impl Renderable for Progress {
1175    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
1176        self.make_tasks_table().rich_render(console, options)
1177    }
1178}
1179
1180impl Progress {
1181    /// Start an auto-refreshing live display of this progress, redrawn
1182    /// `refresh_per_second` times a second on a background thread. Port of
1183    /// `Progress.start` with `auto_refresh=True`; stop it with
1184    /// [`LiveProgress::stop`] (upstream's `with progress:` block).
1185    pub fn start<W: std::io::Write + Send + 'static>(
1186        self,
1187        console: Console,
1188        writer: W,
1189        refresh_per_second: f64,
1190    ) -> LiveProgress<W> {
1191        // `disable` draws nothing: upstream skips `live.start()` and `stop()`.
1192        if self.disable {
1193            return LiveProgress {
1194                progress: Arc::new(std::sync::Mutex::new(self)),
1195                live: None,
1196                disabled: Some(std::sync::Mutex::new((console, writer))),
1197                interactive: true,
1198                holder: Arc::new(std::sync::Mutex::new(None)),
1199                handle: crate::live::LiveHandle::detached(),
1200            };
1201        }
1202        let options = crate::live::LiveOptions {
1203            transient: self.transient,
1204            redirect_stdout: self.redirect_stdout,
1205            redirect_stderr: self.redirect_stderr,
1206            ..crate::live::LiveOptions::default()
1207        };
1208        // `Console.is_interactive`: a terminal that is not dumb.
1209        let interactive = console.is_terminal() && !console.is_dumb_terminal();
1210        let shared = Arc::new(std::sync::Mutex::new(self));
1211        let view = shared.clone();
1212        let live = crate::live::Live::spawn_get_renderable(
1213            // Upstream's `get_renderable`: the task table as of this refresh.
1214            move || {
1215                let progress = view.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
1216                Box::new(progress.make_tasks_table()) as Box<dyn Renderable>
1217            },
1218            console,
1219            writer,
1220            refresh_per_second,
1221            options,
1222        );
1223        let holder: crate::live::Holder = Arc::new(std::sync::Mutex::new(None));
1224        LiveProgress {
1225            progress: shared,
1226            handle: live.handle().with_holder(holder.clone()),
1227            live: Some(live),
1228            disabled: None,
1229            interactive,
1230            holder,
1231        }
1232    }
1233}
1234
1235/// A running [`Progress`] display. Task changes made through it are picked up
1236/// by the next refresh, as with upstream's `refresh=False` updates.
1237pub struct LiveProgress<W: std::io::Write + Send + 'static> {
1238    progress: Arc<std::sync::Mutex<Progress>>,
1239    live: Option<crate::live::AutoLive<W>>,
1240    /// The console and sink of a disabled display, which never reaches a
1241    /// live thread; prints go straight to the sink.
1242    disabled: Option<std::sync::Mutex<(Console, W)>>,
1243    /// Whether the console is a terminal; `stop` ends a file with a newline.
1244    interactive: bool,
1245    /// The thread inside [`with`](LiveProgress::with), if any. Upstream's lock
1246    /// is an `RLock`; ours is not, so re-entry is detected rather than left to
1247    /// deadlock on the mutex or on the refresh thread.
1248    holder: crate::live::Holder,
1249    /// The refresh thread, for prints and proxies (detached when disabled).
1250    handle: crate::live::LiveHandle<W>,
1251}
1252
1253/// Clears [`LiveProgress`]'s `holder` when a `with` block ends, unwinding
1254/// included.
1255struct HolderGuard<'a>(&'a crate::live::Holder);
1256
1257impl Drop for HolderGuard<'_> {
1258    fn drop(&mut self) {
1259        *self
1260            .0
1261            .lock()
1262            .unwrap_or_else(|poisoned| poisoned.into_inner()) = None;
1263    }
1264}
1265
1266impl<W: std::io::Write + Send + 'static> LiveProgress<W> {
1267    /// Run `f` with the progress locked, for any change not wrapped below.
1268    ///
1269    /// `f` may call [`refresh`](Self::refresh) (the frame is drawn as soon as
1270    /// the lock is released), but not `with` or a method built on it: upstream
1271    /// re-enters its `RLock`, which a `&mut Progress` cannot express, so a
1272    /// nested call panics instead of deadlocking.
1273    pub fn with<R>(&self, f: impl FnOnce(&mut Progress) -> R) -> R {
1274        let current = std::thread::current().id();
1275        assert!(
1276            !self.held_by(current),
1277            "LiveProgress::with re-entered from inside a `with` closure; \
1278             use the `&mut Progress` it was given instead"
1279        );
1280        let mut progress = match self.progress.lock() {
1281            Ok(progress) => progress,
1282            Err(poisoned) => poisoned.into_inner(),
1283        };
1284        *self
1285            .holder
1286            .lock()
1287            .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(current);
1288        let _holder = HolderGuard(&self.holder);
1289        f(&mut progress)
1290    }
1291
1292    /// Whether `thread` is inside [`with`](Self::with) right now.
1293    fn held_by(&self, thread: std::thread::ThreadId) -> bool {
1294        *self
1295            .holder
1296            .lock()
1297            .unwrap_or_else(|poisoned| poisoned.into_inner())
1298            == Some(thread)
1299    }
1300
1301    /// [`Progress::add_task`], then redraw (upstream's `add_task` refreshes).
1302    pub fn add_task(
1303        &self,
1304        description: impl Into<String>,
1305        total: impl Into<Option<f64>>,
1306        completed: f64,
1307    ) -> TaskId {
1308        let id = self.with(|progress| progress.add_task(description, total, completed));
1309        self.refresh();
1310        id
1311    }
1312
1313    /// [`Progress::reset`], then redraw (upstream's `reset` refreshes).
1314    pub fn reset(&self, id: TaskId, start: bool, total: Option<f64>, completed: f64) {
1315        self.with(|progress| progress.reset(id, start, total, completed));
1316        self.refresh();
1317    }
1318
1319    /// [`Progress::advance`].
1320    pub fn advance(&self, id: TaskId, amount: f64) {
1321        self.with(|progress| progress.advance(id, amount));
1322    }
1323
1324    /// [`Progress::update`]; redraws when the update asks to
1325    /// ([`TaskUpdate::refresh`]).
1326    pub fn update(&self, id: TaskId, update: TaskUpdate) {
1327        let refresh = update.refresh;
1328        self.with(|progress| progress.update(id, update));
1329        if refresh {
1330            self.refresh();
1331        }
1332    }
1333
1334    /// Redraw now rather than at the next tick, returning once the frame is
1335    /// written. Port of `Progress.refresh`.
1336    ///
1337    /// Called from inside [`with`](Self::with), the redraw is queued instead:
1338    /// the refresh thread needs the lock this thread holds to render, so
1339    /// waiting for it would deadlock. The frame is drawn when `with` returns.
1340    pub fn refresh(&self) {
1341        if let Some(live) = &self.live {
1342            if self.held_by(std::thread::current().id()) {
1343                live.refresh();
1344            } else {
1345                live.refresh_wait();
1346            }
1347        }
1348    }
1349
1350    /// Print `renderable` above the display. Port of `Progress.print`
1351    /// (`console.print` through the live display's render hook). The print
1352    /// is queued on the refresh thread, so it is safe inside
1353    /// [`with`](Self::with).
1354    pub fn print(&self, renderable: Box<dyn Renderable + Send>) {
1355        if self.live.is_some() {
1356            self.handle.print(renderable);
1357        } else {
1358            self.print_disabled(|console| console.print(renderable.as_ref()));
1359        }
1360    }
1361
1362    /// Print straight to a disabled display's sink: no live hook is pushed.
1363    fn print_disabled(&self, print: impl FnOnce(&Console)) {
1364        if let Some(disabled) = &self.disabled {
1365            let mut guard = disabled
1366                .lock()
1367                .unwrap_or_else(|poisoned| poisoned.into_inner());
1368            let (console, writer) = &mut *guard;
1369            let output = console.capture(print);
1370            let _ = writer.write_all(output.as_bytes());
1371        }
1372    }
1373
1374    /// [`print`](Self::print) a string as console markup.
1375    pub fn print_str(&self, content: impl Into<String>) {
1376        if self.live.is_some() {
1377            self.handle.print_str(content);
1378        } else {
1379            let content = content.into();
1380            self.print_disabled(|console| console.print_str(&content));
1381        }
1382    }
1383
1384    /// Log `objects` above the display with the time and the caller. Port
1385    /// of `Progress.log`, which upstream binds to `console.log`: the log goes
1386    /// through the live display's render hook, so it lands above the tasks.
1387    /// The caller is found with `#[track_caller]` (DIVERGENCES §47); a
1388    /// disabled display logs straight to its sink.
1389    #[track_caller]
1390    pub fn log(
1391        &self,
1392        objects: &[crate::console::LogObject<'_>],
1393        options: &crate::console::LogOptions<'_>,
1394    ) -> crate::errors::Result<()> {
1395        let caller = std::panic::Location::caller();
1396        self.log_at(objects, options, caller.file(), caller.line())
1397    }
1398
1399    /// [`log`](Self::log) with the caller's `file` and `line` given
1400    /// ([`Console::log_at`]).
1401    pub fn log_at(
1402        &self,
1403        objects: &[crate::console::LogObject<'_>],
1404        options: &crate::console::LogOptions<'_>,
1405        file: &str,
1406        line: u32,
1407    ) -> crate::errors::Result<()> {
1408        if self.live.is_some() {
1409            return self.handle.log_at(objects, options, file, line);
1410        }
1411        let mut result = Ok(());
1412        self.print_disabled(|console| result = console.log_at(objects, options, file, line));
1413        result
1414    }
1415
1416    /// A writer standing in for stdout while the display runs: whole lines
1417    /// print above it (upstream's `redirect_stdout` `FileProxy`). A disabled
1418    /// display writes straight to stdout.
1419    pub fn stdout(
1420        &self,
1421    ) -> crate::file_proxy::FileProxy<crate::live::LiveHandle<W>, std::io::Stdout> {
1422        self.handle
1423            .file_proxy(crate::file_proxy::ProxyStream::Stdout, std::io::stdout())
1424    }
1425
1426    /// [`stdout`](Self::stdout) for stderr.
1427    pub fn stderr(
1428        &self,
1429    ) -> crate::file_proxy::FileProxy<crate::live::LiveHandle<W>, std::io::Stderr> {
1430        self.handle
1431            .file_proxy(crate::file_proxy::ProxyStream::Stderr, std::io::stderr())
1432    }
1433
1434    /// A proxy for `stream` over any `file`, which receives the writes when
1435    /// the display is not redirecting.
1436    pub fn file_proxy<F: std::io::Write>(
1437        &self,
1438        stream: crate::file_proxy::ProxyStream,
1439        file: F,
1440    ) -> crate::file_proxy::FileProxy<crate::live::LiveHandle<W>, F> {
1441        self.handle.file_proxy(stream, file)
1442    }
1443
1444    /// Iterate `iter`, advancing a new task by one after each item is
1445    /// processed. Port of `Progress.track`: the total defaults to the
1446    /// iterator's exact length, else the task is indeterminate.
1447    pub fn track<I: IntoIterator>(
1448        &self,
1449        iter: I,
1450        total: Option<f64>,
1451        description: impl Into<String>,
1452    ) -> Track<'_, I::IntoIter, W> {
1453        self.track_with(iter, total, 0.0, None, description, 0.1)
1454    }
1455
1456    /// [`track`](Self::track) with every argument of upstream's
1457    /// `Progress.track`: `completed` to start from, an existing `task` (its
1458    /// total and completed are set) and the `update_period` in seconds.
1459    ///
1460    /// As upstream's `_TrackThread`, a background thread advances the task
1461    /// by the items counted every `update_period`, and when the iterator is
1462    /// exhausted or dropped the task's completed is set to the count and the
1463    /// display refreshed.
1464    pub fn track_with<I: IntoIterator>(
1465        &self,
1466        iter: I,
1467        total: Option<f64>,
1468        completed: f64,
1469        task: Option<TaskId>,
1470        description: impl Into<String>,
1471        update_period: f64,
1472    ) -> Track<'_, I::IntoIter, W> {
1473        let iter = iter.into_iter();
1474        let total = total.or_else(|| exact_length(&iter));
1475        let task = match task {
1476            Some(task) => {
1477                let mut update = TaskUpdate::default().completed(completed);
1478                if let Some(total) = total {
1479                    update = update.total(total);
1480                }
1481                self.update(task, update);
1482                task
1483            }
1484            None => self.add_task(description, total, completed),
1485        };
1486        Track {
1487            iter,
1488            progress: self,
1489            state: TrackState::start(self.progress.clone(), task, update_period),
1490        }
1491    }
1492
1493    /// Track reading from `reader`: each read advances the task by the bytes
1494    /// read. Port of `Progress.wrap_file`: `total` is the byte count, or else
1495    /// the total of `task`; a new task named `description` is added when
1496    /// `task` is `None`, otherwise `task`'s total is set.
1497    pub fn wrap_read<R: std::io::Read>(
1498        &self,
1499        reader: R,
1500        total: Option<u64>,
1501        task: Option<TaskId>,
1502        description: impl Into<String>,
1503    ) -> std::io::Result<ProgressReader<'_, R, W>> {
1504        let total = total.map(|total| total as f64).or_else(|| {
1505            task.and_then(|task| self.with(|progress| progress.task(task).and_then(Task::total)))
1506        });
1507        let Some(total) = total else {
1508            return Err(std::io::Error::new(
1509                std::io::ErrorKind::InvalidInput,
1510                "unable to get the total number of bytes, please specify 'total'",
1511            ));
1512        };
1513        let task = self.task_for(task, total, description);
1514        Ok(ProgressReader {
1515            reader,
1516            progress: self,
1517            task,
1518        })
1519    }
1520
1521    /// Open `path` for reading and track it. Port of `Progress.open` in
1522    /// binary mode: `total` defaults to the file's size.
1523    pub fn open(
1524        &self,
1525        path: impl AsRef<std::path::Path>,
1526        total: Option<u64>,
1527        task: Option<TaskId>,
1528        description: impl Into<String>,
1529    ) -> std::io::Result<ProgressReader<'_, std::fs::File, W>> {
1530        let file = std::fs::File::open(path)?;
1531        let total = match total {
1532            Some(total) => total,
1533            None => file.metadata()?.len(),
1534        };
1535        let task = self.task_for(task, total as f64, description);
1536        Ok(ProgressReader {
1537            reader: file,
1538            progress: self,
1539            task,
1540        })
1541    }
1542
1543    /// A new task with `total`, or `task` with its total set to it.
1544    fn task_for(&self, task: Option<TaskId>, total: f64, description: impl Into<String>) -> TaskId {
1545        match task {
1546            Some(task) => {
1547                self.update(task, TaskUpdate::default().total(total));
1548                task
1549            }
1550            None => self.add_task(description, total, 0.0),
1551        }
1552    }
1553
1554    /// Commit the final frame, stop the refresh thread, and return the
1555    /// progress and the output sink. Port of `Progress.stop`.
1556    pub fn stop(mut self) -> (Progress, W) {
1557        let writer = match self.live.take() {
1558            Some(live) => {
1559                let mut writer = live.stop();
1560                // `Progress.stop`: `console.print()` when not interactive.
1561                if !self.interactive {
1562                    let _ = writer.write_all(b"\n");
1563                }
1564                writer
1565            }
1566            None => {
1567                self.disabled
1568                    .take()
1569                    .expect("a disabled display keeps its writer")
1570                    .into_inner()
1571                    .unwrap_or_else(|poisoned| poisoned.into_inner())
1572                    .1
1573            }
1574        };
1575        let progress = match Arc::try_unwrap(std::mem::replace(
1576            &mut self.progress,
1577            Arc::new(std::sync::Mutex::new(Progress::new())),
1578        )) {
1579            Ok(mutex) => mutex
1580                .into_inner()
1581                .unwrap_or_else(|poisoned| poisoned.into_inner()),
1582            // The refresh thread has exited, so this is the only owner left.
1583            Err(_) => unreachable!("progress still shared after the live display stopped"),
1584        };
1585        (progress, writer)
1586    }
1587}
1588
1589/// A reader that advances a task by the bytes read through it. Returned by
1590/// [`LiveProgress::wrap_read`] and [`LiveProgress::open`] (upstream `_Reader`).
1591pub struct ProgressReader<'a, R, W: std::io::Write + Send + 'static> {
1592    reader: R,
1593    progress: &'a LiveProgress<W>,
1594    task: TaskId,
1595}
1596
1597impl<R, W: std::io::Write + Send + 'static> ProgressReader<'_, R, W> {
1598    /// The task this reader advances.
1599    pub fn task(&self) -> TaskId {
1600        self.task
1601    }
1602
1603    /// The wrapped reader.
1604    pub fn into_inner(self) -> R {
1605        self.reader
1606    }
1607}
1608
1609impl<R: std::io::Read, W: std::io::Write + Send + 'static> std::io::Read
1610    for ProgressReader<'_, R, W>
1611{
1612    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1613        let count = self.reader.read(buf)?;
1614        self.progress.advance(self.task, count as f64);
1615        Ok(count)
1616    }
1617}
1618
1619impl<R: std::io::BufRead, W: std::io::Write + Send + 'static> std::io::BufRead
1620    for ProgressReader<'_, R, W>
1621{
1622    fn fill_buf(&mut self) -> std::io::Result<&[u8]> {
1623        self.reader.fill_buf()
1624    }
1625
1626    fn consume(&mut self, amount: usize) {
1627        self.reader.consume(amount);
1628        self.progress.advance(self.task, amount as f64);
1629    }
1630}
1631
1632/// Upstream's `float(length_hint(sequence)) or None`: an exact, non-zero
1633/// length.
1634fn exact_length<I: Iterator>(iter: &I) -> Option<f64> {
1635    match iter.size_hint() {
1636        (lower, Some(upper)) if lower == upper && lower > 0 => Some(lower as f64),
1637        _ => None,
1638    }
1639}
1640
1641/// Upstream's `_TrackThread`: counts the items a tracked iterator hands out
1642/// and advances the task by them every `update_period` on a thread.
1643struct TrackState {
1644    task: TaskId,
1645    /// Items counted so far (upstream's `track_thread.completed`).
1646    completed: Arc<std::sync::atomic::AtomicU64>,
1647    /// Whether an item has been handed out but not yet counted: upstream
1648    /// counts it after the loop body, when the next item is requested.
1649    pending: bool,
1650    stop: Option<std::sync::mpsc::Sender<()>>,
1651    thread: Option<std::thread::JoinHandle<()>>,
1652}
1653
1654impl TrackState {
1655    fn start(progress: Arc<std::sync::Mutex<Progress>>, task: TaskId, update_period: f64) -> Self {
1656        use std::sync::atomic::Ordering;
1657        let completed = Arc::new(std::sync::atomic::AtomicU64::new(0));
1658        let (stop, stopped) = std::sync::mpsc::channel::<()>();
1659        let count = completed.clone();
1660        let period = std::time::Duration::from_secs_f64(update_period.max(0.0));
1661        let thread = std::thread::spawn(move || {
1662            let mut last = 0;
1663            // `while not wait(update_period)`: until told to stop.
1664            while let Err(std::sync::mpsc::RecvTimeoutError::Timeout) = stopped.recv_timeout(period)
1665            {
1666                let now = count.load(Ordering::SeqCst);
1667                if now != last {
1668                    progress
1669                        .lock()
1670                        .unwrap_or_else(|poisoned| poisoned.into_inner())
1671                        .advance(task, (now - last) as f64);
1672                    last = now;
1673                }
1674            }
1675        });
1676        TrackState {
1677            task,
1678            completed,
1679            pending: false,
1680            stop: Some(stop),
1681            thread: Some(thread),
1682        }
1683    }
1684
1685    /// The next item is being requested: count the one before it.
1686    fn next_requested(&mut self) {
1687        if std::mem::take(&mut self.pending) {
1688            self.completed
1689                .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1690        }
1691    }
1692
1693    /// Upstream's `_TrackThread.__exit__`: stop the thread, then set the
1694    /// task's completed to the count and refresh.
1695    fn finish<W: std::io::Write + Send + 'static>(&mut self, live: &LiveProgress<W>) {
1696        let Some(thread) = self.thread.take() else {
1697            return;
1698        };
1699        self.next_requested();
1700        drop(self.stop.take());
1701        let _ = thread.join();
1702        let completed = self.completed.load(std::sync::atomic::Ordering::SeqCst);
1703        live.update(
1704            self.task,
1705            TaskUpdate::default()
1706                .completed(completed as f64)
1707                .refresh(true),
1708        );
1709    }
1710}
1711
1712/// The iterator [`LiveProgress::track`] returns.
1713pub struct Track<'a, I: Iterator, W: std::io::Write + Send + 'static> {
1714    iter: I,
1715    progress: &'a LiveProgress<W>,
1716    state: TrackState,
1717}
1718
1719impl<I: Iterator, W: std::io::Write + Send + 'static> Track<'_, I, W> {
1720    /// The task this iterator advances.
1721    pub fn task(&self) -> TaskId {
1722        self.state.task
1723    }
1724}
1725
1726impl<I: Iterator, W: std::io::Write + Send + 'static> Iterator for Track<'_, I, W> {
1727    type Item = I::Item;
1728
1729    fn next(&mut self) -> Option<I::Item> {
1730        self.state.next_requested();
1731        let item = self.iter.next();
1732        if item.is_some() {
1733            self.state.pending = true;
1734        } else {
1735            self.state.finish(self.progress);
1736        }
1737        item
1738    }
1739
1740    fn size_hint(&self) -> (usize, Option<usize>) {
1741        self.iter.size_hint()
1742    }
1743}
1744
1745impl<I: Iterator, W: std::io::Write + Send + 'static> Drop for Track<'_, I, W> {
1746    /// Upstream's generator closes its `with _TrackThread` block when the
1747    /// loop ends early, too.
1748    fn drop(&mut self) {
1749        // The item handed out last was not processed to the end.
1750        self.state.pending = false;
1751        self.state.finish(self.progress);
1752    }
1753}
1754
1755/// Track progress over `iter` with a live display on stdout. Port of the
1756/// module-level `rich.progress.track`: the description, bar, progress and
1757/// time-remaining columns, refreshed ten times a second, stopped when the
1758/// iterator is exhausted or dropped.
1759pub fn track<I: IntoIterator>(iter: I, description: &str) -> TrackStdout<I::IntoIter> {
1760    let mut columns = Vec::new();
1761    if !description.is_empty() {
1762        columns.push(ProgressColumn::Description);
1763    }
1764    columns.extend([
1765        ProgressColumn::Bar,
1766        ProgressColumn::TaskProgress { show_speed: true },
1767        ProgressColumn::TimeRemaining(TimeRemainingColumn::new(false, true)),
1768    ]);
1769    let iter = iter.into_iter();
1770    let total = exact_length(&iter);
1771    let live = Progress::new()
1772        .columns(columns)
1773        .start(Console::new(), std::io::stdout(), 10.0);
1774    let task = live.add_task(description, total, 0.0);
1775    TrackStdout {
1776        iter,
1777        state: TrackState::start(live.progress.clone(), task, 0.1),
1778        live: Some(live),
1779    }
1780}
1781
1782/// The iterator [`track`] returns; it owns its live display.
1783pub struct TrackStdout<I: Iterator> {
1784    iter: I,
1785    live: Option<LiveProgress<std::io::Stdout>>,
1786    state: TrackState,
1787}
1788
1789impl<I: Iterator> Iterator for TrackStdout<I> {
1790    type Item = I::Item;
1791
1792    fn next(&mut self) -> Option<I::Item> {
1793        self.live.as_ref()?;
1794        self.state.next_requested();
1795        match self.iter.next() {
1796            Some(item) => {
1797                self.state.pending = true;
1798                Some(item)
1799            }
1800            None => {
1801                if let Some(live) = self.live.take() {
1802                    self.state.finish(&live);
1803                    live.stop();
1804                }
1805                None
1806            }
1807        }
1808    }
1809}
1810
1811impl<I: Iterator> Drop for TrackStdout<I> {
1812    fn drop(&mut self) {
1813        if let Some(live) = self.live.take() {
1814            self.state.pending = false;
1815            self.state.finish(&live);
1816            live.stop();
1817        }
1818    }
1819}
1820
1821/// A whole, exactly representable count as an int, else the float.
1822fn whole_number(value: f64) -> FormatValue {
1823    if value.fract() == 0.0 && value.abs() < 9_007_199_254_740_992.0 {
1824        FormatValue::Int(value as i64)
1825    } else {
1826        FormatValue::Float(value)
1827    }
1828}
1829
1830#[cfg(test)]
1831mod tests {
1832    use super::*;
1833    use crate::color::ColorSystem;
1834
1835    fn render(progress: &Progress) -> String {
1836        Console::builder()
1837            .force_terminal(true)
1838            .color_system(Some(ColorSystem::Truecolor))
1839            .width(50)
1840            .no_color(false)
1841            .build()
1842            .render_to_string(progress)
1843    }
1844
1845    #[test]
1846    fn three_tasks_match_upstream() {
1847        // Captured from real rich 15.0.0 (default columns, width 50).
1848        let mut progress = Progress::new().columns(vec![
1849            ProgressColumn::Description,
1850            ProgressColumn::Bar,
1851            ProgressColumn::Percentage,
1852        ]);
1853        progress.add_task("Downloading", 100.0, 50.0);
1854        progress.add_task("Processing", 100.0, 100.0);
1855        progress.add_task("Waiting", 100.0, 0.0);
1856        let expected = concat!(
1857            "Downloading \x1b[38;2;249;38;114m━━━━━━━━━━━━━━━━\x1b[0m",
1858            "\x1b[38;2;249;38;114m╸\x1b[0m\x1b[38;5;237m━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 50%\x1b[0m\n",
1859            "Processing  \x1b[38;2;114;156;31m",
1860            "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m100%\x1b[0m\n",
1861            "Waiting     \x1b[38;5;237m",
1862            "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m  0%\x1b[0m",
1863        );
1864        assert_eq!(render(&progress), expected);
1865    }
1866
1867    #[test]
1868    fn download_text_matches_upstream() {
1869        // Captured from real rich 15.0.0 DownloadColumn.render (decimal units).
1870        let dl = |completed: f64, total: f64| {
1871            let mut progress = Progress::new();
1872            let id = progress.add_task("", total, completed);
1873            progress.task(id).unwrap().download_text(false)
1874        };
1875        assert_eq!(dl(500.0, 1000.0), "0.5/1.0 kB");
1876        assert_eq!(dl(500.0, 999.0), "500/999 bytes");
1877        assert_eq!(dl(1_500_000.0, 3_000_000.0), "1.5/3.0 MB");
1878        assert_eq!(dl(0.0, 1024.0), "0.0/1.0 kB");
1879        assert_eq!(dl(2_500_000_000.0, 10_000_000_000.0), "2.5/10.0 GB");
1880        assert_eq!(dl(250.0, 250.0), "250/250 bytes");
1881    }
1882
1883    #[test]
1884    fn download_column_in_grid_matches_upstream() {
1885        // Captured from real rich 15.0.0: description + bar + download at width 50.
1886        let mut progress = Progress::new().columns(vec![
1887            ProgressColumn::Description,
1888            ProgressColumn::Bar,
1889            ProgressColumn::Download,
1890        ]);
1891        progress.add_task("File", 1000.0, 500.0);
1892        let expected = concat!(
1893            "File \x1b[38;2;249;38;114m━━━━━━━━━━━━━━━━━\x1b[0m\x1b[38;5;237m╺\x1b[0m",
1894            "\x1b[38;5;237m━━━━━━━━━━━━━━━━\x1b[0m \x1b[32m0.5/1.0 kB\x1b[0m",
1895        );
1896        assert_eq!(render(&progress), expected);
1897    }
1898
1899    #[test]
1900    fn custom_columns_with_mofn_match_upstream() {
1901        // Captured from real rich 15.0.0: description + bar + M-of-N (differing
1902        // M-of-N widths → the narrower cell left-justifies with green padding).
1903        let mut progress = Progress::new().columns(vec![
1904            ProgressColumn::Description,
1905            ProgressColumn::Bar,
1906            ProgressColumn::MofN,
1907        ]);
1908        progress.add_task("A", 5.0, 3.0);
1909        progress.add_task("B", 100.0, 50.0);
1910        let console = Console::builder()
1911            .force_terminal(true)
1912            .color_system(Some(ColorSystem::Truecolor))
1913            .width(40)
1914            .no_color(false)
1915            .build();
1916        let expected = concat!(
1917            "A \x1b[38;2;249;38;114m━━━━━━━━━━━━━━━━━━\x1b[0m\x1b[38;5;237m╺\x1b[0m",
1918            "\x1b[38;5;237m━━━━━━━━━━━\x1b[0m \x1b[32m3/5    \x1b[0m\n",
1919            "B \x1b[38;2;249;38;114m━━━━━━━━━━━━━━━\x1b[0m\x1b[38;5;237m╺\x1b[0m",
1920            "\x1b[38;5;237m━━━━━━━━━━━━━━\x1b[0m \x1b[32m 50/100\x1b[0m",
1921        );
1922        assert_eq!(console.render_to_string(&progress), expected);
1923    }
1924
1925    fn live(columns: Vec<ProgressColumn>) -> LiveProgress<Vec<u8>> {
1926        let console = Console::builder()
1927            .force_terminal(true)
1928            .color_system(Some(ColorSystem::Truecolor))
1929            .width(40)
1930            .build();
1931        Progress::new()
1932            .columns(columns)
1933            .clock(|| 0.0)
1934            .start(console, Vec::new(), 1e-9)
1935    }
1936
1937    /// Run `f` on its own thread and fail (rather than hang the suite) if it
1938    /// has not finished within a few seconds.
1939    fn within_deadline<R: Send + 'static>(f: impl FnOnce() -> R + Send + 'static) -> R {
1940        let (done, wait) = std::sync::mpsc::channel();
1941        std::thread::spawn(move || {
1942            let _ = done.send(std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)));
1943        });
1944        match wait.recv_timeout(std::time::Duration::from_secs(10)) {
1945            Ok(Ok(value)) => value,
1946            Ok(Err(payload)) => std::panic::resume_unwind(payload),
1947            Err(_) => panic!("deadlocked: did not finish within 10s"),
1948        }
1949    }
1950
1951    #[test]
1952    fn refresh_inside_with_does_not_deadlock() {
1953        // Upstream's `Progress` lock is an `RLock` and `refresh()` renders in
1954        // the caller's thread, so refreshing while holding the lock is fine.
1955        let output = within_deadline(|| {
1956            let live = live(vec![ProgressColumn::Description, ProgressColumn::MofN]);
1957            live.with(|progress| {
1958                let task = progress.add_task("inside", Some(2.0), 1.0);
1959                live.refresh();
1960                task
1961            });
1962            live.refresh();
1963            String::from_utf8(live.stop().1).unwrap()
1964        });
1965        assert!(
1966            output.ends_with("inside \x1b[32m1/2\x1b[0m\n\x1b[?25h"),
1967            "{output:?}"
1968        );
1969    }
1970
1971    #[test]
1972    fn nested_with_panics_instead_of_deadlocking() {
1973        let result = within_deadline(|| {
1974            let live = live(vec![ProgressColumn::Description]);
1975            let nested = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1976                live.with(|_| live.add_task("nested", None, 0.0))
1977            }));
1978            // The display is still usable after the refused re-entry.
1979            let task = live.add_task("after", None, 0.0);
1980            live.stop();
1981            (nested.is_err(), task)
1982        });
1983        assert!(result.0, "a nested `with` must be refused, not deadlock");
1984    }
1985
1986    #[test]
1987    fn the_track_thread_advances_by_the_items_counted() {
1988        let live = live(vec![ProgressColumn::Description, ProgressColumn::MofN]);
1989        let tracked = live.track_with(0..4, None, 0.0, None, "n", 0.001);
1990        let task = tracked.task();
1991        let mut seen = 0.0;
1992        for item in tracked {
1993            if item == 2 {
1994                // Items 0 and 1 are counted; wait for the thread to see them.
1995                let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
1996                while seen < 2.0 && std::time::Instant::now() < deadline {
1997                    std::thread::sleep(std::time::Duration::from_millis(1));
1998                    seen = live.with(|progress| progress.task(task).unwrap().completed());
1999                }
2000            }
2001        }
2002        assert_eq!(seen, 2.0);
2003        let (progress, _) = live.stop();
2004        assert_eq!(progress.task(task).unwrap().completed(), 4.0);
2005    }
2006
2007    #[test]
2008    fn breaking_out_of_track_counts_only_finished_items() {
2009        let live = live(vec![ProgressColumn::Description, ProgressColumn::MofN]);
2010        let tracked = live.track_with(0..10, None, 0.0, None, "n", 1e9);
2011        let task = tracked.task();
2012        for item in tracked {
2013            if item == 3 {
2014                break;
2015            }
2016        }
2017        let (progress, _) = live.stop();
2018        assert_eq!(progress.task(task).unwrap().completed(), 3.0);
2019    }
2020
2021    #[test]
2022    fn track_counts_each_item_after_its_loop_body() {
2023        let live = live(vec![ProgressColumn::Description, ProgressColumn::MofN]);
2024        let mut seen = Vec::new();
2025        // An update period that never elapses: upstream's `_TrackThread`
2026        // advances only every period, then sets the count on exit.
2027        let tracked = live.track_with(vec!['a', 'b', 'c'], None, 0.0, None, "letters", 1e9);
2028        let task = tracked.task();
2029        for item in tracked {
2030            let completed = live.with(|progress| progress.task(task).unwrap().completed());
2031            seen.push((item, completed));
2032        }
2033        assert_eq!(seen, vec![('a', 0.0), ('b', 0.0), ('c', 0.0)]);
2034        let (progress, bytes) = live.stop();
2035        let task = progress.task(task).unwrap();
2036        assert_eq!((task.total(), task.completed()), (Some(3.0), 3.0));
2037        let output = String::from_utf8(bytes).unwrap();
2038        assert!(
2039            output.ends_with("letters \x1b[32m3/3\x1b[0m\n\x1b[?25h"),
2040            "{output:?}"
2041        );
2042    }
2043
2044    #[test]
2045    fn track_leaves_an_iterator_of_unknown_length_indeterminate() {
2046        let live = live(vec![ProgressColumn::MofN]);
2047        let task = {
2048            let mut tracked = live.track((0..10).filter(|n| n % 3 == 0), None, "");
2049            let task = tracked.task();
2050            assert_eq!(tracked.by_ref().count(), 4);
2051            task
2052        };
2053        let with_total = {
2054            let mut tracked = live.track(0..2, Some(5.0), "");
2055            tracked.by_ref().for_each(drop);
2056            tracked.task()
2057        };
2058        let (progress, _) = live.stop();
2059        assert_eq!(progress.task(task).unwrap().total(), None);
2060        assert_eq!(progress.task(task).unwrap().completed(), 4.0);
2061        assert_eq!(progress.task(with_total).unwrap().total(), Some(5.0));
2062    }
2063
2064    fn quiet_console() -> Console {
2065        Console::builder().force_terminal(false).width(40).build()
2066    }
2067
2068    #[test]
2069    fn wrap_read_advances_by_the_bytes_read() {
2070        use std::io::Read;
2071        let live = Progress::new()
2072            .disable(true)
2073            .start(quiet_console(), Vec::new(), 1.0);
2074        let mut reader = live
2075            .wrap_read(&b"hello world"[..], Some(11), None, "Reading...")
2076            .expect("total given");
2077        let task = reader.task();
2078        let mut buf = [0u8; 4];
2079        reader.read_exact(&mut buf).unwrap();
2080        assert_eq!(live.with(|p| p.task(task).unwrap().completed()), 4.0);
2081        let mut rest = Vec::new();
2082        reader.read_to_end(&mut rest).unwrap();
2083        assert!(live.with(|p| p.task(task).unwrap().finished()));
2084        let (_, out) = live.stop();
2085        assert!(out.is_empty(), "a disabled display writes nothing");
2086    }
2087
2088    #[test]
2089    fn wrap_read_needs_a_total() {
2090        let live = Progress::new()
2091            .disable(true)
2092            .start(quiet_console(), Vec::new(), 1.0);
2093        let err = live
2094            .wrap_read(&b""[..], None, None, "x")
2095            .err()
2096            .expect("no total");
2097        assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
2098        let task = live.add_task("sized", 5.0, 0.0);
2099        assert!(live.wrap_read(&b""[..], None, Some(task), "x").is_ok());
2100    }
2101
2102    #[test]
2103    fn open_takes_the_file_size_as_total() {
2104        use std::io::Read;
2105        let path = std::env::temp_dir().join(format!("rs-rich-open-{}", std::process::id()));
2106        std::fs::write(&path, b"0123456789").unwrap();
2107        let live = Progress::new()
2108            .disable(true)
2109            .start(quiet_console(), Vec::new(), 1.0);
2110        let mut reader = live.open(&path, None, None, "Reading...").unwrap();
2111        let task = reader.task();
2112        assert_eq!(live.with(|p| p.task(task).unwrap().total()), Some(10.0));
2113        std::io::copy(&mut reader, &mut std::io::sink()).unwrap();
2114        assert_eq!(live.with(|p| p.task(task).unwrap().completed()), 10.0);
2115        let _ = reader.read(&mut [0u8; 1]);
2116        drop(reader);
2117        std::fs::remove_file(path).unwrap();
2118    }
2119}