use indicatif::{ProgressBar, ProgressStyle};
use std::time::Duration;
pub struct Spinner {
bar: ProgressBar,
}
impl Spinner {
pub fn new(message: &str) -> Self {
let bar = if atty_stderr() {
let pb = ProgressBar::new_spinner();
pb.set_style(
ProgressStyle::with_template("{spinner:.cyan} {msg}")
.unwrap()
.tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
);
pb.set_message(message.to_string());
pb.enable_steady_tick(Duration::from_millis(80));
pb
} else {
eprintln!("{}", message);
ProgressBar::hidden()
};
Self { bar }
}
pub fn set_message(&self, message: impl Into<String>) {
self.bar.set_message(message.into());
}
pub fn finish(&self, message: &str) {
if !self.bar.is_hidden() {
self.bar.finish_with_message(format!("✓ {}", message));
}
}
pub fn finish_warn(&self, message: &str) {
if !self.bar.is_hidden() {
self.bar.finish_with_message(format!("⚠ {}", message));
}
}
pub fn finish_and_clear(&self) {
self.bar.finish_and_clear();
}
pub fn println(&self, message: &str) {
if self.bar.is_hidden() {
eprintln!("{}", message);
} else {
self.bar.println(message);
}
}
pub fn suspend<R, F: FnOnce() -> R>(&self, f: F) -> R {
if self.bar.is_hidden() {
f()
} else {
self.bar.suspend(f)
}
}
}
pub struct StepProgress {
bar: ProgressBar,
}
impl StepProgress {
pub fn new(total: u64, prefix: &str) -> Self {
let bar = if atty_stderr() {
let pb = ProgressBar::new(total);
pb.set_style(
ProgressStyle::with_template("{prefix} [{bar:30.cyan/dim}] {pos}/{len} {msg}")
.unwrap()
.progress_chars("━━╸"),
);
pb.set_prefix(prefix.to_string());
pb
} else {
eprintln!("{} (0/{})", prefix, total);
ProgressBar::hidden()
};
Self { bar }
}
pub fn inc(&self, message: &str) {
self.bar.set_message(message.to_string());
self.bar.inc(1);
}
pub fn finish(&self, message: &str) {
if !self.bar.is_hidden() {
self.bar.finish_with_message(format!("✓ {}", message));
}
}
}
fn atty_stderr() -> bool {
use std::io::IsTerminal;
std::io::stderr().is_terminal()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_spinner_create_and_finish() {
let sp = Spinner::new("Testing...");
sp.set_message("Updated");
sp.finish("Done");
}
#[test]
fn test_spinner_finish_and_clear() {
let sp = Spinner::new("Testing...");
sp.finish_and_clear();
}
#[test]
fn test_spinner_finish_warn() {
let sp = Spinner::new("Testing...");
sp.finish_warn("Warning");
}
#[test]
fn test_step_progress_create_and_finish() {
let prog = StepProgress::new(3, "Processing");
prog.inc("Step 1");
prog.inc("Step 2");
prog.inc("Step 3");
prog.finish("Done");
}
#[test]
fn test_spinner_multiple_set_message() {
let sp = Spinner::new("Initial");
sp.set_message("First update");
sp.set_message("Second update");
sp.set_message("Third update");
sp.finish("Complete");
}
#[test]
fn test_step_progress_multiple_inc() {
let prog = StepProgress::new(5, "Processing");
prog.inc("Step 1");
prog.inc("Step 2");
prog.inc("Step 3");
prog.inc("Step 4");
prog.inc("Step 5");
prog.finish("Done");
}
#[test]
fn test_step_progress_single_step() {
let prog = StepProgress::new(1, "Single");
prog.inc("Only step");
prog.finish("Complete");
}
#[test]
fn test_step_progress_large_total() {
let prog = StepProgress::new(100, "Large");
for i in 1..=100 {
prog.inc(&format!("Step {}", i));
}
prog.finish("Complete");
}
#[test]
fn test_step_progress_zero_total() {
let prog = StepProgress::new(0, "Empty");
prog.finish("Complete");
}
#[test]
fn test_spinner_finish_warn_with_message() {
let sp = Spinner::new("Warning test");
sp.finish_warn("Something went wrong");
}
#[test]
fn test_spinner_finish_warn_multiple_calls() {
let sp = Spinner::new("Test");
sp.finish_warn("First warning");
sp.finish_warn("Second warning");
}
#[test]
fn test_spinner_println() {
let sp = Spinner::new("Working...");
sp.println("A message above the spinner");
sp.finish("Done");
}
#[test]
fn test_spinner_suspend() {
let sp = Spinner::new("Working...");
sp.suspend(|| {
eprintln!("Suspended output");
});
sp.finish("Done");
}
}