use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
const FRAMES: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
const FRAME_MS: u64 = 80;
pub struct Spinner {
running: Arc<AtomicBool>,
handle: Option<tokio::task::JoinHandle<()>>,
}
impl Spinner {
pub fn start(msg: impl Into<String>) -> Self {
let msg = msg.into();
if !console::Term::stderr().is_term() {
return Spinner {
running: Arc::new(AtomicBool::new(false)),
handle: None,
};
}
let running = Arc::new(AtomicBool::new(true));
let running_clone = running.clone();
let handle = tokio::spawn(async move {
use std::io::Write;
let mut frame_idx = 0usize;
let styled_msg = format!("{}", console::style(&msg).dim());
while running_clone.load(Ordering::Relaxed) {
let frame = FRAMES[frame_idx % FRAMES.len()];
let line = format!(
"\r {} {}",
console::style(frame).cyan(),
styled_msg,
);
eprint!("{}", line);
eprint!(" ");
std::io::stderr().flush().ok();
frame_idx += 1;
tokio::time::sleep(std::time::Duration::from_millis(FRAME_MS)).await;
}
eprint!("\r{}\r", " ".repeat(60));
std::io::stderr().flush().ok();
});
Spinner {
running,
handle: Some(handle),
}
}
pub fn stop(mut self) {
self.cancel();
}
fn cancel(&mut self) {
self.running.store(false, Ordering::Relaxed);
if let Some(handle) = self.handle.take() {
handle.abort();
use std::io::Write;
if console::Term::stderr().is_term() {
eprint!("\r{}\r", " ".repeat(60));
std::io::stderr().flush().ok();
}
}
}
}
impl Drop for Spinner {
fn drop(&mut self) {
self.cancel();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_spinner_non_tty_noop() {
let spinner = Spinner::start("testing…");
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
spinner.stop();
}
#[tokio::test]
async fn test_spinner_drop_safety() {
let spinner = Spinner::start("drop test");
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
drop(spinner);
}
}