use std::borrow::Cow;
use std::io::IsTerminal;
pub const NO_COLOR_ENV: &str = "NO_COLOR";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum ColorChoice {
#[default]
Auto,
Always,
Never,
}
impl ColorChoice {
#[must_use]
pub fn from_name(name: &str) -> Option<Self> {
match name {
"auto" => Some(Self::Auto),
"always" => Some(Self::Always),
"never" => Some(Self::Never),
_ => None,
}
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Auto => "auto",
Self::Always => "always",
Self::Never => "never",
}
}
}
#[must_use]
pub fn resolve_color(choice: ColorChoice, is_terminal: bool) -> bool {
resolve_color_with(choice, no_color_env_set(), is_terminal)
}
#[must_use]
pub const fn resolve_color_with(choice: ColorChoice, no_color: bool, is_terminal: bool) -> bool {
if no_color {
return false;
}
match choice {
ColorChoice::Always => true,
ColorChoice::Never => false,
ColorChoice::Auto => is_terminal,
}
}
#[must_use]
pub fn no_color_env_set() -> bool {
std::env::var_os(NO_COLOR_ENV).is_some()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Palette {
enabled: bool,
}
impl Palette {
#[must_use]
pub const fn new(enabled: bool) -> Self {
Self { enabled }
}
#[must_use]
pub fn for_stream(choice: ColorChoice, stream: &impl IsTerminal) -> Self {
Self::new(resolve_color(choice, stream.is_terminal()))
}
#[must_use]
pub const fn enabled(self) -> bool {
self.enabled
}
#[must_use]
pub fn success(self, text: &str) -> Cow<'_, str> {
self.paint("32", text)
}
#[must_use]
pub fn error(self, text: &str) -> Cow<'_, str> {
self.paint("31", text)
}
#[must_use]
pub fn warn(self, text: &str) -> Cow<'_, str> {
self.paint("33", text)
}
#[must_use]
pub fn info(self, text: &str) -> Cow<'_, str> {
self.paint("36", text)
}
#[must_use]
pub fn dim(self, text: &str) -> Cow<'_, str> {
self.paint("2", text)
}
#[must_use]
pub fn bold(self, text: &str) -> Cow<'_, str> {
self.paint("1", text)
}
fn paint<'a>(self, code: &str, text: &'a str) -> Cow<'a, str> {
if self.enabled {
Cow::Owned(format!("\u{1b}[{code}m{text}\u{1b}[0m"))
} else {
Cow::Borrowed(text)
}
}
}
#[cfg(test)]
mod tests {
use super::{ColorChoice, Palette, no_color_env_set, resolve_color, resolve_color_with};
use std::borrow::Cow;
#[test]
fn no_color_overrides_always() {
assert!(!resolve_color_with(ColorChoice::Always, true, true));
assert!(!resolve_color_with(ColorChoice::Always, true, false));
}
#[test]
fn always_and_never_are_absolute_without_no_color() {
assert!(resolve_color_with(ColorChoice::Always, false, false));
assert!(!resolve_color_with(ColorChoice::Never, false, true));
}
#[test]
fn auto_follows_terminal() {
assert!(resolve_color_with(ColorChoice::Auto, false, true));
assert!(!resolve_color_with(ColorChoice::Auto, false, false));
}
#[test]
fn choice_round_trips_by_name() {
for choice in [ColorChoice::Auto, ColorChoice::Always, ColorChoice::Never] {
assert_eq!(ColorChoice::from_name(choice.as_str()), Some(choice));
}
assert_eq!(ColorChoice::from_name("bogus"), None);
}
#[test]
fn disabled_palette_is_the_identity() {
let plain = Palette::new(false);
assert_eq!(plain.success("ok"), Cow::Borrowed("ok"));
assert_eq!(plain.error("boom"), Cow::Borrowed("boom"));
assert!(matches!(plain.success("ok"), Cow::Borrowed(_)));
assert!(!plain.enabled());
}
#[test]
fn enabled_palette_wraps_in_sgr_and_resets() {
let color = Palette::new(true);
assert_eq!(color.success("ok"), "\u{1b}[32mok\u{1b}[0m");
assert_eq!(color.error("boom"), "\u{1b}[31mboom\u{1b}[0m");
assert!(color.enabled());
}
#[test]
fn never_choice_stays_disabled_through_the_env_aware_resolver() {
assert!(!resolve_color(ColorChoice::Never, true));
let _ = no_color_env_set();
}
#[test]
fn for_stream_resolves_a_palette_against_a_real_stream() {
let palette = Palette::for_stream(ColorChoice::Never, &std::io::stderr());
assert!(!palette.enabled());
}
}