use std::fmt::{Display, Formatter};
use std::io::{self, Write};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, RwLock};
use std::thread;
use std::time::{Duration, Instant};
use crossterm::{
cursor,
style::{self, Color, SetForegroundColor, Stylize},
terminal::{Clear, ClearType},
ExecutableCommand, QueueableCommand,
};
use super::*;
#[derive(Clone, Debug)]
pub struct ProgressContextData {
bars: Vec<ProgressBarData>,
spinners: Vec<SpinnerData>,
active: bool,
}
#[derive(Clone, Debug)]
struct ProgressBarData {
id: String,
current: usize,
total: usize,
message: String,
}
#[derive(Clone, Debug)]
struct SpinnerData {
id: String,
message: String,
active: bool,
}
pub struct ProgressManager {
data: Arc<RwLock<ProgressContextData>>,
}
impl ProgressManager {
pub fn new() -> Self {
let data = Arc::new(RwLock::new(ProgressContextData {
bars: Vec::new(),
spinners: Vec::new(),
active: true,
}));
set!(progress_data => data.clone());
Self { data }
}
pub fn create_bar(&self, id: impl Into<String>, total: usize) -> ProgressBar {
let id = id.into();
let mut data = self.data.write().unwrap();
data.bars.push(ProgressBarData {
id: id.clone(),
current: 0,
total,
message: String::new(),
});
ProgressBar::new(id, total, self.data.clone())
}
pub fn create_spinner(&self, id: impl Into<String>, message: impl Into<String>) -> Spinner {
let id = id.into();
let message = message.into();
let mut data = self.data.write().unwrap();
data.spinners.push(SpinnerData {
id: id.clone(),
message: message.clone(),
active: true,
});
Spinner::new(id, message, self.data.clone())
}
}
#[derive(Clone)]
pub struct ProgressStyle {
pub bar_chars: String,
pub empty_chars: String,
pub prefix: String,
pub suffix: String,
pub colors: Vec<Color>,
}
impl Default for ProgressStyle {
fn default() -> Self {
Self {
bar_chars: "█".to_string(),
empty_chars: "░".to_string(),
prefix: "[".to_string(),
suffix: "]".to_string(),
colors: vec![
Color::Cyan,
Color::Magenta,
Color::Blue,
Color::Green,
],
}
}
}
#[derive(Clone)]
pub struct SpinnerStyle {
pub frames: Vec<String>,
pub interval: Duration,
pub colors: Vec<Color>,
}
impl Default for SpinnerStyle {
fn default() -> Self {
Self {
frames: vec![
"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏",
].into_iter().map(String::from).collect(),
interval: Duration::from_millis(80),
colors: vec![
Color::Cyan,
Color::Magenta,
Color::Blue,
Color::Green,
],
}
}
}
pub struct ProgressBar {
id: String,
total: usize,
style: ProgressStyle,
data: Arc<RwLock<ProgressContextData>>,
}
impl ProgressBar {
fn new(id: String, total: usize, data: Arc<RwLock<ProgressContextData>>) -> Self {
Self {
id,
total,
style: ProgressStyle::default(),
data,
}
}
pub fn with_style(mut self, style: ProgressStyle) -> Self {
self.style = style;
self
}
pub fn increment(&self, amount: usize) {
let mut data = self.data.write().unwrap();
if let Some(bar) = data.bars.iter_mut().find(|b| b.id == self.id) {
bar.current = (bar.current + amount).min(bar.total);
drop(data);
self.draw();
}
}
pub fn set_message(&self, msg: impl Into<String>) {
let mut data = self.data.write().unwrap();
if let Some(bar) = data.bars.iter_mut().find(|b| b.id == self.id) {
bar.message = msg.into();
drop(data);
self.draw();
}
}
pub fn finish(&self) {
let mut data = self.data.write().unwrap();
if let Some(bar) = data.bars.iter_mut().find(|b| b.id == self.id) {
bar.current = bar.total;
drop(data);
self.draw();
println!();
}
}
fn draw(&self) {
let data = self.data.read().unwrap();
if !data.active {
return;
}
if let Some(bar) = data.bars.iter().find(|b| b.id == self.id) {
let width = 40;
let progress = bar.current as f64 / bar.total as f64;
let filled = (width as f64 * progress) as usize;
let empty = width - filled;
let mut stdout = io::stdout();
stdout.queue(Clear(ClearType::CurrentLine)).unwrap()
.queue(cursor::MoveToColumn(0)).unwrap();
let colors = self.style.colors.clone();
let remaining_colors = vec![Color::White; colors.len() - filled];
let all_colors = [colors, remaining_colors].concat();
print!("{}", self.style.prefix);
print!("{}", self.style.bar_chars.repeat(filled));
print!("{}", self.style.empty_chars.repeat(empty));
print!("{} ", self.style.suffix);
print!(
"{:.1}% [{}/{}] {}",
progress * 100.0,
bar.current,
bar.total,
bar.message
);
stdout.flush().unwrap();
}
}
}
pub struct Spinner {
id: String,
style: SpinnerStyle,
data: Arc<RwLock<ProgressContextData>>,
handle: Option<thread::JoinHandle<()>>,
}
impl Spinner {
fn new(id: String, message: String, data: Arc<RwLock<ProgressContextData>>) -> Self {
let style = SpinnerStyle::default();
let style_clone = style.clone();
let spinner_data = data.clone();
let spinner_id = id.clone();
let handle = thread::spawn(move || {
let mut frame_idx = 0;
let mut color_idx = 0;
while {
let data = spinner_data.read().unwrap();
data.active && data.spinners.iter().any(|s| s.id == spinner_id && s.active)
} {
let data = spinner_data.read().unwrap();
if let Some(spinner) = data.spinners.iter().find(|s| s.id == spinner_id) {
let frame = &style.frames[frame_idx];
let color = style.colors[color_idx];
let mut stdout = io::stdout();
stdout.queue(Clear(ClearType::CurrentLine)).unwrap()
.queue(cursor::MoveToColumn(0)).unwrap()
.queue(SetForegroundColor(color)).unwrap();
print!("{} {}", frame, spinner.message);
stdout.flush().unwrap();
frame_idx = (frame_idx + 1) % style.frames.len();
color_idx = (color_idx + 1) % style.colors.len();
}
drop(data);
thread::sleep(style.interval);
}
});
Self {
id,
style: style_clone,
data,
handle: Some(handle),
}
}
pub fn with_style(mut self, style: SpinnerStyle) -> Self {
self.style = style;
self
}
pub fn set_message(&self, msg: impl Into<String>) {
let mut data = self.data.write().unwrap();
if let Some(spinner) = data.spinners.iter_mut().find(|s| s.id == self.id) {
spinner.message = msg.into();
}
}
pub fn finish(&mut self) {
let mut data = self.data.write().unwrap();
if let Some(spinner) = data.spinners.iter_mut().find(|s| s.id == self.id) {
spinner.active = false;
}
drop(data);
if let Some(handle) = self.handle.take() {
handle.join().unwrap();
}
println!();
}
}
impl Drop for Spinner {
fn drop(&mut self) {
self.finish();
}
}
pub struct MultiProgress {
bars: Vec<ProgressBar>,
current_step: usize,
style: ProgressStyle,
data: Arc<RwLock<ProgressContextData>>,
}
impl MultiProgress {
pub fn new(steps: Vec<(String, usize)>, data: Arc<RwLock<ProgressContextData>>) -> Self {
let bars = steps
.into_iter()
.map(|(name, total)| {
ProgressBar::new(name, total, data.clone())
.with_style(ProgressStyle::default())
})
.collect();
Self {
bars,
current_step: 0,
style: ProgressStyle::default(),
data,
}
}
pub fn increment(&mut self, amount: usize) {
if self.current_step < self.bars.len() {
self.bars[self.current_step].increment(amount);
let data = self.data.read().unwrap();
if let Some(bar) = data.bars.iter().find(|b| b.id == self.bars[self.current_step].id) {
if bar.current >= bar.total {
self.current_step += 1;
}
}
}
}
pub fn set_message(&mut self, msg: impl Into<String>) {
if self.current_step < self.bars.len() {
self.bars[self.current_step].set_message(msg);
}
}
pub fn finish(&mut self) {
for bar in &self.bars {
bar.finish();
}
}
}
pub fn download_progress(total_bytes: usize) -> ProgressBar {
let manager = ProgressManager::new();
let style = ProgressStyle {
bar_chars: "━".to_string(),
empty_chars: "╺".to_string(),
prefix: "╭".to_string(),
suffix: "╮".to_string(),
colors: vec![Color::Cyan],
};
manager.create_bar("download", total_bytes)
.with_style(style)
}
pub fn processing_spinner(message: impl Into<String>) -> Spinner {
let manager = ProgressManager::new();
let style = SpinnerStyle {
frames: vec!["◢", "◣", "◤", "◥"]
.into_iter()
.map(String::from)
.collect(),
interval: Duration::from_millis(100),
colors: vec![Color::Cyan, Color::Blue],
};
manager.create_spinner("processing", message)
.with_style(style)
}
pub fn installation_progress() -> MultiProgress {
let manager = ProgressManager::new();
let steps = vec![
("download".to_string(), 100),
("extract".to_string(), 100),
("install".to_string(), 100),
("configure".to_string(), 100),
];
MultiProgress::new(steps, manager.data)
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
use std::time::Duration;
#[test]
fn test_progress_bar() {
let manager = ProgressManager::new();
let bar = manager.create_bar("test", 100);
for i in 0..=100 {
bar.increment(1);
bar.set_message(format!("Processing {}", i));
thread::sleep(Duration::from_millis(10));
}
bar.finish();
}
#[test]
fn test_spinner() {
let manager = ProgressManager::new();
let mut spinner = manager.create_spinner("test", "Processing...");
thread::sleep(Duration::from_secs(2));
spinner.set_message("Almost done...");
thread::sleep(Duration::from_secs(2));
spinner.finish();
}
#[test]
fn test_multi_progress() {
let manager = ProgressManager::new();
let steps = vec![
("Step 1".to_string(), 100),
("Step 2".to_string(), 100),
("Step 3".to_string(), 100),
];
let mut multi = MultiProgress::new(steps, manager.data);
for _ in 0..3 {
for i in 0..=100 {
multi.increment(1);
multi.set_message(format!("Processing step {}", i));
thread::sleep(Duration::from_millis(10));
}
}
multi.finish();
}
}
#[macro_export]
macro_rules! progress_bar {
($total:expr) => {{
let manager = $crate::ProgressManager::new();
manager.create_bar("default", $total)
}};
($id:expr, $total:expr) => {{
let manager = $crate::ProgressManager::new();
manager.create_bar($id, $total)
}};
}
#[macro_export]
macro_rules! spinner {
($message:expr) => {{
let manager = $crate::ProgressManager::new();
manager.create_spinner("default", $message)
}};
($id:expr, $message:expr) => {{
let manager = $crate::ProgressManager::new();
manager.create_spinner($id, $message)
}};
}