1#![doc = include_str!("../README.md")]
2
3use std::{
4 fmt::{self, Write as _},
5 io::{self, stderr, Write as _},
6 sync::atomic::{AtomicUsize, Ordering::Relaxed},
7 sync::RwLock,
8 time::{Duration, Instant},
9};
10
11pub mod prelude {
17 #[cfg(feature = "streams")]
18 pub use crate::ProgressBarStreamExt;
19 pub use crate::{ProgressBar, ProgressBarIterExt};
20}
21
22#[doc(hidden)]
27#[deprecated(note = "renamed to just `Config`")]
28pub type ProgressBarConfig = Config;
29
30#[derive(Clone)]
35pub struct Config {
36 pub width: Option<u32>,
38 pub min_bar_width: u32,
40 pub theme: &'static dyn Theme,
42 pub max_fps: f32,
44 pub should_draw: &'static (dyn Fn() -> bool + Sync),
48}
49
50static DEFAULT_CFG: Config = Config::const_default();
51
52impl Config {
53 pub const fn const_default() -> Self {
55 Config {
56 width: None,
57 min_bar_width: 5,
58 theme: &DefaultTheme,
59 max_fps: 60.0,
60 should_draw: &|| true,
61 }
62 }
63}
64
65impl Default for Config {
66 #[inline]
67 fn default() -> Self {
68 Config::const_default()
69 }
70}
71
72static GLOBAL_CFG: AtomicUsize = AtomicUsize::new(0);
80
81pub fn global_config() -> &'static Config {
83 match GLOBAL_CFG.load(Relaxed) {
84 0 => &DEFAULT_CFG,
85 ptr => unsafe { &*(ptr as *const Config) }
86 }
87}
88
89pub fn set_global_config(new_cfg: &'static Config) {
94 GLOBAL_CFG.store(new_cfg as *const _ as _, Relaxed);
95}
96
97#[cfg_attr(target_arch = "x86_64", repr(align(128)))]
106#[cfg_attr(not(target_arch = "x86_64"), repr(align(64)))]
107struct CachePadded<T>(T);
108
109impl<T> std::ops::Deref for CachePadded<T> {
110 type Target = T;
111
112 fn deref(&self) -> &T {
113 &self.0
114 }
115}
116
117impl<T> std::ops::DerefMut for CachePadded<T> {
118 fn deref_mut(&mut self) -> &mut T {
119 &mut self.0
120 }
121}
122
123#[derive(Debug)]
129pub enum RenderError {
130 Io(io::Error),
131 Fmt(fmt::Error),
132}
133
134impl fmt::Display for RenderError {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 match self {
137 RenderError::Fmt(e) => e.fmt(f),
138 RenderError::Io(e) => e.fmt(f),
139 }
140 }
141}
142
143impl std::error::Error for RenderError {}
145
146impl From<io::Error> for RenderError {
147 fn from(e: io::Error) -> Self {
148 RenderError::Io(e)
149 }
150}
151
152impl From<fmt::Error> for RenderError {
153 fn from(e: fmt::Error) -> Self {
154 RenderError::Fmt(e)
155 }
156}
157
158pub trait Theme: Sync {
164 fn render(&self, pb: &ProgressBar) -> Result<(), RenderError>;
165}
166
167#[derive(Debug, Default)]
168struct DefaultTheme;
169
170fn bar(progress: f32, length: u32) -> String {
172 if length == 0 {
173 return String::new();
174 }
175
176 let inner_len = length.saturating_sub(2);
177 let rescaled = (progress * (inner_len - 1) as f32 * 8.0).round() as u32;
178 let (i, r) = (rescaled / 8, rescaled % 8);
179 let main = "█".repeat(i as usize);
180 let tail = '▏' as u32 - r;
181 let tail = unsafe { std::char::from_u32_unchecked(tail) };
182 let pad_len = inner_len - i - 1 ;
183 let pad = " ".repeat(pad_len as usize);
184
185 let bar = format!("|{}{}{}|", main, tail, pad);
186 debug_assert_eq!(bar.chars().count() as u32, length);
187 bar
188}
189
190fn human_time(duration: Duration) -> String {
191 let total = duration.as_secs();
192 let h = total / 3600;
193 let m = total % 3600 / 60;
194 let s = total % 60;
195 format!("{:02}:{:02}:{:02}", h, m, s)
196}
197
198fn spinner(x: f32, width: u32) -> String {
199 let inner_width = width.saturating_sub(3);
201
202 fn easing_inout_cubic(mut x: f32) -> f32 {
213 x *= 2.0;
214
215 if x < 1.0 {
216 0.5 * x.powi(3)
217 } else {
218 x -= 2.;
219 0.5 * (x.powi(3) + 2.)
220 }
221 }
222
223 let x = ((-x + 0.5).abs() - 0.5) * -2.;
229
230 let x = easing_inout_cubic(x).max(0.).min(1.);
232 let x = ((inner_width as f32) * x).round() as u32;
234
235 let lpad = x as usize;
236 let rpad = inner_width.saturating_sub(x) as usize;
237
238 let ball_offs = x / 8 % 8; let ball = unsafe { std::char::from_u32_unchecked('🌑' as u32 + ball_offs) };
240
241 let spinner = format!("[{}{}{}]", " ".repeat(lpad), ball, " ".repeat(rpad));
242 debug_assert_eq!(spinner.chars().count() as u32, width);
243 spinner
244}
245
246#[cfg(feature = "auto-width")]
262fn stderr_dimensions() -> (usize, usize) {
263 #[cfg(target_os = "windows")]
267 return term_size::dimensions_stdout().unwrap_or((80, 30));
268
269 #[cfg(not(target_os = "windows"))]
270 return term_size::dimensions_stderr().unwrap_or((80, 30));
271}
272
273#[cfg(not(feature = "auto-width"))]
275fn stderr_dimensions() -> (usize, usize) {
276 (80, 30)
277}
278
279impl Theme for DefaultTheme {
280 fn render(&self, pb: &ProgressBar) -> Result<(), RenderError> {
281 let mut o = stderr();
282 let cfg = pb.active_config();
283
284 let left = {
286 let mut buf = String::new();
287
288 if let Some(desc) = pb.message() {
290 write!(buf, "{} ", desc)?;
291 }
292
293 if let Some(progress) = pb.progress() {
294 write!(buf, "{:>6.2}% ", progress * 100.0)?;
295 }
296
297 buf
298 };
299
300 let right = {
302 let mut buf = String::new();
303
304 buf.write_char(' ')?;
306 pb.unit.write_total(&mut buf, pb.value())?;
307 buf.write_char('/')?;
308 match pb.target {
309 Some(target) => pb.unit.write_total(&mut buf, target)?,
310 None => buf.write_char('?')?,
311 }
312
313 if let Some(eta) = pb.eta() {
315 write!(buf, " [{}]", human_time(eta))?;
316 } else {
317 write!(buf, " [{}]", human_time(pb.elapsed()))?;
318 }
319
320 buf.write_str(" (")?;
322 pb.unit.write_rate(&mut buf, pb.iters_per_sec())?;
323 buf.write_char(')')?;
324
325 buf
326 };
327
328 let max_width = cfg
329 .width
330 .unwrap_or_else(|| stderr_dimensions().0 as u32);
331
332 let bar_width = max_width
333 .saturating_sub(left.len() as u32)
334 .saturating_sub(right.len() as u32);
335
336 write!(o, "{}", left)?;
337
338 if bar_width > cfg.min_bar_width {
339 if let Some(progress) = pb.progress() {
341 write!(o, "{}", bar(progress, bar_width))?;
342 }
343 else {
345 let duration = Duration::from_secs(3);
346 let pos = pb.timer_progress(duration);
347 write!(o, "{}", spinner(pos, bar_width - 1))?;
349 }
350 }
351
352 write!(o, "{}\r", right)?;
353
354 o.flush().map_err(Into::into)
355 }
356}
357
358#[non_exhaustive]
364#[derive(Debug, PartialEq, Eq, Copy, Clone)]
365pub enum Unit {
366 Iterations,
367 Bytes,
368}
369
370fn human_iter_unit(x: usize) -> (&'static str, f32) {
371 if x > 10usize.pow(9) {
372 ("B", 1e9)
373 } else if x > 10usize.pow(6) {
374 ("M", 1e6)
375 } else if x > 10usize.pow(3) {
376 ("K", 1e3)
377 } else {
378 ("", 1e0)
379 }
380}
381
382fn bytes_unit(x: usize) -> (&'static str, f32) {
383 if x > 1024usize.pow(4) {
384 ("TiB", 1024_f32.powi(4))
385 } else if x > 1024usize.pow(3) {
386 ("GiB", 1024_f32.powi(3))
387 } else if x > 1024usize.pow(2) {
388 ("MiB", 1024_f32.powi(2))
389 } else if x > 1024usize.pow(1) {
390 ("KiB", 1024_f32.powi(1))
391 } else {
392 ("b", 1024_f32.powi(0))
393 }
394}
395
396impl Unit {
397 fn write_total<W: fmt::Write>(self, mut out: W, amount: usize) -> fmt::Result {
399 match self {
400 Unit::Iterations => {
401 let (unit, div) = human_iter_unit(amount);
402 write!(out, "{:.2}{}", (amount as f32) / div, unit)
403 }
404 Unit::Bytes => {
405 let (unit, div) = bytes_unit(amount);
406 write!(out, "{:.2}{}", (amount as f32) / div, unit)
407 }
408 }
409 }
410
411 fn write_rate<W: fmt::Write>(self, mut out: W, rate: f32) -> fmt::Result {
413 match self {
414 Unit::Iterations => {
415 if rate >= 1.0 {
416 let (unit, div) = human_iter_unit(rate as usize);
417 write!(out, "{:.2}{} it/s", rate / div, unit)
418 } else {
419 write!(out, "{:.0} s/it", 1.0 / rate)
420 }
421 }
422 Unit::Bytes => {
423 let (unit, div) = bytes_unit(rate as usize);
424 write!(out, "{:.2}{}/s", rate / div, unit)
425 }
426 }
427 }
428}
429
430pub struct ProgressBar {
447 cfg: Option<&'static Config>,
449 target: Option<usize>,
451 explicit_target: bool,
453 pub(crate) unit: Unit,
455 start: Instant,
457 message: RwLock<Option<String>>,
459 value: CachePadded<AtomicUsize>,
461 update_ctr: CachePadded<AtomicUsize>,
463 next_print: CachePadded<AtomicUsize>,
465}
466
467impl Drop for ProgressBar {
468 fn drop(&mut self) {
469 if (self.active_config().should_draw)() {
470 self.redraw();
471 eprintln!();
472 }
473 }
474}
475
476impl ProgressBar {
478 fn new(target: Option<usize>, explicit_target: bool) -> Self {
479 Self {
480 cfg: None,
481 target,
482 explicit_target,
483 start: Instant::now(),
484 unit: Unit::Iterations,
485 value: CachePadded(0.into()),
486 update_ctr: CachePadded(0.into()),
487 next_print: CachePadded(1.into()),
488 message: RwLock::new(None),
489 }
490 }
491
492 pub fn smart() -> Self {
494 Self::new(None, false)
495 }
496
497 pub fn spinner() -> Self {
499 Self::new(None, true)
500 }
501
502 pub fn with_target(target: usize) -> Self {
504 Self::new(Some(target), true)
505 }
506}
507
508impl ProgressBar {
510 pub fn config(mut self, cfg: &'static Config) -> Self {
514 self.cfg = Some(cfg);
515 self
516 }
517
518 pub fn force_spinner(mut self) -> Self {
520 self.explicit_target = true;
521 self.target = None;
522 self
523 }
524
525 pub fn unit(mut self, unit: Unit) -> Self {
527 self.unit = unit;
528 self
529 }
530}
531
532impl ProgressBar {
534 #[inline]
536 pub fn active_config(&self) -> &'static Config {
537 self.cfg.unwrap_or_else(global_config)
538 }
539
540 #[rustfmt::skip]
541 pub fn process_size_hint(&mut self, hint: (usize, Option<usize>)) {
542 if self.explicit_target {
544 return;
545 }
546
547 self.target = match hint {
549 (_ , Some(hi)) => Some(hi),
550 (0 , None ) => None,
551 (lo, None ) => Some(lo),
552 };
553 }
554
555 #[inline]
561 pub fn set(&mut self, n: usize) {
562 *self.update_ctr.get_mut() += 1;
563 *self.value.get_mut() = n;
564 }
565
566 #[inline]
568 pub fn set_sync(&self, n: usize) {
569 self.update_ctr.fetch_add(1, Relaxed);
570 self.value.store(n, Relaxed);
571 }
572
573 #[inline]
577 pub fn add(&mut self, n: usize) -> usize {
578 *self.value.get_mut() += n;
579 let prev = *self.update_ctr.get_mut();
580 *self.update_ctr.get_mut() += 1;
581 self.maybe_redraw(prev);
582 prev
583 }
584
585 #[inline]
587 pub fn add_sync(&self, n: usize) -> usize {
588 self.value.fetch_add(n, Relaxed);
589 let prev = self.update_ctr.fetch_add(1, Relaxed);
590 self.maybe_redraw(prev);
591 prev
592 }
593
594 #[inline]
596 pub fn update_ctr(&self) -> usize {
597 self.update_ctr.load(Relaxed)
598 }
599
600 #[inline]
602 pub fn value(&self) -> usize {
603 self.value.load(Relaxed)
604 }
605
606 pub fn message(&self) -> Option<String> {
608 self.message.read().unwrap().clone()
609 }
610
611 pub fn set_message(&mut self, text: Option<impl Into<String>>) {
613 *self.message.get_mut().unwrap() = text.map(Into::into);
614 }
615
616 pub fn set_message_sync(&self, text: Option<impl Into<String>>) {
618 let mut message_lock = self.message.write().unwrap();
619 *message_lock = text.map(Into::into);
620 }
621
622 #[inline]
624 pub fn progress(&self) -> Option<f32> {
625 let target = self.target?;
626 Some(self.value() as f32 / target as f32)
627 }
628
629 pub fn elapsed(&self) -> Duration {
631 self.start.elapsed()
632 }
633
634 pub fn eta(&self) -> Option<Duration> {
636 let left = 1. / self.progress()?;
638 let elapsed = self.elapsed();
639 let estimated_total = elapsed.mul_f32(left);
640 Some(estimated_total.saturating_sub(elapsed))
641 }
642
643 pub fn iters_per_sec(&self) -> f32 {
645 let elapsed_sec = self.elapsed().as_secs_f32();
646 self.value() as f32 / elapsed_sec
647 }
648
649 pub fn updates_per_sec(&self) -> f32 {
651 let elapsed_sec = self.elapsed().as_secs_f32();
652 self.update_ctr() as f32 / elapsed_sec
653 }
654
655 pub fn timer_progress(&self, timer: Duration) -> f32 {
660 let elapsed_sec = self.elapsed().as_secs_f32();
661 let timer_sec = timer.as_secs_f32();
662
663 (elapsed_sec % timer_sec) / timer_sec
664 }
665
666 pub fn redraw(&self) {
668 self.active_config().theme.render(self).unwrap();
669 self.update_next_print();
670 }
671}
672
673impl ProgressBar {
675 #[inline]
676 fn next_print(&self) -> usize {
677 self.next_print.load(Relaxed)
678 }
679
680 fn update_next_print(&self) {
682 if self.update_ctr() < 10 {
684 self.next_print.fetch_add(1, Relaxed);
685 return;
686 }
687
688 let freq = (self.updates_per_sec() / self.active_config().max_fps) as usize;
689 let freq = freq.max(1);
690
691 self.next_print.fetch_add(freq as usize, Relaxed);
692 }
693
694 #[inline]
695 fn maybe_redraw(&self, prev: usize) {
696 #[cold]
697 fn cold_redraw(this: &ProgressBar) {
698 if (this.active_config().should_draw)() {
699 this.redraw();
700 }
701 }
702
703 if prev == self.next_print() {
704 cold_redraw(self);
705 }
706 }
707}
708
709pub struct ProgressBarIter<Inner> {
715 bar: ProgressBar,
716 inner: Inner,
717}
718
719impl<Inner> ProgressBarIter<Inner> {
720 pub fn into_inner(self) -> Inner {
721 self.inner
722 }
723}
724
725impl<Inner: Iterator> Iterator for ProgressBarIter<Inner> {
726 type Item = Inner::Item;
727
728 fn next(&mut self) -> Option<Self::Item> {
729 let next = self.inner.next()?;
730 self.bar.add(1);
731 Some(next)
732 }
733}
734
735pub trait ProgressBarIterExt: Iterator + Sized {
750 fn progress(self) -> ProgressBarIter<Self> {
751 let mut bar = ProgressBar::smart();
752 bar.process_size_hint(self.size_hint());
753 ProgressBarIter { bar, inner: self }
754 }
755
756 fn with_progress(self, mut bar: ProgressBar) -> ProgressBarIter<Self> {
757 bar.process_size_hint(self.size_hint());
758 ProgressBarIter { bar, inner: self }
759 }
760}
761
762impl<Inner: Iterator + Sized> ProgressBarIterExt for Inner {}
763
764#[cfg(feature = "streams")]
769pub mod streams {
770 use super::*;
771 use core::pin::Pin;
772 use futures_core::{
773 task::{Context, Poll},
774 Stream,
775 };
776
777 impl<Inner: Stream> Stream for ProgressBarIter<Inner> {
778 type Item = Inner::Item;
779
780 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
781 let (inner, bar) = unsafe {
784 let this = self.get_unchecked_mut();
785 (Pin::new_unchecked(&mut this.inner), &mut this.bar)
786 };
787
788 match inner.poll_next(cx) {
789 x @ Poll::Ready(Some(_)) => {
790 bar.add(1);
791 x
792 }
793 x => x,
794 }
795 }
796 }
797
798 pub trait ProgressBarStreamExt: Stream + Sized {
801 fn progress(self) -> ProgressBarIter<Self> {
802 let mut bar = ProgressBar::smart();
803 bar.process_size_hint(self.size_hint());
804 ProgressBarIter { bar, inner: self }
805 }
806
807 fn with_progress(self, mut bar: ProgressBar) -> ProgressBarIter<Self> {
808 bar.process_size_hint(self.size_hint());
809 ProgressBarIter { bar, inner: self }
810 }
811 }
812
813 impl<Inner: Stream + Sized> ProgressBarStreamExt for Inner {}
814}
815
816#[cfg(feature = "streams")]
817pub use streams::*;
818
819#[cfg(doctest)]
824mod tests {
825 macro_rules! external_doc_test {
826 ($x:expr) => {
827 #[doc = $x]
828 extern "C" {}
829 };
830 }
831
832 external_doc_test!(include_str!("../README.md"));
834}
835
836