use std::borrow::Cow;
use std::sync::atomic::{AtomicU8, Ordering as AtomicOrdering};
use std::sync::OnceLock;
use std::sync::atomic::AtomicBool;
use std::sync::Mutex;
static COLOR_MODE: AtomicU8 = AtomicU8::new(0);
pub fn enable() {
COLOR_MODE.store(1, AtomicOrdering::SeqCst);
}
pub fn disable() {
COLOR_MODE.store(2, AtomicOrdering::SeqCst);
}
static TEST_OVERRIDE: AtomicU8 = AtomicU8::new(0);
static TEST_OVERRIDE_VALUE: AtomicBool = AtomicBool::new(false);
static COLOR_OVERRIDE_LOCK: Mutex<()> = Mutex::new(());
pub fn set_colors_override(enabled: bool) {
TEST_OVERRIDE_VALUE.store(enabled, AtomicOrdering::SeqCst);
TEST_OVERRIDE.store(1, AtomicOrdering::SeqCst);
}
pub fn clear_colors_override() {
TEST_OVERRIDE.store(0, AtomicOrdering::SeqCst);
}
pub fn with_colors_override<R>(enabled: bool, f: impl FnOnce() -> R) -> R {
let _guard = COLOR_OVERRIDE_LOCK.lock().unwrap();
set_colors_override(enabled);
let result = f();
clear_colors_override();
result
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Color {
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
BrightBlack,
BrightRed,
BrightGreen,
BrightYellow,
BrightBlue,
BrightMagenta,
BrightCyan,
BrightWhite,
}
impl Color {
pub fn fg_code(self) -> u8 {
match self {
Color::Black => 30,
Color::Red => 31,
Color::Green => 32,
Color::Yellow => 33,
Color::Blue => 34,
Color::Magenta => 35,
Color::Cyan => 36,
Color::White => 37,
Color::BrightBlack => 90,
Color::BrightRed => 91,
Color::BrightGreen => 92,
Color::BrightYellow => 93,
Color::BrightBlue => 94,
Color::BrightMagenta => 95,
Color::BrightCyan => 96,
Color::BrightWhite => 97,
}
}
pub fn bg_code(self) -> u8 {
self.fg_code() + 10
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Style {
Bold,
Dim,
Italic,
Underline,
Blink,
RapidBlink,
Reverse,
Hidden,
Strikethrough,
Overline,
}
impl Style {
pub fn code(self) -> u8 {
match self {
Style::Bold => 1,
Style::Dim => 2,
Style::Italic => 3,
Style::Underline => 4,
Style::Blink => 5,
Style::RapidBlink => 6,
Style::Reverse => 7,
Style::Hidden => 8,
Style::Strikethrough => 9,
Style::Overline => 53,
}
}
}
pub struct StyledString<'a> {
pub text: Cow<'a, str>,
pub fg: Option<Color>,
pub bg: Option<Color>,
pub styles: Vec<Style>,
pub(crate) condition: Option<bool>,
pub(crate) masked: bool,
}
macro_rules! define_fg_methods {
($($method:ident => $color:ident),* $(,)?) => {
$(fn $method(self) -> StyledString<'static> where Self: Sized {
let mut s = self.styled(); s.fg = Some(Color::$color); s
})*
};
}
macro_rules! define_bg_methods {
($($method:ident => $color:ident),* $(,)?) => {
$(fn $method(self) -> StyledString<'static> where Self: Sized {
let mut s = self.styled(); s.bg = Some(Color::$color); s
})*
};
}
macro_rules! define_style_methods {
($($method:ident => $style:ident),* $(,)?) => {
$(fn $method(self) -> StyledString<'static> where Self: Sized {
let mut s = self.styled(); s.styles.push(Style::$style); s
})*
};
}
pub trait Colorize {
fn styled(self) -> StyledString<'static>;
define_fg_methods! {
black => Black, red => Red, green => Green, yellow => Yellow,
blue => Blue, magenta => Magenta, cyan => Cyan, white => White,
bright_black => BrightBlack, bright_red => BrightRed,
bright_green => BrightGreen, bright_yellow => BrightYellow,
bright_blue => BrightBlue, bright_magenta => BrightMagenta,
bright_cyan => BrightCyan, bright_white => BrightWhite,
}
define_bg_methods! {
on_black => Black, on_red => Red, on_green => Green, on_yellow => Yellow,
on_blue => Blue, on_magenta => Magenta, on_cyan => Cyan, on_white => White,
on_bright_black => BrightBlack, on_bright_red => BrightRed,
on_bright_green => BrightGreen, on_bright_yellow => BrightYellow,
on_bright_blue => BrightBlue, on_bright_magenta => BrightMagenta,
on_bright_cyan => BrightCyan, on_bright_white => BrightWhite,
}
define_style_methods! {
bold => Bold, dim => Dim, italic => Italic, underline => Underline,
blink => Blink, rapid_blink => RapidBlink, reverse => Reverse,
hidden => Hidden, strikethrough => Strikethrough, overline => Overline,
}
}
impl Colorize for &str {
fn styled(self) -> StyledString<'static> {
StyledString {
text: Cow::Owned(self.to_owned()),
fg: None,
bg: None,
styles: Vec::new(),
condition: None,
masked: false,
}
}
}
impl Colorize for String {
fn styled(self) -> StyledString<'static> {
StyledString {
text: Cow::Owned(self),
fg: None,
bg: None,
styles: Vec::new(),
condition: None,
masked: false,
}
}
}
impl<'a> Colorize for StyledString<'a> {
fn styled(self) -> StyledString<'static> {
StyledString {
text: Cow::Owned(self.text.into_owned()),
fg: self.fg,
bg: self.bg,
styles: self.styles,
condition: self.condition,
masked: self.masked,
}
}
}
impl<'a> StyledString<'a> {
pub fn whenever(mut self, condition: bool) -> Self {
self.condition = Some(condition);
self
}
pub fn mask(mut self) -> Self {
self.masked = true;
self
}
}
macro_rules! impl_colorize {
($($t:ty),*) => {
$(
impl Colorize for $t {
fn styled(self) -> StyledString<'static> {
StyledString {
text: Cow::Owned(self.to_string()),
fg: None,
bg: None,
styles: Vec::new(),
condition: None,
masked: false,
}
}
}
)*
};
}
impl_colorize!(
i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64, bool, char
);
pub fn style<T: std::fmt::Display>(value: T) -> StyledString<'static> {
StyledString {
text: Cow::Owned(value.to_string()),
fg: None,
bg: None,
styles: Vec::new(),
condition: None,
masked: false,
}
}
#[cfg(unix)]
fn isatty_stdout() -> bool {
extern "C" {
fn isatty(fd: i32) -> i32;
}
unsafe { isatty(1) != 0 }
}
#[cfg(windows)]
fn isatty_stdout() -> bool {
extern "system" {
fn GetStdHandle(nStdHandle: u32) -> *mut core::ffi::c_void;
fn GetConsoleMode(hConsoleHandle: *mut core::ffi::c_void, lpMode: *mut u32) -> i32;
}
const STD_OUTPUT_HANDLE: u32 = 0xFFFF_FFF5; unsafe {
let handle = GetStdHandle(STD_OUTPUT_HANDLE);
if handle.is_null() {
return false;
}
let mut mode: u32 = 0;
GetConsoleMode(handle, &mut mode) != 0
}
}
#[cfg(not(any(unix, windows)))]
fn isatty_stdout() -> bool {
false
}
pub fn colors_enabled() -> bool {
match COLOR_MODE.load(AtomicOrdering::SeqCst) {
1 => return true,
2 => return false,
_ => {}
}
{
let ov = TEST_OVERRIDE.load(AtomicOrdering::SeqCst);
if ov != 0 {
return TEST_OVERRIDE_VALUE.load(AtomicOrdering::SeqCst);
}
}
static ENABLED: OnceLock<bool> = OnceLock::new();
*ENABLED.get_or_init(|| {
if std::env::var("NO_COLOR").is_ok_and(|v| !v.is_empty()) {
return false;
}
isatty_stdout()
})
}
impl<'a> std::fmt::Display for StyledString<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let styling_active = match self.condition {
Some(false) => false,
_ => colors_enabled(),
};
if self.masked && !styling_active {
return Ok(());
}
if !styling_active || (self.fg.is_none() && self.bg.is_none() && self.styles.is_empty()) {
return f.write_str(&self.text);
}
write!(f, "\x1b[")?;
let mut first = true;
for style in &self.styles {
if !first {
write!(f, ";")?;
}
write!(f, "{}", style.code())?;
first = false;
}
if let Some(fg) = self.fg {
if !first {
write!(f, ";")?;
}
write!(f, "{}", fg.fg_code())?;
first = false;
}
if let Some(bg) = self.bg {
if !first {
write!(f, ";")?;
}
write!(f, "{}", bg.bg_code())?;
}
write!(f, "m{}\x1b[0m", self.text)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_foreground_colors() {
let cases: Vec<(fn(&str) -> StyledString<'static>, Color)> = vec![
(|s| s.black(), Color::Black),
(|s| s.red(), Color::Red),
(|s| s.green(), Color::Green),
(|s| s.yellow(), Color::Yellow),
(|s| s.blue(), Color::Blue),
(|s| s.magenta(), Color::Magenta),
(|s| s.cyan(), Color::Cyan),
(|s| s.white(), Color::White),
(|s| s.bright_black(), Color::BrightBlack),
(|s| s.bright_red(), Color::BrightRed),
(|s| s.bright_green(), Color::BrightGreen),
(|s| s.bright_yellow(), Color::BrightYellow),
(|s| s.bright_blue(), Color::BrightBlue),
(|s| s.bright_magenta(), Color::BrightMagenta),
(|s| s.bright_cyan(), Color::BrightCyan),
(|s| s.bright_white(), Color::BrightWhite),
];
for (method, expected_color) in cases {
let styled = method("hi");
assert_eq!(styled.fg, Some(expected_color));
assert_eq!(styled.bg, None);
assert!(styled.styles.is_empty());
}
}
#[test]
fn test_background_colors() {
let cases: Vec<(fn(&str) -> StyledString<'static>, Color)> = vec![
(|s| s.on_black(), Color::Black),
(|s| s.on_red(), Color::Red),
(|s| s.on_green(), Color::Green),
(|s| s.on_yellow(), Color::Yellow),
(|s| s.on_blue(), Color::Blue),
(|s| s.on_magenta(), Color::Magenta),
(|s| s.on_cyan(), Color::Cyan),
(|s| s.on_white(), Color::White),
(|s| s.on_bright_black(), Color::BrightBlack),
(|s| s.on_bright_red(), Color::BrightRed),
(|s| s.on_bright_green(), Color::BrightGreen),
(|s| s.on_bright_yellow(), Color::BrightYellow),
(|s| s.on_bright_blue(), Color::BrightBlue),
(|s| s.on_bright_magenta(), Color::BrightMagenta),
(|s| s.on_bright_cyan(), Color::BrightCyan),
(|s| s.on_bright_white(), Color::BrightWhite),
];
for (method, expected_color) in cases {
let styled = method("hi");
assert_eq!(styled.fg, None);
assert_eq!(styled.bg, Some(expected_color));
assert!(styled.styles.is_empty());
}
}
#[test]
fn test_style_methods() {
let cases: Vec<(fn(&str) -> StyledString<'static>, Style)> = vec![
(|s| s.bold(), Style::Bold),
(|s| s.dim(), Style::Dim),
(|s| s.italic(), Style::Italic),
(|s| s.underline(), Style::Underline),
(|s| s.blink(), Style::Blink),
(|s| s.rapid_blink(), Style::RapidBlink),
(|s| s.reverse(), Style::Reverse),
(|s| s.hidden(), Style::Hidden),
(|s| s.strikethrough(), Style::Strikethrough),
(|s| s.overline(), Style::Overline),
];
for (method, expected_style) in cases {
let styled = method("hi");
assert_eq!(styled.fg, None);
assert_eq!(styled.bg, None);
assert_eq!(styled.styles, vec![expected_style]);
}
}
#[test]
fn test_chaining_order_independence() {
let a = "hi".bold().italic();
let b = "hi".italic().bold();
assert!(a.styles.contains(&Style::Bold));
assert!(a.styles.contains(&Style::Italic));
assert!(b.styles.contains(&Style::Bold));
assert!(b.styles.contains(&Style::Italic));
}
#[test]
fn test_string_impl() {
let s = String::from("hello").red();
assert_eq!(s.fg, Some(Color::Red));
assert_eq!(&*s.text, "hello");
}
#[test]
fn test_display_colors_forced_on() {
with_colors_override(true, || {
let s = "hello".red();
let output = format!("{}", s);
assert_eq!(output, "\x1b[31mhello\x1b[0m");
});
}
#[test]
fn test_display_colors_forced_off() {
with_colors_override(false, || {
let s = "hello".red().bold();
let output = format!("{}", s);
assert_eq!(output, "hello");
});
}
#[test]
fn test_display_plain_styled_string() {
with_colors_override(true, || {
let s = "plain".styled();
let output = format!("{}", s);
assert_eq!(output, "plain");
});
}
#[test]
fn test_display_empty_string() {
with_colors_override(true, || {
let s = "".red();
let output = format!("{}", s);
assert_eq!(output, "\x1b[31m\x1b[0m");
});
}
#[test]
fn test_display_special_characters() {
with_colors_override(true, || {
let s = "hello\nworld\ttab".green();
let output = format!("{}", s);
assert_eq!(output, "\x1b[32mhello\nworld\ttab\x1b[0m");
});
}
#[test]
fn test_display_combined_fg_bg_style() {
with_colors_override(true, || {
let s = "hi".bold().red().on_blue();
let output = format!("{}", s);
assert_eq!(output, "\x1b[1;31;44mhi\x1b[0m");
});
}
#[test]
fn test_integer_red() {
let s = 42.red();
assert_eq!(&*s.text, "42");
assert_eq!(s.fg, Some(Color::Red));
assert_eq!(s.bg, None);
assert!(s.styles.is_empty());
}
#[test]
fn test_float_green() {
let s = 3.14_f64.green();
assert_eq!(&*s.text, "3.14");
assert_eq!(s.fg, Some(Color::Green));
}
#[test]
fn test_bool_bold() {
let s = true.bold();
assert_eq!(&*s.text, "true");
assert_eq!(s.styles, vec![Style::Bold]);
assert_eq!(s.fg, None);
}
#[test]
fn test_char_cyan() {
let s = 'x'.cyan();
assert_eq!(&*s.text, "x");
assert_eq!(s.fg, Some(Color::Cyan));
}
#[test]
fn test_style_helper_function() {
let s = style(format!("ID-{}", 42)).red().bold();
assert_eq!(&*s.text, "ID-42");
assert_eq!(s.fg, Some(Color::Red));
assert_eq!(s.styles, vec![Style::Bold]);
}
#[test]
fn test_primitive_chaining_matches_str() {
let s = 99.red().on_blue().bold();
assert_eq!(&*s.text, "99");
assert_eq!(s.fg, Some(Color::Red));
assert_eq!(s.bg, Some(Color::Blue));
assert_eq!(s.styles, vec![Style::Bold]);
}
#[test]
fn test_str_still_works() {
let s = "hello".red();
assert_eq!(&*s.text, "hello");
assert_eq!(s.fg, Some(Color::Red));
let s2 = String::from("world").green();
assert_eq!(&*s2.text, "world");
assert_eq!(s2.fg, Some(Color::Green));
}
#[test]
fn test_whenever_false_with_colors_forced_on() {
with_colors_override(true, || {
let output = format!("{}", "hello".red().whenever(false));
assert_eq!(output, "hello");
});
}
#[test]
fn test_whenever_true_with_colors_forced_off() {
with_colors_override(false, || {
let output = format!("{}", "hello".red().whenever(true));
assert_eq!(output, "hello");
});
}
#[test]
fn test_whenever_false_before_color() {
with_colors_override(true, || {
let output = format!("{}", "hi".styled().whenever(false).red());
assert_eq!(output, "hi");
});
}
#[test]
fn test_whenever_true_after_color_with_colors_on() {
with_colors_override(true, || {
let output = format!("{}", "hi".red().whenever(true));
assert_eq!(output, "\x1b[31mhi\x1b[0m");
});
}
#[test]
fn test_whenever_last_write_wins() {
with_colors_override(true, || {
let output = format!("{}", "hi".red().whenever(true).whenever(false));
assert_eq!(output, "hi");
});
}
#[test]
fn test_mask_colors_off_renders_empty() {
with_colors_override(false, || {
let output = format!("{}", "✓".green().mask());
assert_eq!(output, "");
});
}
#[test]
fn test_mask_colors_on_renders_ansi() {
with_colors_override(true, || {
let output = format!("{}", "✓".green().mask());
assert_eq!(output, "\x1b[32m✓\x1b[0m");
});
}
#[test]
fn test_mask_whenever_false_renders_empty() {
with_colors_override(true, || {
let output = format!("{}", "decorative".red().mask().whenever(false));
assert_eq!(output, "");
});
}
#[test]
fn test_mask_whenever_true_colors_on_renders_normally() {
with_colors_override(true, || {
let output = format!("{}", "decorative".red().mask().whenever(true));
assert_eq!(output, "\x1b[31mdecorative\x1b[0m");
});
}
}