use std::io::{self, Write};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread::{self, JoinHandle};
use std::time::Duration;
const ACCENT: (u8, u8, u8) = (224, 64, 127); const GREEN: (u8, u8, u8) = (47, 211, 107);
const RED: (u8, u8, u8) = (255, 77, 109);
const BEAK: (u8, u8, u8) = (246, 169, 60);
const TICK: Duration = Duration::from_millis(130);
#[derive(Clone, Copy)]
pub enum Style {
Thinking,
Flock,
}
pub struct Spinner {
running: Arc<AtomicBool>,
handle: Option<JoinHandle<()>>,
style: Style,
label: String,
lines: usize,
}
impl Spinner {
pub fn start(label: &str, style: Style) -> Self {
let running = Arc::new(AtomicBool::new(true));
let r = running.clone();
let lbl = label.to_string();
let lines = match style {
Style::Thinking => 7, Style::Flock => 4, };
let c = use_color();
let handle = thread::spawn(move || {
hide_cursor();
let mut t: u64 = 0;
let mut first = true;
while r.load(Ordering::Relaxed) {
let frame = match style {
Style::Thinking => thinking_build(t, &lbl, c),
Style::Flock => flock_cross(t, &lbl, c),
};
draw(&frame, first);
first = false;
t = t.wrapping_add(1);
thread::sleep(TICK);
}
});
Spinner {
running,
handle: Some(handle),
style,
label: label.to_string(),
lines,
}
}
pub fn run<T, E: std::fmt::Display>(
label: &str,
style: Style,
f: impl FnOnce() -> Result<T, E>,
) -> Result<T, E> {
let sp = Spinner::start(label, style);
match f() {
Ok(v) => {
sp.success(&format!("{label} done"));
Ok(v)
}
Err(e) => {
let msg = e.to_string();
sp.fail(&msg);
Err(e)
}
}
}
pub fn success(mut self, msg: &str) {
self.halt();
let c = use_color();
match self.style {
Style::Thinking => {
for k in 0..4 {
draw(&thinking_finish(k, msg, true, c), false);
thread::sleep(TICK);
}
}
Style::Flock => {
for k in 0..5 {
draw(&flock_v(k, msg, false, true, c), false);
thread::sleep(TICK);
}
draw(&flock_v(0, msg, true, true, c), false);
}
}
show_cursor();
self.handle = None; }
pub fn fail(mut self, msg: &str) {
self.halt();
let c = use_color();
match self.style {
Style::Thinking => {
for k in 0..4 {
draw(&thinking_finish(k, msg, false, c), false);
thread::sleep(TICK);
}
}
Style::Flock => {
for k in 0..5 {
draw(&flock_v(k, msg, false, false, c), false);
thread::sleep(TICK);
}
draw(&flock_v(0, msg, true, false, c), false);
}
}
show_cursor();
self.handle = None;
}
pub fn stop(mut self) {
self.halt();
self.clear();
show_cursor();
self.handle = None;
}
fn halt(&mut self) {
self.running.store(false, Ordering::Relaxed);
if let Some(h) = self.handle.take() {
let _ = h.join();
}
}
fn clear(&self) {
let mut o = io::stdout();
let n = self.lines;
let _ = write!(o, "\r\x1b[{n}A");
for _ in 0..n {
let _ = write!(o, "\x1b[2K\x1b[1B");
}
let _ = write!(o, "\x1b[{n}A");
let _ = o.flush();
let _ = &self.label;
}
}
impl Drop for Spinner {
fn drop(&mut self) {
if self.handle.is_some() {
self.halt();
self.clear();
show_cursor();
}
}
}
const CREST: [char; 4] = ['✳', '❉', '✲', '✦'];
const DOTS: [&str; 4] = [" ", ". ", ".. ", "..."];
fn head_calm(eye: char, c: bool) -> String {
format!(
"{}{}{}{}{}",
paint("(", ACCENT, c),
paint(&eye.to_string(), ACCENT, c),
paint("ᴗ", BEAK, c),
paint(&eye.to_string(), ACCENT, c),
paint(")", ACCENT, c),
)
}
fn head_happy(c: bool) -> String {
format!(
"{}{}{}{}{}",
paint("(", ACCENT, c),
paint("^", ACCENT, c),
paint("ᴗ", BEAK, c),
paint("^", ACCENT, c),
paint(")", ACCENT, c),
)
}
fn head_cool(c: bool) -> String {
format!(
"{}{}{}{}{}",
paint("(", ACCENT, c),
paint("¬", ACCENT, c),
paint("ᴗ", BEAK, c),
paint("¬", ACCENT, c),
paint(")", ACCENT, c),
)
}
fn body_rest(c: bool) -> String {
paint("<(___)>", ACCENT, c)
}
fn body_hop(c: bool) -> String {
paint(" (___) ", ACCENT, c)
} fn feet(c: bool) -> String {
paint(" ^ ^ ", ACCENT, c)
}
fn thinking_build(t: u64, label: &str, c: bool) -> Vec<String> {
let spk = CREST[(t / 3) as usize % CREST.len()];
let eye = if t.is_multiple_of(14) { '-' } else { '•' };
let d = DOTS[(t / 3) as usize % DOTS.len()];
let bob = if (t / 3).is_multiple_of(2) { "" } else { " " };
vec![
format!("{bob} {}", paint(&format!(" _{spk}_"), ACCENT, c)),
format!(
"{bob} {} {}",
head_calm(eye, c),
paint(&format!("{label}{d}"), DIM, c)
),
format!("{bob} {}", body_rest(c)),
format!("{bob} {}", feet(c)),
]
}
fn thinking_finish(k: u64, msg: &str, ok: bool, c: bool) -> Vec<String> {
let mark_rgb = if ok { GREEN } else { RED };
let mark = if ok { "✓" } else { "✗" };
let msg_rgb = if ok { GREEN } else { RED };
if k == 0 {
vec![
String::new(),
format!(" {}", paint(&format!("_{mark}_"), mark_rgb, c)),
format!(" {}", head_calm('•', c)),
format!(" {}", body_rest(c)),
format!(" {}", feet(c)),
]
} else if ok {
vec![
String::new(),
format!(" {}", paint(&format!("_{mark}_"), mark_rgb, c)),
format!(
" {}{}{} {}",
paint("\\", ACCENT, c),
head_happy(c),
paint("/", ACCENT, c),
paint(msg, msg_rgb, c)
),
format!(" {}", body_hop(c)),
format!(" {}", feet(c)),
]
} else {
vec![
String::new(),
format!(" {}", paint(&format!("_{mark}_"), mark_rgb, c)),
format!(" {} {}", head_cool(c), paint(msg, msg_rgb, c)),
format!(" {}", paint("<(\\|/)>", ACCENT, c)),
format!(" {}", feet(c)),
]
}
}
const FW: usize = 36;
const VPOS: [(usize, usize); 5] = [
(0, 1), (0, 28), (1, 9), (1, 20), (2, 14), ];
fn glyph(flap: bool) -> String {
let w = if flap { '•' } else { '^' };
format!("~({w}ᴗ{w})~")
}
fn place(row: &mut [char], x: i64, g: &str) {
for (k, ch) in g.chars().enumerate() {
let xx = x + k as i64;
if xx >= 0 && (xx as usize) < row.len() {
row[xx as usize] = ch;
}
}
}
fn rows_to_lines(rows: Vec<Vec<char>>, c: bool, color: (u8, u8, u8)) -> Vec<String> {
rows.into_iter()
.map(|r| paint(&r.into_iter().collect::<String>(), color, c))
.collect()
}
fn flock_cross(t: u64, label: &str, c: bool) -> Vec<String> {
let mut rows = vec![vec![' '; FW]; 3];
for (i, o) in [0usize, 12, 24].iter().enumerate() {
let span = (FW + 9) as i64;
let x = ((t as i64 + *o as i64) % span) - 5;
let flap = (t as usize + i).is_multiple_of(2);
let y = if ((t / 2) as usize + i).is_multiple_of(2) {
0
} else {
1
};
place(&mut rows[y], x, &glyph(flap));
}
let mut out = rows_to_lines(rows, c, ACCENT);
out.push(format!(" {}", paint(&format!("{label}…"), DIM, c)));
out
}
fn flock_v(k: u64, msg: &str, done: bool, ok: bool, c: bool) -> Vec<String> {
let mut rows = vec![vec![' '; FW]; 3];
if ok {
for (i, (ty, tx)) in VPOS.iter().enumerate() {
let flap = (k as usize + i).is_multiple_of(2);
place(&mut rows[*ty], *tx as i64, &glyph(flap));
}
} else {
let scatter: [(usize, usize); 4] = [(0, 3), (2, 9), (1, 20), (0, 28)];
for (i, (ty, tx)) in scatter.iter().enumerate() {
let flap = (k as usize + i).is_multiple_of(2);
place(&mut rows[*ty], *tx as i64, &glyph(flap));
}
}
let color = if ok { ACCENT } else { RED };
let mut out = rows_to_lines(rows, c, color);
out.push(if done {
if ok {
format!(" {}", paint(&format!("✓ {msg}"), GREEN, c))
} else {
format!(" {}", paint(&format!("✗ {msg}"), RED, c))
}
} else if ok {
format!(" {}", paint("finalizing…", DIM, c))
} else {
format!(" {}", paint("aborting…", DIM, c))
});
out
}
const DIM: (u8, u8, u8) = (0, 0, 0);
fn draw(lines: &[String], first: bool) {
let mut s = String::new();
if !first {
s.push_str(&format!("\x1b[{}A", lines.len()));
}
s.push('\r');
for l in lines {
s.push_str("\x1b[2K");
s.push_str(l);
s.push('\n');
}
let mut o = io::stdout();
let _ = write!(o, "{}", s);
let _ = o.flush();
}
fn paint(text: &str, rgb: (u8, u8, u8), color: bool) -> String {
if !color {
return text.to_string();
}
if rgb == DIM {
return format!("\x1b[2m{text}\x1b[22m");
}
let (r, g, b) = rgb;
format!("\x1b[38;2;{r};{g};{b}m{text}\x1b[39m")
}
fn use_color() -> bool {
std::env::var_os("NO_COLOR").is_none()
}
fn hide_cursor() {
let _ = write!(io::stdout(), "\x1b[?25l");
let _ = io::stdout().flush();
}
fn show_cursor() {
let _ = write!(io::stdout(), "\x1b[?25h");
let _ = io::stdout().flush();
}