ml_progress/lib.rs
1#![doc = include_str!(concat!(env!("OUT_DIR"), "/README-rustdocified.md"))]
2#![deny(missing_docs)]
3#![forbid(unsafe_code)]
4
5use std::{
6 borrow::Cow,
7 error::Error as StdError,
8 fmt,
9 sync::Arc,
10 thread::{self, JoinHandle},
11 time::Duration,
12};
13
14use parking_lot::Mutex;
15
16pub use crate::state::State;
17
18use crate::internal::Item;
19
20#[allow(missing_docs)]
21pub mod internal;
22mod macros;
23mod state;
24
25// ======================================================================
26// CONST - PRIVATE
27
28const DEFAULT_DRAW_RATE: usize = 20;
29const DEFAULT_DRAW_INTERVAL: Duration =
30 Duration::from_nanos(1_000_000_000 / DEFAULT_DRAW_RATE as u64);
31
32const DEFAULT_DRAW_DELAY: Duration = Duration::from_millis(5);
33
34const MIN_ETA_ELAPSED: Duration = Duration::from_millis(100);
35const MIN_SPEED_ELAPSED: Duration = Duration::from_millis(100);
36
37const BINARY_PREFIXES: &[&str] = &["", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi", "Yi"];
38const DECIMAL_PREFIXES: &[&str] = &["", "k", "M", "G", "T", "P", "E", "Z", "Y"];
39
40// ======================================================================
41// Error - PUBLIC
42
43/// Represents all possible errors that can occur in this library.
44#[derive(Debug, PartialEq)]
45pub enum Error {
46 /// Given items contain multiple `*_fill` items but at most one is allowed.
47 ///
48 /// # Examples
49 ///
50 /// ```rust
51 /// use ml_progress::{progress, Error};
52 ///
53 /// assert_eq!(
54 /// progress!(10; bar_fill message_fill).err(),
55 /// Some(Error::MultipleFillItems)
56 /// );
57 /// ```
58 MultipleFillItems,
59
60 /// Given `total` is out-of-range of `u64`.
61 ///
62 /// # Examples
63 ///
64 /// ```rust
65 /// use ml_progress::{progress, Error};
66 ///
67 /// assert_eq!(progress!(-1).err(), Some(Error::TotalIsOutOfRange));
68 /// ```
69 TotalIsOutOfRange,
70}
71
72// ======================================================================
73// Error - IMPL DISPLAY
74
75impl fmt::Display for Error {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 match self {
78 Error::MultipleFillItems => {
79 write!(f, "got multiple fill items, at most one is allowed")
80 }
81
82 Error::TotalIsOutOfRange => {
83 write!(f, "total is out-of-range of `u64`")
84 }
85 }
86 }
87}
88
89// ======================================================================
90// Error - IMPL ERROR
91
92impl StdError for Error {}
93
94// ======================================================================
95// Progress - PUBLIC
96
97/// Progress indicator.
98///
99/// `Progress` is created either
100/// - directly with [`progress!`] macro (if you don’t need custom configuration) or
101/// - by first creating [`ProgressBuilder`] with [`progress_builder!`] macro,
102/// setting custom options, and then creating `Progress` with [`build`].
103///
104/// `Progress` is drawn
105/// - using background thread to guarantee timely updates
106/// - only if terminal is detected
107/// - to `STDERR` starting with `"\r"`
108/// - from the moment `Progress` is created until `Progress` is finished or dropped
109///
110/// See crate index for [usage](crate#usage) and [examples](crate#examples).
111///
112/// [`build`]: crate::ProgressBuilder::build
113#[derive(Clone)]
114pub struct Progress {
115 // This is `None` only in `Drop::drop`.
116 drawer: Option<Arc<JoinHandle<()>>>,
117 state: Arc<Mutex<State>>,
118}
119
120impl Progress {
121 /// Finishes `Progress` with 100% completion.
122 ///
123 /// - Sets [`State`] of `Progress` to 100% completion.
124 /// - Draws `Progress` once with additional `"\n"`
125 /// to move cursor to next line.
126 /// - Finishes `Progress`, i.e. there will be no further draws.
127 ///
128 /// # Examples
129 ///
130 /// ```rust
131 /// use ml_progress::progress;
132 ///
133 /// eprintln!("Begin");
134 /// let progress = progress!(10)?;
135 /// progress.finish();
136 /// eprintln!("End");
137 /// # Ok::<(), ml_progress::Error>(())
138 /// ```
139 ///
140 /// ```text
141 /// Begin
142 /// ################################################# 10/10 (0s)
143 /// End
144 /// ```
145 pub fn finish(&self) {
146 self.state.lock().finish(self.drawer.as_ref().unwrap());
147 }
148
149 /// Finishes and clears `Progress`.
150 ///
151 /// - Clears drawn `Progress` by overwriting with spaces + `"\r"`,
152 /// leaving cursor at start of the cleared line.
153 /// - Finishes `Progress`, i.e. there will be no further draws.
154 ///
155 /// # Examples
156 ///
157 /// ```rust
158 /// use ml_progress::progress;
159 ///
160 /// eprintln!("Begin");
161 /// let progress = progress!(10)?;
162 /// progress.finish_and_clear();
163 /// eprintln!("End");
164 /// # Ok::<(), ml_progress::Error>(())
165 /// ```
166 ///
167 /// ```text
168 /// Begin
169 /// End
170 /// ```
171 pub fn finish_and_clear(&self) {
172 self.state
173 .lock()
174 .finish_and_clear(self.drawer.as_ref().unwrap());
175 }
176
177 /// Finishes `Progress`.
178 ///
179 /// - Draws `Progress` once with additional `"\n"`
180 /// to move cursor to next line.
181 /// - Finishes `Progress`, i.e. there will be no further draws.
182 ///
183 /// # Examples
184 ///
185 /// ```rust
186 /// use ml_progress::progress;
187 ///
188 /// eprintln!("Begin");
189 /// let progress = progress!(10)?;
190 /// progress.inc(6);
191 /// progress.finish_at_current_pos();
192 /// eprintln!("End");
193 /// # Ok::<(), ml_progress::Error>(())
194 /// ```
195 ///
196 /// ```text
197 /// Begin
198 /// ##############################-------------------- 6/10 (0s)
199 /// End
200 /// ```
201 pub fn finish_at_current_pos(&self) {
202 self.state
203 .lock()
204 .finish_at_current_pos(self.drawer.as_ref().unwrap());
205 }
206
207 /// Increments position of `Progress`.
208 ///
209 /// # Examples
210 ///
211 /// ```rust
212 /// use ml_progress::progress;
213 ///
214 /// let progress = progress!(10)?;
215 /// progress.inc(6);
216 /// progress.finish_at_current_pos();
217 /// # Ok::<(), ml_progress::Error>(())
218 /// ```
219 ///
220 /// ```text
221 /// ##############################-------------------- 6/10 (0s)
222 /// ```
223 pub fn inc(&self, steps: u64) {
224 self.state.lock().inc(steps, self.drawer.as_ref().unwrap());
225 }
226
227 /// Sets the message shown by item `message_fill`.
228 ///
229 /// # Examples
230 ///
231 /// ```rust
232 /// use ml_progress::progress;
233 ///
234 /// let progress = progress!(10; pos "/" total " " message_fill)?;
235 /// progress.inc(6);
236 /// progress.message("Hello, World!");
237 /// progress.finish_at_current_pos();
238 /// # Ok::<(), ml_progress::Error>(())
239 /// ```
240 ///
241 /// ```text
242 /// 6/10 Hello, World!
243 /// ```
244 pub fn message(&self, message: impl Into<Cow<'static, str>>) {
245 self.state
246 .lock()
247 .message(message, self.drawer.as_ref().unwrap());
248 }
249
250 /// Returns current state of `Progress`.
251 ///
252 /// # Examples
253 ///
254 /// ```rust
255 /// use ml_progress::progress;
256 ///
257 /// let progress = progress!(10)?;
258 /// progress.inc(6);
259 /// assert_eq!(progress.state().lock().pos(), 6);
260 /// # Ok::<(), ml_progress::Error>(())
261 /// ```
262 pub fn state(&self) -> &Arc<Mutex<State>> {
263 &self.state
264 }
265}
266
267impl Drop for Progress {
268 fn drop(&mut self) {
269 if let Ok(drawer) = Arc::try_unwrap(self.drawer.take().unwrap()) {
270 let mut state = self.state.lock();
271 if !state.is_finished() {
272 state.finish_quietly(&drawer);
273 }
274 drop(state);
275 let _ = drawer.join();
276 }
277 }
278}
279
280// ======================================================================
281// Progress - CRATE
282
283impl Progress {
284 pub(crate) fn new(state: State) -> Self {
285 let state = Arc::new(Mutex::new(state));
286
287 let drawer = thread::spawn({
288 let state = state.clone();
289 move || loop {
290 let mut state = state.lock();
291
292 if state.is_finished() {
293 break;
294 }
295
296 let timeout = match state.try_draw() {
297 Ok(()) => None,
298 Err(timeout) => timeout,
299 };
300
301 drop(state);
302
303 // NOTE: These may wake spuriously
304 if let Some(timeout) = timeout {
305 thread::park_timeout(timeout);
306 } else {
307 thread::park();
308 }
309 }
310 });
311
312 Self {
313 drawer: Some(Arc::new(drawer)),
314 state,
315 }
316 }
317}
318
319// ======================================================================
320// ProgressBuilder - PUBLIC
321
322/// A builder to create [`Progress`] with custom configuration.
323///
324/// See [custom configuration] for an example.
325///
326/// [custom configuration]: crate#custom-configuration
327pub struct ProgressBuilder {
328 total: Result<Option<u64>, Error>,
329 pre_inc: bool,
330 thousands_separator: String,
331 items: Vec<Item>,
332}
333
334impl ProgressBuilder {
335 /// Creates [`Progress`] using configuration of this `ProgressBuilder`.
336 ///
337 /// See [custom configuration] for an example.
338 ///
339 /// [custom configuration]: crate#custom-configuration
340 pub fn build(self) -> Result<Progress, Error> {
341 let state = State::new(
342 self.total?,
343 self.pre_inc,
344 self.thousands_separator,
345 self.items,
346 )?;
347
348 Ok(Progress::new(state))
349 }
350
351 /// Creates `ProgressBuilder` to configure [`Progress`].
352 ///
353 /// If `items` is empty then default items are used instead.
354 ///
355 /// [`progress_builder!`] macro should be used instead of this,
356 /// which is same as `ProgressBuilder::new(items!(ITEMS))`.
357 pub fn new(items: Vec<Item>) -> Self {
358 let items = if items.is_empty() {
359 // DEFAULT ITEMS
360 items!(bar_fill " " pos "/" total " (" eta ")")
361 } else {
362 items
363 };
364
365 Self {
366 total: Ok(None),
367 pre_inc: false,
368 thousands_separator: " ".to_owned(),
369 items,
370 }
371 }
372
373 /// Sets increment mode to `PreInc`.
374 ///
375 /// Increment mode can be `PostInc` (default) or `PreInc`.
376 ///
377 /// - `PostInc` means that progress position is
378 /// incremented after the associated work.
379 /// - For example incrementing position from 2 to 3 means that work
380 /// of step 3 has been completed and work of step 4 is about to begin.
381 /// - `PreInc` means that progress position is
382 /// incremented before the associated work.
383 /// - For example incrementing position from 2 to 3 means that work
384 /// of step 2 has been completed and work of step 3 is about to begin.
385 ///
386 /// # Examples
387 ///
388 /// Here first step has been completed and second is about to begin
389 /// so completion percentage is 33%.
390 ///
391 /// ```rust
392 /// use ml_progress::progress_builder;
393 ///
394 /// let progress = progress_builder!("[" percent "] " pos "/" total)
395 /// .total(Some(3))
396 /// .pre_inc()
397 /// .build()?;
398 /// progress.inc(1);
399 /// progress.inc(1);
400 /// progress.finish_at_current_pos();
401 /// # Ok::<(), ml_progress::Error>(())
402 /// ```
403 ///
404 /// ```text
405 /// [ 33%] 2/3
406 /// ```
407 pub fn pre_inc(self) -> Self {
408 Self {
409 pre_inc: true,
410 ..self
411 }
412 }
413
414 /// Sets thousands separator, default is space.
415 ///
416 /// See [custom configuration] for an example.
417 ///
418 /// [custom configuration]: crate#custom-configuration
419 pub fn thousands_separator(self, separator: &str) -> Self {
420 Self {
421 thousands_separator: separator.to_owned(),
422 ..self
423 }
424 }
425
426 /// Sets progress total, default is `None`.
427 ///
428 /// See [custom configuration] for an example.
429 ///
430 /// [custom configuration]: crate#custom-configuration
431 pub fn total<T: TryInto<u64>>(self, total: Option<T>) -> Self {
432 let total = if let Some(total) = total {
433 match total.try_into() {
434 Ok(total) => Ok(Some(total)),
435 Err(_) => Err(Error::TotalIsOutOfRange),
436 }
437 } else {
438 Ok(None)
439 };
440
441 Self { total, ..self }
442 }
443}
444
445// ======================================================================
446// FUNCTIONS - PUBLIC
447
448/// Returns given value as binary prefix with corresponding value.
449///
450/// Uses 1024-based prefixes `Ki`, `Mi`, `Gi`, ..., `Yi`.
451///
452/// # Examples
453///
454/// ```rust
455/// assert_eq!(ml_progress::binary_prefix(2048.0), (2.0, "Ki"));
456/// ```
457pub fn binary_prefix(mut value: f64) -> (f64, &'static str) {
458 let mut scale = 0;
459 while value.abs() >= 1024.0 && scale < BINARY_PREFIXES.len() - 1 {
460 value /= 1024.0;
461 scale += 1;
462 }
463 (value, BINARY_PREFIXES[scale])
464}
465
466/// Returns given value as decimal prefix with corresponding value.
467///
468/// Uses 1000-based prefixes `k`, `M`, `G`, ..., `Y`.
469///
470/// # Examples
471///
472/// ```rust
473/// assert_eq!(ml_progress::decimal_prefix(2000.0), (2.0, "k"));
474/// ```
475pub fn decimal_prefix(mut value: f64) -> (f64, &'static str) {
476 let mut scale = 0;
477 while value.abs() >= 1000.0 && scale < DECIMAL_PREFIXES.len() - 1 {
478 value /= 1000.0;
479 scale += 1;
480 }
481 (value, DECIMAL_PREFIXES[scale])
482}
483
484/// Returns given duration in approximate format: amount and unit.
485///
486/// - Amount is the number of full units, i.e. it's not rounded.
487/// - Unit can be `h` (hours), `m` (minutes) or `s` (seconds)
488///
489/// # Examples
490///
491/// ```rust
492/// use std::time::Duration;
493/// assert_eq!(ml_progress::duration_approx(Duration::from_secs(234)), (3, "m"));
494/// ```
495pub fn duration_approx(duration: Duration) -> (u64, &'static str) {
496 let secs = duration.as_secs();
497 if secs < 60 {
498 (secs, "s")
499 } else if secs < 3600 {
500 (secs / 60, "m")
501 } else {
502 (secs / 3600, "h")
503 }
504}
505
506/// Returns given duration as hours, minutes and seconds.
507///
508/// Returned value is the number of full seconds, i.e. it's not rounded.
509///
510/// # Examples
511///
512/// ```rust
513/// use std::time::Duration;
514/// assert_eq!(ml_progress::duration_hms(Duration::from_secs(234)), (0, 3, 54));
515/// ```
516pub fn duration_hms(duration: Duration) -> (u64, u64, u64) {
517 let secs = duration.as_secs();
518 let h = secs / 3600;
519 let m = (secs % 3600) / 60;
520 let s = secs % 60;
521 (h, m, s)
522}
523
524/// Formats integer with digits in groups of three.
525///
526/// # Examples
527///
528/// ```rust
529/// assert_eq!(ml_progress::group_digits(12345, " "), "12 345");
530/// ```
531pub fn group_digits(mut value: u64, separator: &str) -> String {
532 // `u64` can have at most 7 3-digit groups
533 let mut groups = [0; 7];
534 let mut pos = 0;
535 while pos == 0 || value > 0 {
536 groups[pos] = value % 1000;
537 value /= 1000;
538 pos += 1;
539 }
540
541 let mut result = String::with_capacity(pos * 3 + (pos - 1) * separator.len());
542 pos -= 1;
543 result.push_str(&format!("{}", groups[pos]));
544 while pos > 0 {
545 pos -= 1;
546 result.push_str(separator);
547 result.push_str(&format!("{:03}", groups[pos]));
548 }
549 result
550}
551
552// ======================================================================
553// TESTS
554
555#[cfg(test)]
556mod tests {
557 use super::*;
558
559 // ============================================================
560 // binary_prefix
561
562 #[test]
563 fn binary_prefix_misc() {
564 assert_eq!(binary_prefix(0.0), (0.0, ""));
565
566 assert_eq!(binary_prefix(2560.0), (2.5, "Ki"));
567 assert_eq!(binary_prefix(2621440.0), (2.5, "Mi"));
568
569 assert_eq!(binary_prefix(-2560.0), (-2.5, "Ki"));
570 assert_eq!(binary_prefix(-2621440.0), (-2.5, "Mi"));
571 }
572
573 #[test]
574 fn binary_prefix_overflow() {
575 assert_eq!(binary_prefix(91.0f64.exp2()), (2048.0, "Yi"));
576 }
577
578 // ============================================================
579 // decimal_prefix
580
581 #[test]
582 fn decimal_prefix_misc() {
583 assert_eq!(decimal_prefix(0.0), (0.0, ""));
584
585 assert_eq!(decimal_prefix(2500.0), (2.5, "k"));
586 assert_eq!(decimal_prefix(2500000.0), (2.5, "M"));
587
588 assert_eq!(decimal_prefix(-2500.0), (-2.5, "k"));
589 assert_eq!(decimal_prefix(-2500000.0), (-2.5, "M"));
590 }
591
592 #[test]
593 fn decimal_prefix_overflow() {
594 // TODO: This shouldn't use exact comparison.
595 assert_eq!(decimal_prefix(2.0e27), (2000.0, "Y"));
596 }
597
598 // ============================================================
599 // duration_approx
600
601 #[test]
602 fn duration_approx_no_rounding() {
603 assert_eq!(duration_approx(Duration::from_millis(1800)), (1, "s"));
604 }
605
606 // ============================================================
607 // duration_hms
608
609 #[test]
610 fn duration_hms_no_rounding() {
611 assert_eq!(duration_hms(Duration::from_millis(1800)), (0, 0, 1));
612 }
613
614 // ============================================================
615 // group_digits
616
617 #[test]
618 fn group_digits_0() {
619 assert_eq!(group_digits(0, " "), "0");
620 }
621
622 #[test]
623 fn group_digits_max() {
624 assert_eq!(group_digits(u64::MAX, " "), "18 446 744 073 709 551 615");
625 }
626
627 #[test]
628 fn group_digits_long_separator() {
629 assert_eq!(group_digits(12_345, "abc"), "12abc345");
630 }
631
632 #[test]
633 fn group_digits_has_zero_padding() {
634 assert_eq!(group_digits(1_002_034, " "), "1 002 034");
635 }
636
637 #[test]
638 fn group_digits_no_zero_padding() {
639 assert_eq!(group_digits(1_234_567, " "), "1 234 567");
640 }
641}