use std::io::{IsTerminal, Write};
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::time::{Duration, Instant};
const DIM: &str = "\x1b[2m";
const BOLD: &str = "\x1b[1m";
const GREEN: &str = "\x1b[32m";
const CYAN: &str = "\x1b[36m";
const YELLOW: &str = "\x1b[33m";
const RESET: &str = "\x1b[0m";
const SPINNER: [&str; 10] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
pub struct Ui {
color: bool,
stderr_tty: bool,
log: Option<Arc<Mutex<Vec<String>>>>,
}
impl Ui {
#[allow(clippy::new_without_default)]
pub fn new() -> Ui {
let color = std::env::var("NO_COLOR").is_err() && std::io::stdout().is_terminal();
Ui {
color,
stderr_tty: std::io::stderr().is_terminal(),
log: None,
}
}
pub fn recorder() -> (Ui, Arc<Mutex<Vec<String>>>) {
let log = Arc::new(Mutex::new(Vec::new()));
(
Ui {
color: false,
stderr_tty: false,
log: Some(Arc::clone(&log)),
},
log,
)
}
fn paint(&self, code: &str, text: &str) -> String {
if self.color {
format!("{code}{text}{RESET}")
} else {
text.to_string()
}
}
fn out(&self, plain: &str, colored: &str) {
if let Some(log) = &self.log {
log.lock()
.unwrap_or_else(|e| e.into_inner())
.push(plain.to_string());
return;
}
let mut err = std::io::stderr().lock();
if self.stderr_tty {
let _ = write!(err, "\r\x1b[2K");
}
let _ = writeln!(err, "{}", if self.color { colored } else { plain });
let _ = err.flush();
}
pub fn heading(&self, text: &str) {
self.out(text, &self.paint(BOLD, text));
}
pub fn step(&self, verb: &str, detail: &str) {
self.out(
&format!(" ● {verb} {detail}…"),
&format!(
" {} {} {detail}…",
self.paint(CYAN, "●"),
self.paint(&format!("{BOLD}{CYAN}"), verb),
),
);
}
pub fn done(&self, verb: &str, detail: &str) {
self.out(
&format!(" ✓ {verb} {detail}"),
&format!(
" {} {} {detail}",
self.paint(GREEN, "✓"),
self.paint(&format!("{BOLD}{GREEN}"), verb),
),
);
}
pub fn summary(&self, verb: &str, subject: &str, detail: &str, elapsed: Duration) {
let secs = elapsed.as_secs_f64();
let timing = if secs >= 1.0 {
format!(" in {secs:.1}s")
} else if secs * 1000.0 >= 100.0 {
format!(" in {:.0}ms", secs * 1000.0)
} else {
String::new() };
let mut parts: Vec<String> = vec![
self.paint(GREEN, "✓"),
self.paint(BOLD, verb),
self.paint(BOLD, subject),
];
let mut plain = format!(" ✓ {verb} {subject}");
if !detail.is_empty() {
parts.push(self.paint(DIM, detail));
plain.push_str(&format!(" {detail}"));
}
if !timing.is_empty() {
parts.push(self.paint(DIM, timing.trim_start()));
plain.push_str(&format!(" {}", timing.trim_start()));
}
self.out(&plain, &parts.join(" "));
}
pub fn note(&self, text: &str) {
self.out(
&format!(" ▸ {text}"),
&format!(" {} {}", self.paint(DIM, "▸"), self.paint(DIM, text)),
);
}
pub fn spinner(&self, label: &str) -> SpinnerGuard {
if let Some(log) = &self.log {
log.lock()
.unwrap_or_else(|e| e.into_inner())
.push(format!(" ● {label}…"));
return SpinnerGuard {
stop: Arc::new(AtomicBool::new(true)),
handle: None,
};
}
let stop = Arc::new(AtomicBool::new(false));
let frame = Arc::new(AtomicUsize::new(0));
let color = self.color;
let label_owned = label.to_string();
let handle = if self.stderr_tty {
let stop = Arc::clone(&stop);
let frame = Arc::clone(&frame);
Some(std::thread::spawn(move || {
let mut err = std::io::stderr().lock();
loop {
if stop.load(Ordering::Relaxed) {
break;
}
let i = frame.load(Ordering::Relaxed) % SPINNER.len();
let sp = SPINNER[i];
let _ = write!(err, "\r\x1b[2K");
if color {
let _ = write!(err, "{CYAN}{sp}{RESET} {DIM}{label_owned}…{RESET}");
} else {
let _ = write!(err, "{sp} {label_owned}…");
}
let _ = err.flush();
std::thread::sleep(Duration::from_millis(90));
frame.fetch_add(1, Ordering::Relaxed);
}
let _ = write!(err, "\r\x1b[2K");
let _ = err.flush();
}))
} else {
eprintln!(" {label_owned}…");
None
};
SpinnerGuard { stop, handle }
}
pub fn row(&self, name: &str, version: &str, source: &str, active: bool, pinned: bool) {
let mut err = std::io::stderr().lock();
let pin = if pinned {
format!(" {}", self.paint(YELLOW, "📌"))
} else {
String::new()
};
let marker = if active {
format!(" {}", self.paint(&format!("{BOLD}{GREEN}"), "← active"))
} else {
String::new()
};
let _ = writeln!(
err,
" {} {} {}{}{}",
self.paint(BOLD, name),
self.paint(GREEN, version),
self.paint(DIM, &format!("from {source}")),
pin,
marker,
);
let _ = err.flush();
}
pub fn empty_hint(&self) {
let mut err = std::io::stderr().lock();
let _ = writeln!(
err,
" {} no providers installed — {}",
self.paint(DIM, "·"),
self.paint(&format!("{BOLD}{CYAN}"), "rootle provider install gitlab"),
);
let _ = err.flush();
}
}
pub struct SpinnerGuard {
stop: Arc<AtomicBool>,
handle: Option<std::thread::JoinHandle<()>>,
}
impl Drop for SpinnerGuard {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
if let Some(h) = self.handle.take() {
let _ = h.join();
}
}
}
pub struct Timer {
start: Instant,
}
impl Timer {
pub fn start() -> Timer {
Timer {
start: Instant::now(),
}
}
pub fn elapsed(&self) -> Duration {
self.start.elapsed()
}
}