use parking_lot::Once;
use std::fmt::{self, Formatter};
use std::sync::atomic::{AtomicBool, Ordering};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::style::{parse_next_u8, Color};
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)]
pub enum Colored {
ForegroundColor(Color),
BackgroundColor(Color),
UnderlineColor(Color),
}
static ANSI_COLOR_DISABLED: AtomicBool = AtomicBool::new(false);
static INITIALIZER: Once = Once::new();
impl Colored {
pub fn parse_ansi(ansi: &str) -> Option<Self> {
use Colored::{BackgroundColor, ForegroundColor, UnderlineColor};
let values = &mut ansi.split(';');
let output = match parse_next_u8(values)? {
38 => return Color::parse_ansi_iter(values).map(ForegroundColor),
48 => return Color::parse_ansi_iter(values).map(BackgroundColor),
58 => return Color::parse_ansi_iter(values).map(UnderlineColor),
39 => ForegroundColor(Color::Reset),
49 => BackgroundColor(Color::Reset),
59 => UnderlineColor(Color::Reset),
_ => return None,
};
if values.next().is_some() {
return None;
}
Some(output)
}
pub fn ansi_color_disabled() -> bool {
!std::env::var("NO_COLOR")
.unwrap_or("".to_string())
.is_empty()
}
pub fn ansi_color_disabled_memoized() -> bool {
INITIALIZER.call_once(|| {
ANSI_COLOR_DISABLED.store(Self::ansi_color_disabled(), Ordering::SeqCst);
});
ANSI_COLOR_DISABLED.load(Ordering::SeqCst)
}
pub fn set_ansi_color_disabled(val: bool) {
_ = Self::ansi_color_disabled_memoized();
ANSI_COLOR_DISABLED.store(val, Ordering::SeqCst);
}
}
impl fmt::Display for Colored {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let color;
if Self::ansi_color_disabled_memoized() {
return Ok(());
}
match *self {
Colored::ForegroundColor(new_color) => {
if new_color == Color::Reset {
return f.write_str("39");
} else {
f.write_str("38;")?;
color = new_color;
}
}
Colored::BackgroundColor(new_color) => {
if new_color == Color::Reset {
return f.write_str("49");
} else {
f.write_str("48;")?;
color = new_color;
}
}
Colored::UnderlineColor(new_color) => {
if new_color == Color::Reset {
return f.write_str("59");
} else {
f.write_str("58;")?;
color = new_color;
}
}
}
match color {
Color::Black => f.write_str("5;0"),
Color::DarkGrey => f.write_str("5;8"),
Color::Red => f.write_str("5;9"),
Color::DarkRed => f.write_str("5;1"),
Color::Green => f.write_str("5;10"),
Color::DarkGreen => f.write_str("5;2"),
Color::Yellow => f.write_str("5;11"),
Color::DarkYellow => f.write_str("5;3"),
Color::Blue => f.write_str("5;12"),
Color::DarkBlue => f.write_str("5;4"),
Color::Magenta => f.write_str("5;13"),
Color::DarkMagenta => f.write_str("5;5"),
Color::Cyan => f.write_str("5;14"),
Color::DarkCyan => f.write_str("5;6"),
Color::White => f.write_str("5;15"),
Color::Grey => f.write_str("5;7"),
Color::Rgb { r, g, b } => write!(f, "2;{r};{g};{b}"),
Color::AnsiValue(val) => write!(f, "5;{val}"),
_ => Ok(()),
}
}
}
#[cfg(test)]
mod tests {
use crate::style::{Color, Colored};
fn check_format_color(colored: Colored, expected: &str) {
Colored::set_ansi_color_disabled(true);
assert_eq!(colored.to_string(), "");
Colored::set_ansi_color_disabled(false);
assert_eq!(colored.to_string(), expected);
}
#[test]
fn test_format_fg_color() {
let colored = Colored::ForegroundColor(Color::Red);
check_format_color(colored, "38;5;9");
}
#[test]
fn test_format_bg_color() {
let colored = Colored::BackgroundColor(Color::Red);
check_format_color(colored, "48;5;9");
}
#[test]
fn test_format_reset_fg_color() {
let colored = Colored::ForegroundColor(Color::Reset);
check_format_color(colored, "39");
}
#[test]
fn test_format_reset_bg_color() {
let colored = Colored::BackgroundColor(Color::Reset);
check_format_color(colored, "49");
}
#[test]
fn test_format_fg_rgb_color() {
let colored = Colored::BackgroundColor(Color::Rgb { r: 1, g: 2, b: 3 });
check_format_color(colored, "48;2;1;2;3");
}
#[test]
fn test_format_fg_ansi_color() {
let colored = Colored::ForegroundColor(Color::AnsiValue(255));
check_format_color(colored, "38;5;255");
}
#[test]
fn test_parse_ansi_fg() {
test_parse_ansi(Colored::ForegroundColor)
}
#[test]
fn test_parse_ansi_bg() {
test_parse_ansi(Colored::ForegroundColor)
}
fn test_parse_ansi(bg_or_fg: impl Fn(Color) -> Colored) {
macro_rules! test {
($color:expr) => {
let colored = bg_or_fg($color);
assert_eq!(Colored::parse_ansi(&format!("{}", colored)), Some(colored));
};
}
use Color::*;
test!(Reset);
test!(Black);
test!(DarkGrey);
test!(Red);
test!(DarkRed);
test!(Green);
test!(DarkGreen);
test!(Yellow);
test!(DarkYellow);
test!(Blue);
test!(DarkBlue);
test!(Magenta);
test!(DarkMagenta);
test!(Cyan);
test!(DarkCyan);
test!(White);
test!(Grey);
for n in 16..=255 {
test!(AnsiValue(n));
}
for r in 0..=255 {
for g in [0, 2, 18, 19, 60, 100, 200, 250, 254, 255].iter().copied() {
for b in [0, 12, 16, 99, 100, 161, 200, 255].iter().copied() {
test!(Rgb { r, g, b });
}
}
}
}
#[test]
fn test_parse_invalid_ansi_color() {
fn test(s: &str) {
assert_eq!(Colored::parse_ansi(s), None);
}
test("");
test(";");
test(";;");
test(";;");
test("0");
test("1");
test("12");
test("100");
test("100048949345");
test("39;");
test("49;");
test("39;2");
test("49;2");
test("38");
test("38;");
test("38;0");
test("38;5");
test("38;5;0;");
test("38;5;0;2");
test("38;5;80;");
test("38;5;80;2");
test("38;5;257");
test("38;2");
test("38;2;");
test("38;2;0");
test("38;2;0;2");
test("38;2;0;2;257");
test("38;2;0;2;25;");
test("38;2;0;2;25;3");
test("48");
test("48;");
test("48;0");
test("48;5");
test("48;5;0;");
test("48;5;0;2");
test("48;5;80;");
test("48;5;80;2");
test("48;5;257");
test("48;2");
test("48;2;");
test("48;2;0");
test("48;2;0;2");
test("48;2;0;2;257");
test("48;2;0;2;25;");
test("48;2;0;2;25;3");
}
#[test]
fn test_no_color() {
std::env::set_var("NO_COLOR", "1");
assert!(Colored::ansi_color_disabled());
std::env::set_var("NO_COLOR", "XXX");
assert!(Colored::ansi_color_disabled());
std::env::set_var("NO_COLOR", "");
assert!(!Colored::ansi_color_disabled());
std::env::remove_var("NO_COLOR");
assert!(!Colored::ansi_color_disabled());
}
}