Skip to main content

rich_rs/
progress.rs

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