Skip to main content

tycho_util/
progress_bar.rs

1pub struct ProgressBar {
2    percentage_step: u64,
3    current: u64,
4    total: Option<u64>,
5    exact_unit: Option<&'static str>,
6    mapper: Box<MapperFn>,
7    printer: Box<PrinterFn>,
8}
9
10type MapperFn = dyn Fn(u64) -> String + Send + 'static;
11type PrinterFn = dyn Fn(&dyn std::fmt::Display) + Send + 'static;
12
13impl ProgressBar {
14    pub fn builder() -> ProgressBarBuilder {
15        ProgressBarBuilder {
16            percentage_step: PERCENTAGE_STEP,
17            total: None,
18            exact_unit: None,
19            mapper: None,
20        }
21    }
22
23    pub fn set_total(&mut self, total: impl Into<u64>) {
24        self.total = Some(total.into());
25    }
26
27    pub fn add_progress(&mut self, value: impl Into<u64>) {
28        self.set_progress(self.current + value.into());
29    }
30
31    pub fn set_progress(&mut self, current: impl Into<u64>) {
32        let old = self.compute_current_progress();
33        self.current = current.into();
34        let new = self.compute_current_progress();
35
36        if matches!(
37            (old, new),
38            (Some(old), Some(new)) if old / self.percentage_step != new / self.percentage_step
39        ) {
40            self.progress_message();
41        }
42    }
43
44    pub fn complete(&self) {
45        self.message("complete");
46    }
47
48    #[inline(always)]
49    fn progress_message(&self) {
50        let total = match self.total {
51            Some(total) if total > 0 => total,
52            _ => return,
53        };
54
55        let percent = self.current * 100 / total;
56        let current = (self.mapper)(self.current);
57        let total = (self.mapper)(total);
58
59        match self.exact_unit {
60            Some(exact_unit) => self.message(format_args!(
61                "{percent}% ({current} / {total} {exact_unit})",
62            )),
63            None => self.message(format_args!("{percent}%")),
64        }
65    }
66
67    #[inline(always)]
68    fn message(&self, text: impl std::fmt::Display) {
69        (self.printer)(&text);
70    }
71
72    fn compute_current_progress(&self) -> Option<u64> {
73        self.total
74            .filter(|&total| total > 0)
75            .map(|total| self.current * 100u64 / total)
76    }
77}
78
79pub struct ProgressBarBuilder {
80    percentage_step: u64,
81    total: Option<u64>,
82    exact_unit: Option<&'static str>,
83    mapper: Option<Box<dyn Fn(u64) -> String + Send + 'static>>,
84}
85
86impl ProgressBarBuilder {
87    pub fn with_mapper<F>(mut self, mapper: F) -> Self
88    where
89        F: Fn(u64) -> String + Send + 'static,
90    {
91        self.mapper = Some(Box::new(mapper));
92        self
93    }
94
95    pub fn percentage_step(mut self, step: u64) -> Self {
96        self.percentage_step = std::cmp::max(step, 1);
97        self
98    }
99
100    pub fn total(mut self, total: impl Into<u64>) -> Self {
101        self.total = Some(total.into());
102        self
103    }
104
105    pub fn exact_unit(mut self, unit: &'static str) -> Self {
106        self.exact_unit = Some(unit);
107        self
108    }
109
110    pub fn build<F>(self, printer: F) -> ProgressBar
111    where
112        F: Fn(&dyn std::fmt::Display) + Send + 'static,
113    {
114        let pg = ProgressBar {
115            percentage_step: self.percentage_step,
116            current: 0,
117            total: self.total,
118            exact_unit: self.exact_unit,
119            mapper: self.mapper.unwrap_or_else(|| Box::new(|x| x.to_string())),
120            printer: Box::new(printer),
121        };
122
123        if self.total.is_some() {
124            pg.progress_message();
125        } else {
126            pg.message("estimating total");
127        }
128
129        pg
130    }
131}
132
133const PERCENTAGE_STEP: u64 = 5;