use std::io::{IsTerminal, Write};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
fn no_color() -> bool {
std::env::var_os("NO_COLOR").is_some()
}
fn force_color() -> bool {
std::env::var("CLICOLOR_FORCE").is_ok_and(|v| !v.is_empty() && v != "0")
}
fn stdout_tty() -> bool {
!no_color() && (force_color() || std::io::stdout().is_terminal())
}
fn stderr_tty() -> bool {
!no_color() && (force_color() || std::io::stderr().is_terminal())
}
const RESET: &str = "\x1b[0m";
const BOLD: &str = "\x1b[1m";
const DIM: &str = "\x1b[90m";
const RED: &str = "\x1b[31m";
const GREEN: &str = "\x1b[32m";
const YELLOW: &str = "\x1b[33m";
const CYAN: &str = "\x1b[36m";
fn paint(on: bool, code: &str, s: &str) -> String {
if on {
format!("{code}{s}{RESET}")
} else {
s.to_string()
}
}
pub fn dim(s: &str) -> String {
paint(stdout_tty(), DIM, s)
}
pub fn accent(s: &str) -> String {
paint(stdout_tty(), &format!("{BOLD}{CYAN}"), s)
}
pub fn red(s: &str) -> String {
paint(stdout_tty(), &format!("{BOLD}{RED}"), s)
}
pub fn yellow(s: &str) -> String {
paint(stdout_tty(), YELLOW, s)
}
pub fn green(s: &str) -> String {
paint(stdout_tty(), GREEN, s)
}
pub fn header(title: &str) {
if stdout_tty() {
println!("\n{BOLD}{CYAN}▸ {title}{RESET}");
println!("{DIM}{}{RESET}", "─".repeat(title.chars().count() + 2));
} else {
println!("\n== {title} ==");
}
}
pub fn field(key: &str, val: &str) {
if stdout_tty() {
println!(" {DIM}{key}:{RESET} {val}");
} else {
println!(" {key}: {val}");
}
}
fn glyph(color: &str, ascii: &str, uni: &str) -> String {
if stderr_tty() {
format!("{color}{uni}{RESET}")
} else {
ascii.to_string()
}
}
pub fn ok(msg: &str) {
eprintln!("{} {msg}", glyph(GREEN, "[+]", "✓"));
}
pub fn warn(msg: &str) {
eprintln!("{} {msg}", glyph(YELLOW, "[!]", "▲"));
}
pub fn bad(msg: &str) {
eprintln!("{} {msg}", glyph(RED, "[-]", "✗"));
}
pub fn info(msg: &str) {
eprintln!("{} {msg}", glyph(CYAN, "[*]", "•"));
}
pub struct Spinner {
stop: Arc<AtomicBool>,
handle: Option<JoinHandle<()>>,
}
impl Spinner {
pub fn start(msg: impl Into<String>) -> Self {
let msg = msg.into();
let stop = Arc::new(AtomicBool::new(false));
if !std::io::stderr().is_terminal() {
eprintln!("[*] {msg}…");
return Spinner { stop, handle: None };
}
let flag = stop.clone();
let handle = std::thread::spawn(move || {
let frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
let start = Instant::now();
let mut i = 0usize;
while !flag.load(Ordering::Relaxed) {
let secs = start.elapsed().as_secs();
eprint!(
"\r{CYAN}{}{RESET} {msg} {DIM}({secs}s){RESET} ",
frames[i % frames.len()]
);
let _ = std::io::stderr().flush();
i += 1;
std::thread::sleep(Duration::from_millis(90));
}
});
Spinner {
stop,
handle: Some(handle),
}
}
fn stop_thread(&mut self) {
self.stop.store(true, Ordering::Relaxed);
if let Some(h) = self.handle.take() {
let _ = h.join();
eprint!("\r\x1b[2K"); let _ = std::io::stderr().flush();
}
}
pub fn done(mut self, msg: &str) {
self.stop_thread();
ok(msg);
}
pub fn done_warn(mut self, msg: &str) {
self.stop_thread();
warn(msg);
}
}
impl Drop for Spinner {
fn drop(&mut self) {
self.stop_thread();
}
}