use std::io::{self, Write};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use crate::detect;
pub const SPINNER_FRAMES: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
const DOTS_FRAMES: &[&str] = &[".", "..", "...", "....", "....."];
pub struct Spinner {
running: Arc<AtomicBool>,
handle: Option<thread::JoinHandle<()>>,
}
impl Spinner {
pub fn start(message: &str) -> Self {
let running = Arc::new(AtomicBool::new(true));
if !detect::should_style() || !detect::is_tty_stderr() {
eprintln!("{message}...");
return Self {
running,
handle: None,
};
}
let running_clone = running.clone();
let msg = message.to_string();
let handle = thread::spawn(move || {
let frames = if detect::is_accessible_mode() {
DOTS_FRAMES
} else {
SPINNER_FRAMES
};
let mut idx = 0;
while running_clone.load(Ordering::Relaxed) {
let frame = frames[idx % frames.len()];
eprint!("\r{frame} {msg} ");
let _ = io::stderr().flush();
idx += 1;
thread::sleep(Duration::from_millis(80));
}
eprint!("\r{}\r", " ".repeat(msg.len() + 10));
let _ = io::stderr().flush();
});
Self {
running,
handle: Some(handle),
}
}
pub fn stop_with_message(self, message: &str) {
self.running.store(false, Ordering::Relaxed);
if let Some(handle) = self.handle {
let _ = handle.join();
}
eprintln!("{message}");
}
pub fn stop(self) {
self.running.store(false, Ordering::Relaxed);
if let Some(handle) = self.handle {
let _ = handle.join();
}
}
}
impl Drop for Spinner {
fn drop(&mut self) {
self.running.store(false, Ordering::Relaxed);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn spinner_frames_are_not_empty() {
assert!(!SPINNER_FRAMES.is_empty());
}
#[test]
fn spinner_frames_are_single_width() {
for frame in SPINNER_FRAMES {
assert_eq!(frame.chars().count(), 1);
}
}
}