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