use std::io::{self, Write};
#[derive(Debug)]
pub struct ProgressBar {
total: u64,
current: u64,
width: usize,
message: String,
enabled: bool,
}
impl ProgressBar {
pub fn new(total: u64) -> Self {
Self {
total,
current: 0,
width: 40,
message: String::new(),
enabled: true,
}
}
pub fn with_width(mut self, width: usize) -> Self {
self.width = width;
self
}
pub fn with_enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn set_message(&mut self, message: impl Into<String>) {
self.message = message.into();
self.render();
}
pub fn inc(&mut self, amount: u64) {
self.current = (self.current + amount).min(self.total);
self.render();
}
pub fn set(&mut self, current: u64) {
self.current = current.min(self.total);
self.render();
}
pub fn percentage(&self) -> f64 {
if self.total == 0 {
return 100.0;
}
(self.current as f64 / self.total as f64) * 100.0
}
pub fn finish(&mut self) {
self.current = self.total;
self.render();
if self.enabled {
println!();
}
}
pub fn finish_with_message(&mut self, message: impl Into<String>) {
self.message = message.into();
self.finish();
}
fn render(&self) {
if !self.enabled {
return;
}
let percentage = self.percentage();
let filled = (percentage / 100.0 * self.width as f64) as usize;
let empty = self.width - filled;
let bar = format!(
"\r[{}{}] {:>5.1}% {}",
"█".repeat(filled),
"░".repeat(empty),
percentage,
self.message
);
print!("{bar}");
let _ = io::stdout().flush();
}
}
#[derive(Debug)]
pub struct Spinner {
frames: Vec<&'static str>,
current_frame: usize,
message: String,
enabled: bool,
}
impl Default for Spinner {
fn default() -> Self {
Self::new()
}
}
impl Spinner {
pub fn new() -> Self {
Self {
frames: vec!["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
current_frame: 0,
message: String::new(),
enabled: true,
}
}
pub fn with_enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn set_message(&mut self, message: impl Into<String>) {
self.message = message.into();
}
pub fn tick(&mut self) {
if !self.enabled {
return;
}
self.current_frame = (self.current_frame + 1) % self.frames.len();
print!("\r{} {}", self.frames[self.current_frame], self.message);
let _ = io::stdout().flush();
}
pub fn finish_with_message(&mut self, message: impl Into<String>) {
if self.enabled {
println!("\r✓ {}", message.into());
}
}
pub fn finish_with_error(&mut self, message: impl Into<String>) {
if self.enabled {
println!("\r✗ {}", message.into());
}
}
}
#[derive(Debug)]
pub struct StepTracker {
steps: Vec<String>,
current: usize,
enabled: bool,
}
impl StepTracker {
pub fn new(steps: Vec<impl Into<String>>) -> Self {
Self {
steps: steps.into_iter().map(Into::into).collect(),
current: 0,
enabled: true,
}
}
pub fn with_enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn next_step(&mut self) {
if self.current < self.steps.len() {
if self.enabled {
println!(
"[{}/{}] {}...",
self.current + 1,
self.steps.len(),
self.steps[self.current]
);
}
self.current += 1;
}
}
pub fn complete_step(&self, message: impl Into<String>) {
if self.enabled {
println!(" ✓ {}", message.into());
}
}
pub fn fail_step(&self, message: impl Into<String>) {
if self.enabled {
println!(" ✗ {}", message.into());
}
}
pub fn is_complete(&self) -> bool {
self.current >= self.steps.len()
}
pub fn total_steps(&self) -> usize {
self.steps.len()
}
pub fn current_step(&self) -> usize {
self.current
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_progress_bar_percentage() {
let mut bar = ProgressBar::new(100).with_enabled(false);
assert_eq!(bar.percentage(), 0.0);
bar.set(50);
assert_eq!(bar.percentage(), 50.0);
bar.set(100);
assert_eq!(bar.percentage(), 100.0);
}
#[test]
fn test_progress_bar_increment() {
let mut bar = ProgressBar::new(100).with_enabled(false);
bar.inc(25);
assert_eq!(bar.current, 25);
bar.inc(25);
assert_eq!(bar.current, 50);
}
#[test]
fn test_progress_bar_overflow_protection() {
let mut bar = ProgressBar::new(100).with_enabled(false);
bar.set(200);
assert_eq!(bar.current, 100);
assert_eq!(bar.percentage(), 100.0);
}
#[test]
fn test_progress_bar_zero_total() {
let bar = ProgressBar::new(0).with_enabled(false);
assert_eq!(bar.percentage(), 100.0);
}
#[test]
fn test_step_tracker_progression() {
let mut tracker = StepTracker::new(vec!["Step 1", "Step 2", "Step 3"]).with_enabled(false);
assert_eq!(tracker.total_steps(), 3);
assert_eq!(tracker.current_step(), 0);
assert!(!tracker.is_complete());
tracker.next_step();
assert_eq!(tracker.current_step(), 1);
tracker.next_step();
tracker.next_step();
assert!(tracker.is_complete());
}
#[test]
fn test_spinner_frames() {
let spinner = Spinner::new();
assert!(!spinner.frames.is_empty());
}
#[test]
fn test_spinner_default() {
let spinner = Spinner::default();
assert_eq!(spinner.frames.len(), 10);
assert!(spinner.enabled);
}
#[test]
fn test_spinner_disabled() {
let spinner = Spinner::new().with_enabled(false);
assert!(!spinner.enabled);
}
#[test]
fn test_spinner_message() {
let mut spinner = Spinner::new().with_enabled(false);
spinner.set_message("Loading...");
assert_eq!(spinner.message, "Loading...");
}
#[test]
fn test_spinner_tick_cycles_frames() {
let mut spinner = Spinner::new();
spinner.enabled = false; spinner.current_frame = (spinner.current_frame + 1) % spinner.frames.len();
assert_eq!(spinner.current_frame, 1);
spinner.current_frame = (spinner.current_frame + 1) % spinner.frames.len();
assert_eq!(spinner.current_frame, 2);
}
#[test]
fn test_spinner_tick_wraps_around() {
let spinner = Spinner::new();
let frame_count = spinner.frames.len();
let after_15_ticks = 15 % frame_count;
assert_eq!(after_15_ticks, 5);
}
#[test]
fn test_progress_bar_width() {
let bar = ProgressBar::new(100).with_width(20);
assert_eq!(bar.width, 20);
}
#[test]
fn test_progress_bar_message() {
let mut bar = ProgressBar::new(100).with_enabled(false);
bar.set_message("Downloading");
assert_eq!(bar.message, "Downloading");
}
#[test]
fn test_progress_bar_finish() {
let mut bar = ProgressBar::new(100).with_enabled(false);
bar.set(50);
bar.finish();
assert_eq!(bar.current, 100);
}
#[test]
fn test_progress_bar_finish_with_message() {
let mut bar = ProgressBar::new(100).with_enabled(false);
bar.finish_with_message("Done!");
assert_eq!(bar.message, "Done!");
assert_eq!(bar.current, 100);
}
#[test]
fn test_step_tracker_empty() {
let tracker = StepTracker::new(Vec::<String>::new()).with_enabled(false);
assert_eq!(tracker.total_steps(), 0);
assert!(tracker.is_complete());
}
#[test]
fn test_step_tracker_current_step_1_indexed() {
let mut tracker = StepTracker::new(vec!["A", "B"]).with_enabled(false);
assert_eq!(tracker.current_step(), 0);
tracker.next_step();
assert_eq!(tracker.current_step(), 1);
tracker.next_step();
assert_eq!(tracker.current_step(), 2);
}
#[test]
fn test_step_tracker_overflow_protection() {
let mut tracker = StepTracker::new(vec!["Only"]).with_enabled(false);
tracker.next_step();
tracker.next_step(); tracker.next_step();
assert!(tracker.is_complete());
assert_eq!(tracker.current_step(), 1);
}
}