#![allow(clippy::nursery)]
#![warn(clippy::pedantic)]
use colored::Colorize;
use std::borrow::Cow;
use std::io::Write;
use std::sync::{atomic::AtomicBool, Arc};
use std::thread::sleep;
use std::thread::{self, JoinHandle};
use std::time::Duration;
pub mod spinners;
mod streams;
mod utils;
use spinners::SpinnerFrames;
pub use streams::Streams;
pub use utils::Color;
use utils::{colorize, delete_last_line};
pub struct Spinner {
thread_handle: Option<JoinHandle<()>>,
still_spinning: Arc<AtomicBool>,
spinner_frames: SpinnerFrames,
msg: Cow<'static, str>,
stream: Streams,
color: Option<Color>,
}
#[macro_export]
macro_rules! spinner {
( [ $( $frame:expr ),* ], $interval:expr ) => {
spinners::SpinnerFrames {
interval: $interval,
frames: vec![$($frame),*]
}
};
}
impl Spinner {
pub fn new<S, T, U>(spinner_type: S, msg: T, color: U) -> Self
where
S: Into<SpinnerFrames>,
T: Into<Cow<'static, str>>,
U: Into<Option<Color>>,
{
Self::new_with_stream(spinner_type, msg, color, Streams::default())
}
pub fn new_with_stream<S, T, U>(spinner_type: S, msg: T, color: U, stream: Streams) -> Self
where
S: Into<SpinnerFrames>,
T: Into<Cow<'static, str>>,
U: Into<Option<Color>>,
{
let still_spinning = Arc::new(AtomicBool::new(true));
let spinner_frames = spinner_type.into();
let msg = msg.into();
let color = color.into();
let handle = thread::spawn({
let still_spinning = Arc::clone(&still_spinning);
let spinner_frames = spinner_frames.clone();
let msg = msg.clone();
move || {
let frames = spinner_frames
.frames
.iter()
.cycle()
.take_while(|_| still_spinning.load(std::sync::atomic::Ordering::Relaxed));
let mut last_length = 0;
for frame in frames {
let frame_str = format!("{} {}", colorize(color, frame), msg);
delete_last_line(last_length, stream);
last_length = frame_str.bytes().len();
write!(stream, "{frame_str}");
stream
.get_stream()
.flush()
.expect("error: failed to flush stream");
thread::sleep(std::time::Duration::from_millis(
u64::from(spinner_frames.interval)
));
}
delete_last_line(last_length, stream);
}
});
Self {
thread_handle: Some(handle),
still_spinning,
spinner_frames,
msg,
stream,
color,
}
}
pub fn stop(&mut self) {
self.stop_spinner_thread();
writeln!(self.stream, "{}", self.msg);
}
pub fn stop_with_message(&mut self, msg: &str) {
self.stop_spinner_thread();
writeln!(self.stream, "{msg}");
}
pub fn stop_and_persist(&mut self, symbol: &str, msg: &str) {
self.stop_spinner_thread();
writeln!(self.stream, "{symbol} {msg}");
}
pub fn success(&mut self, msg: &str) {
self.stop_spinner_thread();
writeln!(self.stream, "{} {}", colorize(Some(Color::Green), "✓").bold(), msg);
}
pub fn fail(&mut self, msg: &str) {
self.stop_spinner_thread();
writeln!(self.stream, "{} {}", colorize(Some(Color::Red), "✗").bold(), msg);
}
pub fn warn(&mut self, msg: &str) {
self.stop_spinner_thread();
writeln!(self.stream, "{} {}", colorize(Some(Color::Yellow), "⚠").bold(), msg);
}
pub fn info(&mut self, msg: &str) {
self.stop_spinner_thread();
writeln!(self.stream, "{} {}", colorize(Some(Color::Blue), "ℹ").bold(), msg);
}
pub fn update<S, T, U>(&mut self, spinner: S, msg: T, color: U)
where
S: Into<SpinnerFrames>,
T: Into<Cow<'static, str>>,
U: Into<Option<Color>>,
{
self.stop_spinner_thread();
let _replaced = std::mem::replace(
self,
Self::new_with_stream(spinner, msg, color, self.stream),
);
}
pub fn update_text<T>(&mut self, msg: T)
where
T: Into<Cow<'static, str>>,
{
self.stop_spinner_thread();
let _replaced = std::mem::replace(
self,
Self::new_with_stream(self.spinner_frames.clone(), msg, self.color, self.stream),
);
}
pub fn update_after_time<T>(&mut self, updated_msg: T, duration: Duration)
where
T: Into<Cow<'static, str>>
{
sleep(duration);
self.stop_spinner_thread();
let _replaced = std::mem::replace(
self,
Self::new_with_stream(self.spinner_frames.clone(), updated_msg, self.color, self.stream),
);
}
pub fn clear(&mut self) {
self.stop_spinner_thread();
}
fn stop_spinner_thread(&mut self) {
self.still_spinning
.store(false, std::sync::atomic::Ordering::Relaxed);
self.thread_handle
.take()
.expect("Stopping the spinner thread should only happen once.")
.join()
.expect("Thread to join.");
}
}