#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, clap::ValueEnum)]
pub enum ColorChoice {
#[default]
Auto,
Always,
Never,
}
pub(crate) fn no_color_set(value: Option<&str>) -> bool {
value.is_some_and(|value| !value.is_empty())
}
#[derive(Clone, Copy)]
enum Role {
Good,
Bad,
Busy,
Unknown,
}
impl Role {
fn code(self) -> u8 {
match self {
Self::Good => 32,
Self::Bad => 31,
Self::Busy => 33,
Self::Unknown => 35,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Palette {
on: bool,
}
impl Palette {
#[must_use]
pub const fn plain() -> Self {
Self { on: false }
}
#[must_use]
pub fn resolve(choice: ColorChoice, is_terminal: bool, no_color: Option<&str>) -> Self {
let on = match choice {
ColorChoice::Never => false,
ColorChoice::Always => true,
ColorChoice::Auto => is_terminal && !no_color_set(no_color),
};
Self { on }
}
#[must_use]
pub const fn is_on(self) -> bool {
self.on
}
fn paint(self, role: Role, text: &str) -> String {
if self.on {
format!("\x1b[{}m{text}\x1b[0m", role.code())
} else {
text.to_string()
}
}
#[must_use]
pub fn ok(self, text: &str) -> String {
self.paint(Role::Good, text)
}
#[must_use]
pub fn bad(self, text: &str) -> String {
self.paint(Role::Bad, text)
}
#[must_use]
pub fn warn(self, text: &str) -> String {
self.paint(Role::Busy, text)
}
#[must_use]
pub fn unknown(self, text: &str) -> String {
self.paint(Role::Unknown, text)
}
#[must_use]
pub fn status(self, text: &str) -> String {
let role = match text.trim() {
"valid" | "ready" | "active" | "enabled" | "success" | "on" | "allow" | "allowed"
| "pass" | "done" => Role::Good,
"invalid" | "revoked" | "deactivated" | "expired" | "disabled" | "off" | "failure"
| "deny" | "denied" | "fail" | "failed" | "cancelled" => Role::Bad,
"pending" | "processing" | "pending_mfa" | "running" => Role::Busy,
"unknown" | "undecided" => Role::Unknown,
_ => return text.to_string(),
};
self.paint(role, text)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_covers_every_choice_against_the_stream_and_the_environment() {
for (choice, is_terminal, no_color, expected) in [
(ColorChoice::Auto, true, None, true),
(ColorChoice::Auto, false, None, false),
(ColorChoice::Auto, true, Some("1"), false),
(ColorChoice::Auto, true, Some("anything"), false),
(ColorChoice::Auto, true, Some(""), true),
(ColorChoice::Always, false, None, true),
(ColorChoice::Always, false, Some("1"), true),
(ColorChoice::Always, true, Some("1"), true),
(ColorChoice::Never, true, None, false),
(ColorChoice::Never, true, Some(""), false),
] {
assert_eq!(
Palette::resolve(choice, is_terminal, no_color).is_on(),
expected,
"{choice:?} tty={is_terminal} NO_COLOR={no_color:?}"
);
}
}
#[test]
fn auto_is_the_default_choice() {
assert_eq!(ColorChoice::default(), ColorChoice::Auto);
}
#[test]
fn a_plain_palette_returns_its_argument_untouched() {
let plain = Palette::plain();
for text in ["valid", "invalid", "pending", "unknown", "", " ready "] {
assert_eq!(plain.ok(text), text);
assert_eq!(plain.bad(text), text);
assert_eq!(plain.warn(text), text);
assert_eq!(plain.unknown(text), text);
assert_eq!(plain.status(text), text);
}
}
#[test]
fn each_role_emits_its_own_escape() {
let colour = Palette::resolve(ColorChoice::Always, false, None);
assert_eq!(colour.ok("x"), "\x1b[32mx\x1b[0m");
assert_eq!(colour.bad("x"), "\x1b[31mx\x1b[0m");
assert_eq!(colour.warn("x"), "\x1b[33mx\x1b[0m");
assert_eq!(colour.unknown("x"), "\x1b[35mx\x1b[0m");
}
#[test]
fn the_status_vocabulary_maps_every_domains_words() {
let colour = Palette::resolve(ColorChoice::Always, false, None);
for good in [
"valid", "ready", "active", "enabled", "success", "on", "done",
] {
assert_eq!(colour.status(good), format!("\x1b[32m{good}\x1b[0m"));
}
for bad in [
"invalid",
"revoked",
"deactivated",
"expired",
"disabled",
"off",
"failure",
"failed",
"cancelled",
] {
assert_eq!(colour.status(bad), format!("\x1b[31m{bad}\x1b[0m"));
}
for busy in ["pending", "processing", "pending_mfa", "running"] {
assert_eq!(colour.status(busy), format!("\x1b[33m{busy}\x1b[0m"));
}
}
#[test]
fn an_unrecognised_status_is_never_painted() {
let colour = Palette::resolve(ColorChoice::Always, false, None);
assert_eq!(colour.status("quiescent"), "quiescent");
assert!(!colour.status("quiescent").contains('\x1b'));
}
#[test]
fn painting_a_padded_column_preserves_its_width() {
let colour = Palette::resolve(ColorChoice::Always, false, None);
let padded = format!("{:<11}", "valid");
let painted = colour.status(&padded);
assert_eq!(painted, format!("\x1b[32m{padded}\x1b[0m"));
assert_eq!(
painted
.trim_start_matches("\x1b[32m")
.trim_end_matches("\x1b[0m"),
Palette::plain().status(&padded)
);
}
}