use std::io::IsTerminal;
use clap::{Args, ValueEnum};
#[derive(Args, Debug, Clone, Default)]
pub struct OutputArgs {
#[arg(short = 'v', long = "verbose", global = true, action = clap::ArgAction::Count)]
pub verbose: u8,
#[arg(long = "color", global = true, value_enum, default_value = "auto")]
pub color: ColorMode,
#[arg(short = 'F', long = "format", global = true, default_value = "tree")]
pub format: String,
}
#[derive(ValueEnum, Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum ColorMode {
#[default]
Auto,
Always,
Never,
}
pub struct CliOutput {
pub verbosity: Verbosity,
pub color: bool,
pub format: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Verbosity {
Quiet,
Default,
Verbose,
Trace,
}
impl CliOutput {
pub fn from_args(args: &OutputArgs) -> Self {
let color = match args.color {
ColorMode::Always => true,
ColorMode::Never => false,
ColorMode::Auto => {
std::io::stdout().is_terminal() && std::env::var("NO_COLOR").is_err()
}
};
let verbosity = match args.verbose {
0 => Verbosity::Default,
1 => Verbosity::Verbose,
_ => Verbosity::Trace,
};
Self {
verbosity,
color,
format: args.format.clone(),
}
}
pub fn format_info(&self, msg: impl AsRef<str>) -> String {
use console::Style;
let prefix = if self.color {
Style::new().green().bold().apply_to(">").to_string()
} else {
">".to_string()
};
format!("{} {}", prefix, msg.as_ref())
}
pub fn format_warn(&self, msg: impl AsRef<str>) -> String {
use console::Style;
let prefix = if self.color {
Style::new().yellow().bold().apply_to("!").to_string()
} else {
"!".to_string()
};
format!("{} {}", prefix, msg.as_ref())
}
pub fn format_error(&self, msg: impl AsRef<str>) -> String {
use console::Style;
let prefix = if self.color {
Style::new().red().bold().apply_to("ERROR").to_string()
} else {
"ERROR".to_string()
};
format!("{} {}", prefix, msg.as_ref())
}
pub fn info(&self, msg: impl AsRef<str>) {
eprintln!("{}", self.format_info(msg));
}
pub fn warn(&self, msg: impl AsRef<str>) {
eprintln!("{}", self.format_warn(msg));
}
pub fn error(&self, msg: impl AsRef<str>) {
eprintln!("{}", self.format_error(msg));
}
pub fn is_verbose(&self) -> bool {
matches!(self.verbosity, Verbosity::Verbose | Verbosity::Trace)
}
pub fn is_trace(&self) -> bool {
matches!(self.verbosity, Verbosity::Trace)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn args_with(verbose: u8, color: ColorMode, format: &str) -> OutputArgs {
OutputArgs {
verbose,
color,
format: format.to_string(),
}
}
#[test]
fn color_always_forces_color() {
let out = CliOutput::from_args(&args_with(0, ColorMode::Always, "tree"));
assert!(out.color);
}
#[test]
fn color_never_forces_no_color() {
let out = CliOutput::from_args(&args_with(0, ColorMode::Never, "tree"));
assert!(!out.color);
}
#[test]
fn color_auto_respects_no_color_env() {
std::env::set_var("NO_COLOR", "1");
let out = CliOutput::from_args(&args_with(0, ColorMode::Auto, "tree"));
std::env::remove_var("NO_COLOR");
assert!(!out.color);
}
#[test]
fn verbosity_zero_is_default() {
let out = CliOutput::from_args(&args_with(0, ColorMode::Never, "tree"));
assert_eq!(out.verbosity, Verbosity::Default);
assert!(!out.is_verbose());
}
#[test]
fn verbosity_one_is_verbose() {
let out = CliOutput::from_args(&args_with(1, ColorMode::Never, "tree"));
assert_eq!(out.verbosity, Verbosity::Verbose);
assert!(out.is_verbose());
assert!(!out.is_trace());
}
#[test]
fn verbosity_two_is_trace() {
let out = CliOutput::from_args(&args_with(2, ColorMode::Never, "tree"));
assert_eq!(out.verbosity, Verbosity::Trace);
assert!(out.is_verbose());
assert!(out.is_trace());
}
fn no_color_out() -> CliOutput {
CliOutput::from_args(&args_with(0, ColorMode::Never, "tree"))
}
#[test]
fn info_prefix_no_color() {
let out = no_color_out();
assert_eq!(out.format_info("hello"), "> hello");
}
#[test]
fn warn_prefix_no_color() {
let out = no_color_out();
assert_eq!(out.format_warn("careful"), "! careful");
}
#[test]
fn error_prefix_no_color() {
let out = no_color_out();
assert_eq!(out.format_error("boom"), "ERROR boom");
}
#[test]
fn format_field_passed_through() {
let out = CliOutput::from_args(&args_with(0, ColorMode::Never, "json"));
assert_eq!(out.format, "json");
}
}