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