use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
pub struct Spinner {
message: String,
running: Arc<AtomicBool>,
handle: Option<tokio::task::JoinHandle<()>>,
}
impl Spinner {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
running: Arc::new(AtomicBool::new(false)),
handle: None,
}
}
pub fn start(&mut self) {
if self.running.load(Ordering::SeqCst) {
return; }
self.running.store(true, Ordering::SeqCst);
let running = Arc::clone(&self.running);
let message = self.message.clone();
let handle = tokio::spawn(async move {
let frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
let mut frame_idx = 0;
while running.load(Ordering::SeqCst) {
eprint!("\r{} {}", frames[frame_idx], message);
frame_idx = (frame_idx + 1) % frames.len();
tokio::time::sleep(Duration::from_millis(80)).await;
}
eprint!("\r{}\r", " ".repeat(message.len() + 10)); });
self.handle = Some(handle);
}
pub fn stop(&mut self) {
self.running.store(false, Ordering::SeqCst);
if let Some(handle) = self.handle.take() {
std::thread::sleep(Duration::from_millis(100));
handle.abort();
}
}
pub fn finish_with_message(&mut self, message: &str) {
self.stop();
eprintln!("✓ {}", message);
}
}
impl Drop for Spinner {
fn drop(&mut self) {
self.stop();
}
}
pub struct ProgressBar {
message: String,
total: u64,
current: u64,
width: usize,
}
impl ProgressBar {
pub fn new(message: impl Into<String>, total: u64) -> Self {
Self {
message: message.into(),
total,
current: 0,
width: 40,
}
}
pub fn set_progress(&mut self, current: u64) {
self.current = current.min(self.total);
self.render();
}
pub fn inc(&mut self) {
self.set_progress(self.current + 1);
}
pub fn inc_by(&mut self, n: u64) {
self.set_progress(self.current + n);
}
fn render(&self) {
let percentage = if self.total > 0 {
(self.current as f64 / self.total as f64 * 100.0) as u64
} else {
0
};
let filled = if self.total > 0 {
(self.current as f64 / self.total as f64 * self.width as f64) as usize
} else {
0
};
let empty = self.width.saturating_sub(filled);
eprint!(
"\r{} [{}{}] {}/{} ({}%)",
self.message,
"█".repeat(filled),
"░".repeat(empty),
self.current,
self.total,
percentage
);
if self.current >= self.total {
eprintln!(); }
}
pub fn finish(&mut self) {
self.set_progress(self.total);
}
pub fn finish_with_message(&mut self, message: &str) {
self.finish();
eprintln!("✓ {}", message);
}
}
pub async fn with_spinner<F, T>(message: impl Into<String>, future: F) -> T
where
F: std::future::Future<Output = T>,
{
let mut spinner = Spinner::new(message);
spinner.start();
let result = future.await;
spinner.stop();
result
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_spinner_creation() {
let spinner = Spinner::new("Testing");
assert!(!spinner.running.load(Ordering::SeqCst));
}
#[tokio::test]
async fn test_progress_bar() {
let mut bar = ProgressBar::new("Testing", 100);
assert_eq!(bar.current, 0);
assert_eq!(bar.total, 100);
bar.set_progress(50);
assert_eq!(bar.current, 50);
bar.inc();
assert_eq!(bar.current, 51);
bar.inc_by(10);
assert_eq!(bar.current, 61);
bar.finish();
assert_eq!(bar.current, 100);
}
#[tokio::test]
async fn test_with_spinner() {
let result = with_spinner("Processing", async {
tokio::time::sleep(Duration::from_millis(100)).await;
42
})
.await;
assert_eq!(result, 42);
}
}