use std::io::{self, IsTerminal};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use indicatif::{ProgressBar, ProgressStyle};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProgressEnabled {
Auto,
On,
Off,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProgressFinish {
Leave,
Clear,
}
#[derive(Debug, Clone)]
pub struct ProgressOptions {
pub enabled: ProgressEnabled,
pub prefix: String,
pub width: Option<u16>,
pub finish: ProgressFinish,
pub draw_target: ProgressDrawTarget,
}
impl Default for ProgressOptions {
fn default() -> Self {
Self {
enabled: ProgressEnabled::Auto,
prefix: String::new(),
width: None,
finish: ProgressFinish::Leave,
draw_target: ProgressDrawTarget::stderr(),
}
}
}
impl ProgressOptions {
pub fn with_enabled(mut self, enabled: ProgressEnabled) -> Self {
self.enabled = enabled;
self
}
pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
self.prefix = prefix.into();
self
}
pub fn with_width(mut self, width: Option<u16>) -> Self {
self.width = width;
self
}
pub fn with_finish(mut self, finish: ProgressFinish) -> Self {
self.finish = finish;
self
}
pub fn with_draw_target(mut self, draw_target: ProgressDrawTarget) -> Self {
self.draw_target = draw_target;
self
}
}
#[derive(Debug, Clone)]
pub enum ProgressDrawTarget {
Stderr,
Writer { buffer: Arc<Mutex<Vec<u8>>> },
}
impl ProgressDrawTarget {
pub fn stderr() -> Self {
Self::Stderr
}
pub fn to_writer(buffer: Arc<Mutex<Vec<u8>>>) -> Self {
Self::Writer { buffer }
}
}
#[derive(Debug, Clone)]
pub struct Progress {
state: Option<Arc<ProgressState>>,
}
#[derive(Debug)]
struct ProgressState {
bar: ProgressBar,
finish: ProgressFinish,
rendered: AtomicBool,
finished: AtomicBool,
}
impl Drop for ProgressState {
fn drop(&mut self) {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if self.finished.load(Ordering::Relaxed) {
return;
}
if !self.rendered.load(Ordering::Relaxed) {
return;
}
match self.finish {
ProgressFinish::Leave => self.bar.finish(),
ProgressFinish::Clear => self.bar.finish_and_clear(),
}
}));
}
}
impl Progress {
pub fn new(total: u64, options: ProgressOptions) -> Self {
if !should_enable(&options) {
return Self { state: None };
}
let draw_target = to_indicatif_draw_target(&options.draw_target, options.width);
let bar = ProgressBar::new(total);
bar.set_draw_target(draw_target);
bar.set_style(determinate_style());
let state = Arc::new(ProgressState {
bar,
finish: options.finish,
rendered: AtomicBool::new(false),
finished: AtomicBool::new(false),
});
if !options.prefix.is_empty() {
state.rendered.store(true, Ordering::Relaxed);
state.bar.set_prefix(options.prefix);
}
Self { state: Some(state) }
}
pub fn spinner(options: ProgressOptions) -> Self {
if !should_enable(&options) {
return Self { state: None };
}
let draw_target = to_indicatif_draw_target(&options.draw_target, options.width);
let bar = ProgressBar::new_spinner();
bar.set_draw_target(draw_target);
bar.set_style(spinner_style());
let state = Arc::new(ProgressState {
bar,
finish: options.finish,
rendered: AtomicBool::new(false),
finished: AtomicBool::new(false),
});
if !options.prefix.is_empty() {
state.rendered.store(true, Ordering::Relaxed);
state.bar.set_prefix(options.prefix);
}
Self { state: Some(state) }
}
pub fn set_position(&self, pos: u64) {
if let Some(state) = &self.state {
state.rendered.store(true, Ordering::Relaxed);
state.bar.set_position(pos);
}
}
pub fn inc(&self, delta: u64) {
if let Some(state) = &self.state {
state.rendered.store(true, Ordering::Relaxed);
state.bar.inc(delta);
}
}
pub fn tick(&self) {
if let Some(state) = &self.state {
state.rendered.store(true, Ordering::Relaxed);
state.bar.tick();
}
}
pub fn set_message(&self, message: impl Into<String>) {
if let Some(state) = &self.state {
state.rendered.store(true, Ordering::Relaxed);
state.bar.set_message(message.into());
}
}
pub fn finish(&self) {
if let Some(state) = &self.state {
if state.finished.swap(true, Ordering::Relaxed) {
return;
}
state.rendered.store(true, Ordering::Relaxed);
state.bar.finish();
}
}
pub fn finish_with_message(&self, message: impl Into<String>) {
if let Some(state) = &self.state {
if state.finished.swap(true, Ordering::Relaxed) {
return;
}
state.rendered.store(true, Ordering::Relaxed);
state.bar.finish_with_message(message.into());
}
}
pub fn finish_and_clear(&self) {
if let Some(state) = &self.state {
if state.finished.swap(true, Ordering::Relaxed) {
return;
}
state.rendered.store(true, Ordering::Relaxed);
state.bar.finish_and_clear();
}
}
pub fn suspend<F: FnOnce() -> R, R>(&self, f: F) -> R {
match &self.state {
Some(state) => state.bar.suspend(f),
None => f(),
}
}
}
fn should_enable(options: &ProgressOptions) -> bool {
match options.enabled {
ProgressEnabled::On => true,
ProgressEnabled::Off => false,
ProgressEnabled::Auto => match &options.draw_target {
ProgressDrawTarget::Stderr => io::stderr().is_terminal(),
ProgressDrawTarget::Writer { .. } => true,
},
}
}
fn determinate_style() -> ProgressStyle {
let style = ProgressStyle::with_template("{prefix}{wide_bar} {pos}/{len} {msg}");
match style {
Ok(style) => style.progress_chars("#-"),
Err(_) => ProgressStyle::default_bar().progress_chars("#-"),
}
}
fn spinner_style() -> ProgressStyle {
let style = ProgressStyle::with_template("{prefix}{spinner} {msg}");
match style {
Ok(style) => style.tick_chars(r"-\|/"),
Err(_) => ProgressStyle::default_spinner().tick_chars(r"-\|/"),
}
}
fn to_indicatif_draw_target(
draw_target: &ProgressDrawTarget,
width: Option<u16>,
) -> indicatif::ProgressDrawTarget {
match draw_target {
ProgressDrawTarget::Stderr => indicatif::ProgressDrawTarget::stderr(),
ProgressDrawTarget::Writer { buffer } => indicatif::ProgressDrawTarget::term_like(
Box::new(WriterTerm::new(buffer.clone(), width.unwrap_or(80))),
),
}
}
#[derive(Debug)]
struct WriterTerm {
buffer: Arc<Mutex<Vec<u8>>>,
width: u16,
}
impl WriterTerm {
fn new(buffer: Arc<Mutex<Vec<u8>>>, width: u16) -> Self {
Self { buffer, width }
}
fn write_all(&self, bytes: &[u8]) -> io::Result<()> {
let mut guard = self.buffer.lock().expect("writer buffer lock");
guard.extend_from_slice(bytes);
Ok(())
}
fn write_str_bytes(&self, s: &str) -> io::Result<()> {
self.write_all(s.as_bytes())
}
}
impl indicatif::TermLike for WriterTerm {
fn width(&self) -> u16 {
self.width
}
fn move_cursor_up(&self, n: usize) -> io::Result<()> {
self.write_str_bytes(&format!("\u{1b}[{n}A"))
}
fn move_cursor_down(&self, n: usize) -> io::Result<()> {
self.write_str_bytes(&format!("\u{1b}[{n}B"))
}
fn move_cursor_right(&self, n: usize) -> io::Result<()> {
self.write_str_bytes(&format!("\u{1b}[{n}C"))
}
fn move_cursor_left(&self, n: usize) -> io::Result<()> {
self.write_str_bytes(&format!("\u{1b}[{n}D"))
}
fn write_line(&self, s: &str) -> io::Result<()> {
self.write_str_bytes(s)?;
self.write_all(b"\n")
}
fn write_str(&self, s: &str) -> io::Result<()> {
self.write_str_bytes(s)
}
fn clear_line(&self) -> io::Result<()> {
self.write_str_bytes("\r\u{1b}[2K")
}
fn flush(&self) -> io::Result<()> {
Ok(())
}
}