//! A log-friendly command-line progress bar.
//!
//! Many progress bar implementations update the progress report in place,
//! by going back and overwriting previous output. This allows for beautiful
//! progress displays, but becomes a problem when moving backwards is not
//! possible, for example when writing to a pipe. This crate's progress bar
//! never tries to modify what was printed before, so the output can be
//! piped directly to a log file.
//!
//! # Usage
//!
//! Add this to your Cargo.toml:
//!
//! ```toml
//! [dependencies]
//! logbar = "0.1"
//! ```
//!
//! # Examples
//!
//!
//! This example creates a default progress bar for a ten-step process:
//! ```rust
//! let bar = logbar::ProgressBar::new(10);
//! // first step (10%) done
//! bar.inc(1);
//! // next three steps done
//! bar.inc(3);
//! // everything done
//! bar.finish();
//! ```
//!
//! We can also customise the style of the progress bar:
//! ```rust
//! let style = logbar::Style::default()
//! .width(80) // 80 characters wide
//! .labels(false) // no XX% labels
//! .tick('↓').bar('-') // rendered as ↓---↓---↓ etc.
//! .indicator('█') // indicating the progress with '█' characters
//! ;
//! let bar = logbar::ProgressBar::with_style(10, style);
//! bar.finish();
//! ```
//!
use std::default::Default;
use std::{cmp, default, sync};
static DEFAULT_WIDTH: usize = 50;
static DEFAULT_TICK: char = '|';
static DEFAULT_BAR: char = '=';
static DEFAULT_INDICATOR: char = '#';
static SEGMENTS: [usize; 4] = [10, 5, 4, 2];
/// Progress bar style
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Style {
width: usize,
labels: bool,
tick: char,
bar: char,
indicator: char,
}
impl Style {
/// Default progress bar style
///
/// # Example
///
/// Create a progress bar, explicitly asking for the default style:
/// ```rust
/// let style = logbar::Style::new();
/// let max_progress = 100;
/// let bar = logbar::ProgressBar::with_style(max_progress, style);
/// ```
pub fn new() -> Self {
Style::default()
}
/// Set the progress bar width in characters
///
/// # Example
///
/// Create a progress bar with a width of 80 characters:
/// ```rust
/// let style = logbar::Style::new().width(80);
/// let max_progress = 100;
/// let bar = logbar::ProgressBar::with_style(max_progress, style);
/// ```
pub fn width(mut self, width: usize) -> Self {
self.width = width;
self
}
/// Toggle progress bar labels of the form XX%
///
/// # Example
///
/// Create a progress bar without labels:
/// ```rust
/// let style = logbar::Style::new().labels(false);
/// let max_progress = 100;
/// let bar = logbar::ProgressBar::with_style(max_progress, style);
/// ```
pub fn labels(mut self, labels: bool) -> Self {
self.labels = labels;
self
}
/// Choose a "tick" character separating the progress bar segments
///
/// # Example
///
/// Create a progress bar with '↓' as tick character:
/// ```rust
/// let style = logbar::Style::new().tick('↓');
/// let max_progress = 100;
/// let bar = logbar::ProgressBar::with_style(max_progress, style);
/// ```
pub fn tick(mut self, tick: char) -> Self {
self.tick = tick;
self
}
/// Choose a character for the progress bar segments
///
/// # Example
///
/// Create a progress bar made out of '-' characters, separated
/// by the default "tick" character.
/// ```rust
/// let style = logbar::Style::new().bar('-');
/// let max_progress = 100;
/// let bar = logbar::ProgressBar::with_style(max_progress, style);
/// ```
#[allow(clippy::blacklisted_name)]
pub fn bar(mut self, bar: char) -> Self {
self.bar = bar;
self
}
/// Choose a progress indicator
///
/// # Example
///
/// Create a progress bar where the progress is indicated by the
/// number of '█' characters.
/// ```rust
/// let style = logbar::Style::new().indicator('█');
/// let max_progress = 100;
/// let bar = logbar::ProgressBar::with_style(max_progress, style);
/// ```
pub fn indicator(mut self, indicator: char) -> Self {
self.indicator = indicator;
self
}
}
impl default::Default for Style {
/// Default progress bar style
///
/// # Example
///
/// Create a progress bar, explicitly asking for the default style:
/// ```rust
/// let style = logbar::Style::default();
/// let max_progress = 100;
/// let bar = logbar::ProgressBar::with_style(max_progress, style);
/// ```
fn default() -> Self {
Style {
width: DEFAULT_WIDTH,
labels: true,
tick: DEFAULT_TICK,
bar: DEFAULT_BAR,
indicator: DEFAULT_INDICATOR,
}
}
}
#[derive(Clone, Default, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
struct Counter {
count: usize,
progress: usize,
finished: bool,
}
/// A log-friendly progress bar
#[derive(Debug)]
pub struct ProgressBar {
counter: sync::Arc<sync::Mutex<Counter>>,
max_progress: usize,
style: Style,
}
fn num_segments(width: usize) -> usize {
for s in SEGMENTS.iter() {
// the number of segments must divide the total width
// and each segment must be large enough for labels
if width % s == 0 && width / s > 3 {
return *s;
}
}
1
}
fn draw_labels(width: usize, segments: usize) {
debug_assert_eq!(width % segments, 0);
eprint!("0% ");
let seg_width = width / segments;
for p in 1..=segments {
for _ in 0..(seg_width - 3) {
eprint!(" ");
}
eprint!("{}%", p * 100 / segments)
}
eprintln!("")
}
fn draw_tickbar(style: &Style, segments: usize) {
let width = style.width;
debug_assert_eq!(width % segments, 0);
eprint!("{}", style.tick);
let seg_width = width / segments;
for _ in 1..=segments {
for _ in 0..(seg_width - 1) {
eprint!("{}", style.bar);
}
eprint!("{}", style.tick)
}
eprintln!("")
}
fn draw_bar(style: &Style) {
let width = style.width;
let segments = num_segments(width);
if width > 3 && style.labels {
draw_labels(width, segments)
}
if width > 1 {
draw_tickbar(style, segments)
} else if width == 1 {
eprintln!("{}", style.tick)
}
}
impl ProgressBar {
/// Create a new progress bar with default style
///
/// # Example
///
/// ```rust
/// let max_progress = 100;
/// let bar = logbar::ProgressBar::new(max_progress);
/// ```
pub fn new(max_progress: usize) -> Self {
ProgressBar::with_style(max_progress, Style::default())
}
/// Create a new progress bar with custom style
///
/// # Example
///
/// Create a progress bar with a width of 80 characters:
/// ```rust
/// let style = logbar::Style::new().width(80);
/// let max_progress = 100;
/// let bar = logbar::ProgressBar::with_style(max_progress, style);
/// ```
pub fn with_style(max_progress: usize, style: Style) -> Self {
let counter = sync::Arc::new(sync::Mutex::new(Counter::default()));
draw_bar(&style);
ProgressBar {
counter,
max_progress,
style,
}
}
/// Get the style of the current progress bar
///
/// # Example
///
/// ```rust
/// let max_progress = 100;
/// let bar = logbar::ProgressBar::new(max_progress);
/// assert_eq!(bar.style(), &logbar::Style::default())
/// ```
pub fn style(&self) -> &Style {
&self.style
}
/// Increment the progress
///
/// This method increments the internal progress counter, up to the
/// maximum defined during the construction of the progress bar. It
/// then updates the progress display.
///
/// # Example
///
/// ```rust
/// let max_progress = 50;
/// let bar = logbar::ProgressBar::new(max_progress);
/// bar.inc(10); // Increment progress to 10 out of 50.
/// // The progress bar is at 20% now
/// bar.inc(10); // Increment progress to 20 out of 50.
/// // The progress bar is at 40% now
/// bar.inc(100); // Increment progress to 50 out of 50.
/// // The progress bar is at 100% now
/// ```
pub fn inc(&self, i: usize) {
let new_progress = {
let mut c = self.counter.lock().unwrap();
let new_count = cmp::min(c.count + i, self.max_progress);
let new_progress = if self.max_progress > 0 {
new_count * self.style.width / self.max_progress
} else {
0
};
let diff = new_progress - c.progress;
*c = Counter {
count: new_count,
progress: new_progress,
finished: false,
};
diff
};
for _ in 0..new_progress {
eprint!("{}", self.style.indicator);
}
}
/// Finish the progress bar
///
/// This method sets the progress to 100% and moves to the next line
/// after the progress bar
///
/// # Example
///
/// ```rust
/// let max_progress = 50;
/// let bar = logbar::ProgressBar::new(max_progress);
/// bar.finish();
/// // The progress bar is at 100% now
/// ```
pub fn finish(&self) {
self.inc(self.max_progress);
let mut c = self.counter.lock().unwrap();
if !c.finished {
eprintln!("");
c.finished = true;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
//TODO: capture stderr and check
// for the time being, run
// cargo test -- --nocapture --test-threads=1
// and check the output manually
#[test]
fn construct() {
eprintln!("");
let max_progress = 1000;
{
let bar = ProgressBar::new(max_progress);
assert_eq!(bar.style().width, DEFAULT_WIDTH);
}
{
let width = 80;
let mut style = Style::default();
style.width = width;
let bar = ProgressBar::with_style(max_progress, style);
assert_eq!(bar.style().width, width);
}
}
#[test]
fn inc() {
eprintln!("");
let max_progress = 20;
let ten_millis = std::time::Duration::from_millis(10);
let bar = ProgressBar::new(max_progress);
for _ in 0..max_progress {
std::thread::sleep(ten_millis);
bar.inc(1);
}
eprintln!("");
let bar = ProgressBar::new(max_progress);
bar.inc(2 * max_progress);
eprintln!("");
let _bar = ProgressBar::new(max_progress);
eprintln!("");
}
#[test]
fn empty() {
eprintln!("");
let max_progress = 0;
let bar = ProgressBar::new(max_progress);
bar.inc(0);
bar.inc(1);
bar.finish();
}
#[test]
fn finish() {
eprintln!("");
let max_progress = 200;
let bar = ProgressBar::new(max_progress);
bar.finish();
}
#[test]
fn abort() {
eprintln!("");
let max_progress = 200;
let bar = ProgressBar::new(max_progress);
bar.inc(50);
}
#[test]
fn alt_styles() {
eprintln!("");
let max_progress = 200;
eprintln!("indicator █:");
let style = Style::new().indicator('█');
let bar = ProgressBar::with_style(max_progress, style);
bar.finish();
eprintln!("\ntick ↓:");
let style = Style::new().tick('↓');
let bar = ProgressBar::with_style(max_progress, style);
bar.finish();
eprintln!("\nbar -:");
let style = Style::new().bar('-');
let bar = ProgressBar::with_style(max_progress, style);
bar.finish();
eprintln!("\nno labels:");
let style = Style::new().labels(false);
let bar = ProgressBar::with_style(max_progress, style);
bar.finish();
eprintln!("\nall of the above:");
let style = Style::new().indicator('█').labels(false).tick('↓').bar('-');
let bar = ProgressBar::with_style(max_progress, style);
bar.finish();
for w in 0..=20 {
eprintln!("\nwidth {}:", w);
let style = Style::new().width(w);
let bar = ProgressBar::with_style(max_progress, style);
bar.finish();
}
let w = 40;
eprintln!("\nwidth {}:", w);
let style = Style::new().width(w);
let bar = ProgressBar::with_style(max_progress, style.clone());
bar.finish();
}
}