ml_progress/
state.rs

1use std::{
2    borrow::Cow,
3    thread::JoinHandle,
4    time::{Duration, Instant},
5};
6
7use terminal_size::Width;
8
9use crate::{
10    internal::{FillItem, Item},
11    Error, DEFAULT_DRAW_DELAY, DEFAULT_DRAW_INTERVAL, MIN_ETA_ELAPSED, MIN_SPEED_ELAPSED,
12};
13
14// ======================================================================
15// State - PUBLIC
16
17/// Current state of [`Progress`].
18///
19/// This is used with [custom item] and returned by [`Progress::state`].
20///
21/// See [custom item] for an example.
22///
23/// [custom item]: crate#custom-item
24/// [`Progress`]: crate::Progress
25/// [`Progress::state`]: crate::Progress::state
26pub struct State {
27    pos: u64,
28    total: Option<u64>,
29    percent: Option<f64>,
30    pre_inc: bool,
31    thousands_separator: String,
32    message: Cow<'static, str>,
33
34    start_time: Instant,
35    speed: Option<f64>,
36    eta_instant: Option<Instant>,
37
38    items: Vec<Item>,
39
40    prev_draw: Option<Instant>,
41    next_draw: Option<Instant>,
42    is_finished: bool,
43}
44
45impl State {
46    /// Returns estimated time remaining or `None` if estimate is not available.
47    ///
48    /// Estimate is based on completed steps and time of latest completion.
49    ///
50    /// Estimate is available if
51    /// - [`total`] is `Some` and
52    /// - at least one step and at most [`total`] steps have been completed and
53    /// - at least 100 ms has elapsed since [`Progress`] creation.
54    ///
55    /// See [custom item] for an example.
56    ///
57    /// [custom item]: crate#custom-item
58    /// [`Progress`]: crate::Progress
59    /// [`total`]: State::total
60    pub fn eta(&self) -> Option<Duration> {
61        if self.is_finished {
62            Some(Duration::ZERO)
63        } else if let Some(eta) = self.eta_instant {
64            eta.checked_duration_since(Instant::now())
65        } else {
66            None
67        }
68    }
69
70    /// Returns percentual completion or `None` if [`total`] is `None`.
71    ///
72    /// Returned value can be over 100 if [`position`]
73    /// is incremented beyond [`total`].
74    ///
75    /// # Examples
76    ///
77    /// ```rust
78    /// use ml_progress::progress;
79    ///
80    /// let progress = progress!(10)?;
81    /// progress.inc(6);
82    /// assert_eq!(progress.state().lock().percent(), Some(60.0));
83    /// # Ok::<(), ml_progress::Error>(())
84    /// ```
85    ///
86    /// [`position`]: State::pos
87    /// [`total`]: State::total
88    pub fn percent(&self) -> Option<f64> {
89        self.percent
90    }
91
92    /// Returns position.
93    ///
94    /// # Examples
95    ///
96    /// ```rust
97    /// use ml_progress::progress;
98    ///
99    /// let progress = progress!(10)?;
100    /// progress.inc(6);
101    /// assert_eq!(progress.state().lock().pos(), 6);
102    /// # Ok::<(), ml_progress::Error>(())
103    /// ```
104    pub fn pos(&self) -> u64 {
105        self.pos
106    }
107
108    /// Returns speed in steps per second
109    /// or `None` if speed is not available.
110    ///
111    /// Speed is average from when [`Progress`] was created until latest [`inc`].
112    ///
113    /// Speed is available if
114    /// - at least one step has been completed and
115    /// - at least 100 ms has elapsed since [`Progress`] creation.
116    ///
117    /// [`Progress`]: crate::Progress
118    /// [`inc`]: crate::Progress::inc
119    pub fn speed(&self) -> Option<f64> {
120        self.speed
121    }
122
123    /// Returns thousands separator.
124    ///
125    /// Separator can be set with [`ProgressBuilder::thousands_separator`],
126    /// default is space.
127    ///
128    /// # Examples
129    ///
130    /// ```rust
131    /// use ml_progress::progress_builder;
132    ///
133    /// let progress = progress_builder!().thousands_separator(",").build()?;
134    /// assert_eq!(progress.state().lock().thousands_separator(), ",");
135    /// # Ok::<(), ml_progress::Error>(())
136    /// ```
137    ///
138    /// [`ProgressBuilder::thousands_separator`]: crate::ProgressBuilder::thousands_separator
139    pub fn thousands_separator(&self) -> &str {
140        &self.thousands_separator
141    }
142
143    /// Returns total.
144    ///
145    /// # Examples
146    ///
147    /// ```rust
148    /// use ml_progress::progress;
149    ///
150    /// let progress = progress!(10)?;
151    /// assert_eq!(progress.state().lock().total(), Some(10));
152    /// # Ok::<(), ml_progress::Error>(())
153    /// ```
154    pub fn total(&self) -> Option<u64> {
155        self.total
156    }
157}
158
159// ======================================================================
160// State - CRATE
161
162impl State {
163    pub(crate) fn finish(&mut self, drawer: &JoinHandle<()>) {
164        if !self.is_finished {
165            if let Some(total) = self.total {
166                self.pos = total;
167            } else {
168                self.total = Some(self.pos);
169            }
170            self.percent = Some(100.0);
171            self.eta_instant = None;
172            self.is_finished = true;
173            drawer.thread().unpark();
174
175            self.draw();
176            if terminal_size::terminal_size().is_some() {
177                eprintln!();
178            }
179        }
180    }
181
182    pub(crate) fn finish_and_clear(&mut self, drawer: &JoinHandle<()>) {
183        if !self.is_finished {
184            self.is_finished = true;
185            drawer.thread().unpark();
186
187            if let Some((Width(width), _)) = terminal_size::terminal_size() {
188                let width = width as usize;
189                eprint!("\r{:width$.width$}\r", "");
190            }
191        }
192    }
193
194    pub(crate) fn finish_at_current_pos(&mut self, drawer: &JoinHandle<()>) {
195        if !self.is_finished {
196            self.is_finished = true;
197            drawer.thread().unpark();
198
199            self.draw();
200            if terminal_size::terminal_size().is_some() {
201                eprintln!();
202            }
203        }
204    }
205
206    // Only for `Progress::drop`.
207    //
208    // - Finishes without any additional output.
209    // - Can leave drawn state out-of-sync with internal state.
210    pub(crate) fn finish_quietly(&mut self, drawer: &JoinHandle<()>) {
211        if !self.is_finished {
212            self.is_finished = true;
213            drawer.thread().unpark();
214        }
215    }
216
217    pub(crate) fn is_finished(&self) -> bool {
218        self.is_finished
219    }
220
221    pub(crate) fn inc(&mut self, steps: u64, drawer: &JoinHandle<()>) {
222        let now = Instant::now();
223        let elapsed = now - self.start_time;
224
225        self.pos += steps;
226
227        let completed = if self.pre_inc {
228            self.pos.saturating_sub(1)
229        } else {
230            self.pos
231        };
232
233        if elapsed >= MIN_SPEED_ELAPSED && completed > 0 {
234            self.speed = Some(completed as f64 / elapsed.as_secs_f64());
235        }
236
237        if let Some(total) = self.total {
238            self.percent = Some(completed as f64 / total as f64 * 100.0);
239
240            if completed > total {
241                self.eta_instant = None;
242            } else if elapsed >= MIN_ETA_ELAPSED && completed > 0 {
243                let duration = elapsed.mul_f64(total as f64 / completed as f64);
244                self.eta_instant = Some(self.start_time + duration);
245            }
246        }
247
248        self.queue_draw(now, drawer);
249    }
250
251    pub(crate) fn message(
252        &mut self,
253        message: impl Into<Cow<'static, str>>,
254        drawer: &JoinHandle<()>,
255    ) {
256        self.message = message.into();
257        self.queue_draw(Instant::now(), drawer);
258    }
259
260    pub(crate) fn new(
261        total: Option<u64>,
262        pre_inc: bool,
263        thousands_separator: String,
264        items: Vec<Item>,
265    ) -> Result<Self, Error> {
266        let mut fill_item_count = 0;
267        for item in &items {
268            if let Item::Fill(_) = item {
269                fill_item_count += 1;
270            }
271        }
272
273        if fill_item_count > 1 {
274            Err(Error::MultipleFillItems)
275        } else {
276            let now = Instant::now();
277
278            Ok(Self {
279                pos: 0,
280                total,
281                percent: if total.is_none() { None } else { Some(0.0) },
282                pre_inc,
283                thousands_separator,
284                message: Cow::Borrowed(""),
285
286                start_time: now,
287                speed: None,
288                eta_instant: None,
289
290                items,
291
292                prev_draw: None,
293                next_draw: Some(now + DEFAULT_DRAW_DELAY),
294                is_finished: false,
295            })
296        }
297    }
298
299    // Returns
300    // - `OK(())` - was drawn
301    // - `Err(None)` - not drawn, no draw scheduled
302    // - `Err(Some(..))` - not drawn, draw is scheduled after returned duration
303    pub(crate) fn try_draw(&mut self) -> Result<(), Option<Duration>> {
304        assert!(!self.is_finished);
305
306        if let Some(next_draw) = self.next_draw {
307            let now = Instant::now();
308            if next_draw > now {
309                Err(Some(next_draw - now))
310            } else {
311                self.draw();
312                self.prev_draw = Some(now);
313                self.next_draw = None;
314                Ok(())
315            }
316        } else {
317            Err(None)
318        }
319    }
320}
321
322// ======================================================================
323// State - PRIVATE
324
325impl State {
326    fn draw(&mut self) {
327        if let Some((Width(width), _)) = terminal_size::terminal_size() {
328            let width = width as usize;
329
330            let mut pre_fill = String::with_capacity(width);
331            let mut fill = None;
332            let mut post_fill = String::with_capacity(width);
333
334            for item in &self.items {
335                let active = if fill.is_none() {
336                    &mut pre_fill
337                } else {
338                    &mut post_fill
339                };
340
341                match item {
342                    Item::Fill(item) => fill = Some(item),
343                    Item::Fn(f) => active.push_str(&f(self)),
344                    Item::Literal(s) => active.push_str(s),
345                }
346            }
347
348            let fill_width =
349                width.saturating_sub(pre_fill.chars().count() + post_fill.chars().count());
350
351            let mut line = String::with_capacity(width);
352            line.push_str(&pre_fill);
353            match fill {
354                Some(&FillItem::Bar) => {
355                    if let Some(percent) = self.percent {
356                        let done_width =
357                            ((fill_width as f64 * percent / 100.0) as usize).min(fill_width);
358                        line.push_str(&"#".repeat(done_width));
359                        line.push_str(&"-".repeat(fill_width - done_width));
360                    } else {
361                        line.push_str(&" ".repeat(fill_width));
362                    }
363                }
364
365                Some(FillItem::Message) => {
366                    line.push_str(&format!("{:fill_width$.fill_width$}", self.message))
367                }
368
369                None => (),
370            }
371            line.push_str(&post_fill);
372
373            eprint!("\r{:width$.width$}", line);
374        }
375    }
376
377    fn queue_draw(&mut self, now: Instant, drawer: &JoinHandle<()>) {
378        if !self.is_finished && self.next_draw.is_none() {
379            let mut next_draw = now + DEFAULT_DRAW_DELAY;
380            if let Some(prev_draw) = self.prev_draw {
381                next_draw = next_draw.max(prev_draw + DEFAULT_DRAW_INTERVAL);
382            }
383            self.next_draw = Some(next_draw);
384
385            drawer.thread().unpark();
386        }
387    }
388}