#![doc = include_str!("../README.md")]
use std::{
fmt::{self, Write as _},
io::{self, stderr, IsTerminal, Write as _},
sync::atomic::{AtomicUsize, Ordering::Relaxed},
sync::RwLock,
time::{Duration, Instant},
};
pub mod prelude {
#[cfg(feature = "streams")]
pub use crate::ProgressBarStreamExt;
pub use crate::{ProgressBar, ProgressBarIterExt};
}
#[doc(hidden)]
#[deprecated(note = "renamed to just `Config`")]
pub type ProgressBarConfig = Config;
#[derive(Clone)]
pub struct Config {
pub width: Option<u32>,
pub min_bar_width: u32,
pub theme: &'static dyn Theme,
pub max_fps: f32,
pub should_draw: &'static (dyn Fn() -> bool + Sync),
}
static DEFAULT_CFG: Config = Config::const_default();
impl Config {
pub const fn const_default() -> Self {
Config {
width: None,
min_bar_width: 5,
theme: &DefaultTheme,
max_fps: 60.0,
should_draw: &|| stderr().is_terminal(),
}
}
}
impl Default for Config {
#[inline]
fn default() -> Self {
Config::const_default()
}
}
static GLOBAL_CFG: AtomicUsize = AtomicUsize::new(0);
pub fn global_config() -> &'static Config {
match GLOBAL_CFG.load(Relaxed) {
0 => &DEFAULT_CFG,
ptr => unsafe { &*(ptr as *const Config) },
}
}
pub fn set_global_config(new_cfg: &'static Config) {
GLOBAL_CFG.store(new_cfg as *const _ as _, Relaxed);
}
#[cfg_attr(target_arch = "x86_64", repr(align(128)))]
#[cfg_attr(not(target_arch = "x86_64"), repr(align(64)))]
struct CachePadded<T>(T);
impl<T> std::ops::Deref for CachePadded<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
impl<T> std::ops::DerefMut for CachePadded<T> {
fn deref_mut(&mut self) -> &mut T {
&mut self.0
}
}
#[derive(Debug)]
pub enum RenderError {
Io(io::Error),
Fmt(fmt::Error),
}
impl fmt::Display for RenderError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RenderError::Fmt(e) => e.fmt(f),
RenderError::Io(e) => e.fmt(f),
}
}
}
impl std::error::Error for RenderError {}
impl From<io::Error> for RenderError {
fn from(e: io::Error) -> Self {
RenderError::Io(e)
}
}
impl From<fmt::Error> for RenderError {
fn from(e: fmt::Error) -> Self {
RenderError::Fmt(e)
}
}
pub trait Theme: Sync {
fn render(&self, pb: &ProgressBar) -> Result<(), RenderError>;
}
#[derive(Debug, Default)]
struct DefaultTheme;
fn bar(progress: f32, length: u32) -> String {
if length == 0 {
return String::new();
}
let inner_len = length.saturating_sub(2);
let rescaled = (progress * (inner_len - 1) as f32 * 8.0).round() as u32;
let (i, r) = (rescaled / 8, rescaled % 8);
let main = "█".repeat(i as usize);
let tail = '▏' as u32 - r;
let tail = unsafe { std::char::from_u32_unchecked(tail) };
let pad_len = inner_len - i - 1 ;
let pad = " ".repeat(pad_len as usize);
let bar = format!("|{}{}{}|", main, tail, pad);
debug_assert_eq!(bar.chars().count() as u32, length);
bar
}
fn human_time(duration: Duration) -> String {
let total = duration.as_secs();
let h = total / 3600;
let m = total % 3600 / 60;
let s = total % 60;
format!("{:02}:{:02}:{:02}", h, m, s)
}
fn spinner(x: f32, width: u32) -> String {
let inner_width = width.saturating_sub(3);
fn easing_inout_cubic(mut x: f32) -> f32 {
x *= 2.0;
if x < 1.0 {
0.5 * x.powi(3)
} else {
x -= 2.;
0.5 * (x.powi(3) + 2.)
}
}
let x = ((-x + 0.5).abs() - 0.5) * -2.;
let x = easing_inout_cubic(x).max(0.).min(1.);
let x = ((inner_width as f32) * x).round() as u32;
let lpad = x as usize;
let rpad = inner_width.saturating_sub(x) as usize;
let ball_offs = x / 8 % 8; let ball = unsafe { std::char::from_u32_unchecked('🌑' as u32 + ball_offs) };
let spinner = format!("[{}{}{}]", " ".repeat(lpad), ball, " ".repeat(rpad));
debug_assert_eq!(spinner.chars().count() as u32, width);
spinner
}
#[cfg(feature = "auto-width")]
fn stderr_dimensions() -> (usize, usize) {
#[cfg(target_os = "windows")]
return term_size::dimensions_stdout().unwrap_or((80, 30));
#[cfg(not(target_os = "windows"))]
return term_size::dimensions_stderr().unwrap_or((80, 30));
}
#[cfg(not(feature = "auto-width"))]
fn stderr_dimensions() -> (usize, usize) {
(80, 30)
}
impl Theme for DefaultTheme {
fn render(&self, pb: &ProgressBar) -> Result<(), RenderError> {
let mut o = stderr();
let cfg = pb.active_config();
let left = {
let mut buf = String::new();
if let Some(desc) = pb.message() {
write!(buf, "{} ", desc)?;
}
if let Some(progress) = pb.progress() {
write!(buf, "{:>6.2}% ", progress * 100.0)?;
}
buf
};
let right = {
let mut buf = String::new();
buf.write_char(' ')?;
pb.unit.write_total(&mut buf, pb.value())?;
buf.write_char('/')?;
match pb.target {
Some(target) => pb.unit.write_total(&mut buf, target)?,
None => buf.write_char('?')?,
}
if let Some(eta) = pb.eta() {
write!(buf, " [{}]", human_time(eta))?;
} else {
write!(buf, " [{}]", human_time(pb.elapsed()))?;
}
buf.write_str(" (")?;
pb.unit.write_rate(&mut buf, pb.iters_per_sec())?;
buf.write_char(')')?;
buf
};
let max_width = cfg.width.unwrap_or_else(|| stderr_dimensions().0 as u32);
let bar_width = max_width
.saturating_sub(left.len() as u32)
.saturating_sub(right.len() as u32);
write!(o, "{}", left)?;
if bar_width > cfg.min_bar_width {
if let Some(progress) = pb.progress() {
write!(o, "{}", bar(progress, bar_width))?;
}
else {
let duration = Duration::from_secs(3);
let pos = pb.timer_progress(duration);
write!(o, "{}", spinner(pos, bar_width - 1))?;
}
}
write!(o, "{}\r", right)?;
o.flush().map_err(Into::into)
}
}
#[non_exhaustive]
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum Unit {
Iterations,
Bytes,
}
fn human_iter_unit(x: usize) -> (&'static str, f32) {
if x > 10usize.pow(9) {
("B", 1e9)
} else if x > 10usize.pow(6) {
("M", 1e6)
} else if x > 10usize.pow(3) {
("K", 1e3)
} else {
("", 1e0)
}
}
fn bytes_unit(x: usize) -> (&'static str, f32) {
if x > 1024usize.pow(4) {
("TiB", 1024_f32.powi(4))
} else if x > 1024usize.pow(3) {
("GiB", 1024_f32.powi(3))
} else if x > 1024usize.pow(2) {
("MiB", 1024_f32.powi(2))
} else if x > 1024usize.pow(1) {
("KiB", 1024_f32.powi(1))
} else {
("b", 1024_f32.powi(0))
}
}
impl Unit {
fn write_total<W: fmt::Write>(self, mut out: W, amount: usize) -> fmt::Result {
match self {
Unit::Iterations => {
let (unit, div) = human_iter_unit(amount);
write!(out, "{:.2}{}", (amount as f32) / div, unit)
}
Unit::Bytes => {
let (unit, div) = bytes_unit(amount);
write!(out, "{:.2}{}", (amount as f32) / div, unit)
}
}
}
fn write_rate<W: fmt::Write>(self, mut out: W, rate: f32) -> fmt::Result {
match self {
Unit::Iterations => {
if rate >= 1.0 {
let (unit, div) = human_iter_unit(rate as usize);
write!(out, "{:.2}{} it/s", rate / div, unit)
} else {
write!(out, "{:.0} s/it", 1.0 / rate)
}
}
Unit::Bytes => {
let (unit, div) = bytes_unit(rate as usize);
write!(out, "{:.2}{}/s", rate / div, unit)
}
}
}
}
pub struct ProgressBar {
cfg: Option<&'static Config>,
target: Option<usize>,
explicit_target: bool,
pub(crate) unit: Unit,
start: Instant,
message: RwLock<Option<String>>,
value: CachePadded<AtomicUsize>,
update_ctr: CachePadded<AtomicUsize>,
next_print: CachePadded<AtomicUsize>,
}
impl Drop for ProgressBar {
fn drop(&mut self) {
if (self.active_config().should_draw)() {
self.redraw();
eprintln!();
}
}
}
impl ProgressBar {
fn new(target: Option<usize>, explicit_target: bool) -> Self {
Self {
cfg: None,
target,
explicit_target,
start: Instant::now(),
unit: Unit::Iterations,
value: CachePadded(0.into()),
update_ctr: CachePadded(0.into()),
next_print: CachePadded(1.into()),
message: RwLock::new(None),
}
}
pub fn smart() -> Self {
Self::new(None, false)
}
pub fn spinner() -> Self {
Self::new(None, true)
}
pub fn with_target(target: usize) -> Self {
Self::new(Some(target), true)
}
}
impl ProgressBar {
pub fn config(mut self, cfg: &'static Config) -> Self {
self.cfg = Some(cfg);
self
}
pub fn force_spinner(mut self) -> Self {
self.explicit_target = true;
self.target = None;
self
}
pub fn unit(mut self, unit: Unit) -> Self {
self.unit = unit;
self
}
}
impl ProgressBar {
#[inline]
pub fn active_config(&self) -> &'static Config {
self.cfg.unwrap_or_else(global_config)
}
#[rustfmt::skip]
pub fn process_size_hint(&mut self, hint: (usize, Option<usize>)) {
if self.explicit_target {
return;
}
self.target = match hint {
(_ , Some(hi)) => Some(hi),
(0 , None ) => None,
(lo, None ) => Some(lo),
};
}
#[inline]
pub fn set(&mut self, n: usize) {
*self.update_ctr.get_mut() += 1;
*self.value.get_mut() = n;
}
#[inline]
pub fn set_sync(&self, n: usize) {
self.update_ctr.fetch_add(1, Relaxed);
self.value.store(n, Relaxed);
}
#[inline]
pub fn add(&mut self, n: usize) -> usize {
*self.value.get_mut() += n;
let prev = *self.update_ctr.get_mut();
*self.update_ctr.get_mut() += 1;
self.maybe_redraw(prev);
prev
}
#[inline]
pub fn add_sync(&self, n: usize) -> usize {
self.value.fetch_add(n, Relaxed);
let prev = self.update_ctr.fetch_add(1, Relaxed);
self.maybe_redraw(prev);
prev
}
#[inline]
pub fn update_ctr(&self) -> usize {
self.update_ctr.load(Relaxed)
}
#[inline]
pub fn value(&self) -> usize {
self.value.load(Relaxed)
}
pub fn message(&self) -> Option<String> {
self.message.read().unwrap().clone()
}
pub fn set_message(&mut self, text: Option<impl Into<String>>) {
*self.message.get_mut().unwrap() = text.map(Into::into);
}
pub fn set_message_sync(&self, text: Option<impl Into<String>>) {
let mut message_lock = self.message.write().unwrap();
*message_lock = text.map(Into::into);
}
#[inline]
pub fn progress(&self) -> Option<f32> {
let target = self.target?;
if target == 0 {
return None;
}
Some(self.value() as f32 / target as f32)
}
pub fn elapsed(&self) -> Duration {
self.start.elapsed()
}
pub fn eta(&self) -> Option<Duration> {
let progress = self.progress()?;
if progress == 0.0 {
return None;
}
let left = 1. / progress;
let elapsed = self.elapsed();
let estimated_total = elapsed.mul_f32(left);
Some(estimated_total.saturating_sub(elapsed))
}
pub fn iters_per_sec(&self) -> f32 {
let elapsed_sec = self.elapsed().as_secs_f32();
self.value() as f32 / elapsed_sec
}
pub fn updates_per_sec(&self) -> f32 {
let elapsed_sec = self.elapsed().as_secs_f32();
self.update_ctr() as f32 / elapsed_sec
}
pub fn timer_progress(&self, timer: Duration) -> f32 {
let elapsed_sec = self.elapsed().as_secs_f32();
let timer_sec = timer.as_secs_f32();
(elapsed_sec % timer_sec) / timer_sec
}
pub fn redraw(&self) {
self.active_config().theme.render(self).unwrap();
self.update_next_print();
}
}
impl ProgressBar {
#[inline]
fn next_print(&self) -> usize {
self.next_print.load(Relaxed)
}
fn update_next_print(&self) {
if self.update_ctr() < 10 {
self.next_print.fetch_add(1, Relaxed);
return;
}
let freq = (self.updates_per_sec() / self.active_config().max_fps) as usize;
let freq = freq.max(1);
self.next_print.fetch_add(freq as usize, Relaxed);
}
#[inline]
fn maybe_redraw(&self, prev: usize) {
#[cold]
fn cold_redraw(this: &ProgressBar) {
if (this.active_config().should_draw)() {
this.redraw();
}
}
if prev == self.next_print() {
cold_redraw(self);
}
}
}
pub struct ProgressBarIter<Inner> {
bar: ProgressBar,
inner: Inner,
}
impl<Inner> ProgressBarIter<Inner> {
pub fn into_inner(self) -> Inner {
self.inner
}
}
impl<Inner: Iterator> Iterator for ProgressBarIter<Inner> {
type Item = Inner::Item;
fn next(&mut self) -> Option<Self::Item> {
let next = self.inner.next()?;
self.bar.add(1);
Some(next)
}
}
pub trait ProgressBarIterExt: Iterator + Sized {
fn progress(self) -> ProgressBarIter<Self> {
let mut bar = ProgressBar::smart();
bar.process_size_hint(self.size_hint());
ProgressBarIter { bar, inner: self }
}
fn with_progress(self, mut bar: ProgressBar) -> ProgressBarIter<Self> {
bar.process_size_hint(self.size_hint());
ProgressBarIter { bar, inner: self }
}
}
impl<Inner: Iterator + Sized> ProgressBarIterExt for Inner {}
#[cfg(feature = "streams")]
pub mod streams {
use super::*;
use core::pin::Pin;
use futures_core::{
task::{Context, Poll},
Stream,
};
impl<Inner: Stream> Stream for ProgressBarIter<Inner> {
type Item = Inner::Item;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let (inner, bar) = unsafe {
let this = self.get_unchecked_mut();
(Pin::new_unchecked(&mut this.inner), &mut this.bar)
};
match inner.poll_next(cx) {
x @ Poll::Ready(Some(_)) => {
bar.add(1);
x
}
x => x,
}
}
}
pub trait ProgressBarStreamExt: Stream + Sized {
fn progress(self) -> ProgressBarIter<Self> {
let mut bar = ProgressBar::smart();
bar.process_size_hint(self.size_hint());
ProgressBarIter { bar, inner: self }
}
fn with_progress(self, mut bar: ProgressBar) -> ProgressBarIter<Self> {
bar.process_size_hint(self.size_hint());
ProgressBarIter { bar, inner: self }
}
}
impl<Inner: Stream + Sized> ProgressBarStreamExt for Inner {}
}
#[cfg(feature = "streams")]
pub use streams::*;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zero_target() {
for v in [0, 1, 100] {
let mut pb = ProgressBar::smart();
pb.target = Some(0);
pb.value.0.store(v, Relaxed);
assert_eq!(pb.progress(), None);
assert_eq!(pb.eta(), None);
}
}
}
#[cfg(doctest)]
mod doctests {
macro_rules! external_doc_test {
($x:expr) => {
#[doc = $x]
extern "C" {}
};
}
external_doc_test!(include_str!("../README.md"));
}