use std::fmt;
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
use anstyle::{AnsiColor, Style};
use clap::builder::Styles;
pub const EXIT_OK: i32 = 0;
pub const EXIT_FAILURE: i32 = 1;
pub const EXIT_USAGE: i32 = 2;
pub const ERROR_STYLE: Style = AnsiColor::Red.on_default().bold();
pub const HELP_STYLE: Style = AnsiColor::Cyan.on_default().bold();
pub const PHASE_STYLE: Style = AnsiColor::Green.on_default().bold();
pub const WARN_STYLE: Style = AnsiColor::Yellow.on_default().bold();
pub const CLAP_STYLES: Styles = Styles::styled()
.header(AnsiColor::Green.on_default().bold())
.usage(AnsiColor::Green.on_default().bold())
.literal(AnsiColor::Cyan.on_default().bold())
.placeholder(AnsiColor::Cyan.on_default());
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, clap::ValueEnum)]
pub enum ColorMode {
#[default]
Auto,
Always,
Never,
}
static QUIET: AtomicBool = AtomicBool::new(false);
static VERBOSITY: AtomicU8 = AtomicU8::new(0);
pub fn init(color: ColorMode, quiet: bool, verbosity: u8) {
QUIET.store(quiet, Ordering::Relaxed);
VERBOSITY.store(verbosity, Ordering::Relaxed);
let choice = match color {
ColorMode::Auto => anstream::ColorChoice::Auto,
ColorMode::Always => anstream::ColorChoice::Always,
ColorMode::Never => anstream::ColorChoice::Never,
};
choice.write_global();
}
pub fn is_quiet() -> bool {
QUIET.load(Ordering::Relaxed)
}
pub fn verbosity() -> u8 {
VERBOSITY.load(Ordering::Relaxed)
}
pub fn info(msg: impl fmt::Display) {
if !is_quiet() {
anstream::println!("{msg}");
}
}
pub fn phase(verb: &str, rest: impl fmt::Display) {
if !is_quiet() {
anstream::eprintln!("{PHASE_STYLE}{verb:>12}{PHASE_STYLE:#} {rest}");
}
}
#[derive(Debug)]
pub struct CliError {
pub message: String,
pub hint: Option<String>,
pub exit_code: i32,
}
impl CliError {
pub fn new(message: impl Into<String>) -> Self {
Self { message: message.into(), hint: None, exit_code: EXIT_FAILURE }
}
pub fn with_hint(message: impl Into<String>, hint: impl Into<String>) -> Self {
Self { message: message.into(), hint: Some(hint.into()), exit_code: EXIT_FAILURE }
}
pub fn exit_code(mut self, code: i32) -> Self {
self.exit_code = code;
self
}
}
impl fmt::Display for CliError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for CliError {}
pub fn render_error(e: &(dyn std::error::Error + 'static)) -> i32 {
if let Some(cli_err) = e.downcast_ref::<CliError>() {
anstream::eprintln!("{ERROR_STYLE}error:{ERROR_STYLE:#} {}", cli_err.message);
if let Some(hint) = &cli_err.hint {
anstream::eprintln!("{HELP_STYLE}help:{HELP_STYLE:#} {hint}");
}
cli_err.exit_code
} else {
anstream::eprintln!("{ERROR_STYLE}error:{ERROR_STYLE:#} {e}");
EXIT_FAILURE
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cli_error_defaults_to_failure_exit_code() {
let e = CliError::new("boom");
assert_eq!(e.exit_code, EXIT_FAILURE);
assert!(e.hint.is_none());
assert_eq!(e.to_string(), "boom");
}
#[test]
fn cli_error_carries_hint_and_custom_code() {
let e = CliError::with_hint("boom", "try --fix").exit_code(EXIT_USAGE);
assert_eq!(e.hint.as_deref(), Some("try --fix"));
assert_eq!(e.exit_code, EXIT_USAGE);
}
#[test]
fn render_error_uses_cli_error_exit_code() {
let boxed: Box<dyn std::error::Error> =
Box::new(CliError::new("x").exit_code(EXIT_USAGE));
assert_eq!(render_error(boxed.as_ref()), EXIT_USAGE);
}
#[test]
fn render_error_defaults_other_errors_to_failure() {
let boxed: Box<dyn std::error::Error> = "plain".into();
assert_eq!(render_error(boxed.as_ref()), EXIT_FAILURE);
}
}