#![forbid(unsafe_code)]
use std::io::IsTerminal;
use std::time::Duration;
use indicatif::{ProgressBar, ProgressStyle};
use owo_colors::OwoColorize;
const TEAL: (u8, u8, u8) = (61, 163, 140);
const TEAL_DIM: (u8, u8, u8) = (39, 111, 95);
const WORDMARK: &str = r"
__ ____ _ _ __ | |_ __ _
\ \ / / _` | '_ \| __/ _` |
\ V / (_| | | | | || (_| |
\_/ \__,_|_| |_|\__\__,_|";
pub fn is_rich() -> bool {
!no_color() && std::io::stdout().is_terminal()
}
fn no_color() -> bool {
std::env::var_os("NO_COLOR").is_some_and(|v| !v.is_empty())
}
pub fn banner(version: &str) {
if !is_rich() {
return;
}
eprintln!("{}", WORDMARK.truecolor(TEAL.0, TEAL.1, TEAL.2));
eprintln!(
" {} {}",
"every developer tool, one command".truecolor(TEAL_DIM.0, TEAL_DIM.1, TEAL_DIM.2),
format!("v{version}").truecolor(TEAL_DIM.0, TEAL_DIM.1, TEAL_DIM.2),
);
eprintln!();
}
pub fn step(msg: &str) {
if is_rich() {
eprintln!("{} {msg}", "✓".truecolor(TEAL.0, TEAL.1, TEAL.2));
} else {
eprintln!("✓ {msg}");
}
}
pub fn running(cmd: &str) {
if is_rich() {
eprintln!("{} {}", "▸ running:".truecolor(TEAL.0, TEAL.1, TEAL.2), cmd);
} else {
eprintln!("▸ running: {cmd}");
}
}
pub struct Progress {
bar: ProgressBar,
rich: bool,
}
impl Progress {
pub fn new_spinner(msg: &str) -> Progress {
let rich = is_rich();
let bar = if rich {
let pb = ProgressBar::new_spinner();
pb.set_style(spinner_style());
pb.enable_steady_tick(Duration::from_millis(90));
pb.set_message(msg.to_string());
pb
} else {
ProgressBar::hidden()
};
Progress { bar, rich }
}
pub fn new_bar(msg: &str, total: Option<u64>) -> Progress {
let rich = is_rich();
let bar = if rich {
match total {
Some(n) => {
let pb = ProgressBar::new(n);
pb.set_style(bar_style());
pb.set_message(msg.to_string());
pb
}
None => {
let pb = ProgressBar::new_spinner();
pb.set_style(byte_spinner_style());
pb.enable_steady_tick(Duration::from_millis(90));
pb.set_message(msg.to_string());
pb
}
}
} else {
ProgressBar::hidden()
};
Progress { bar, rich }
}
pub fn inc(&self, n: u64) {
self.bar.inc(n);
}
pub fn set_msg(&self, msg: &str) {
self.bar.set_message(msg.to_string());
}
pub fn clear(&self) {
self.bar.finish_and_clear();
}
pub fn finish_ok(&self, msg: &str) {
if self.rich {
self.bar.finish_and_clear();
eprintln!("{} {msg}", "✓".truecolor(TEAL.0, TEAL.1, TEAL.2));
} else {
self.bar.finish_and_clear();
eprintln!("✓ {msg}");
}
}
pub fn finish_err(&self, msg: &str) {
if self.rich {
self.bar.finish_and_clear();
eprintln!("{} {msg}", "✗".truecolor(TEAL.0, TEAL.1, TEAL.2));
} else {
self.bar.finish_and_clear();
eprintln!("✗ {msg}");
}
}
}
fn spinner_style() -> ProgressStyle {
ProgressStyle::with_template("{spinner:.cyan} {msg}")
.unwrap()
.tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ ")
}
fn byte_spinner_style() -> ProgressStyle {
ProgressStyle::with_template("{spinner:.cyan} {msg} {bytes} ({bytes_per_sec})")
.unwrap()
.tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ ")
}
fn bar_style() -> ProgressStyle {
ProgressStyle::with_template(
"{spinner:.cyan} {msg} [{bar:24.36/23}] {bytes}/{total_bytes} ({eta})",
)
.unwrap()
.progress_chars("█░")
.tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_color_forces_plain_mode() {
let prev = std::env::var_os("NO_COLOR");
std::env::set_var("NO_COLOR", "1");
assert!(no_color());
assert!(!is_rich(), "NO_COLOR must disable rich mode");
std::env::set_var("NO_COLOR", "");
assert!(!no_color(), "empty NO_COLOR must not count as set");
match prev {
Some(v) => std::env::set_var("NO_COLOR", v),
None => std::env::remove_var("NO_COLOR"),
}
}
#[test]
fn styles_parse() {
let _ = spinner_style();
let _ = byte_spinner_style();
let _ = bar_style();
}
#[test]
fn hidden_bar_is_inert() {
let p = Progress {
bar: ProgressBar::hidden(),
rich: false,
};
p.inc(10);
p.set_msg("x");
p.finish_ok("done");
}
}