use crate::terminal::detect::{ColorSupport, color_support};
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum Color {
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
BrightBlack,
BrightRed,
BrightGreen,
BrightYellow,
BrightBlue,
BrightMagenta,
BrightCyan,
BrightWhite,
Color256(u8),
Rgb(u8, u8, u8),
}
impl Color {
pub fn render_fg(&self) -> String {
match self {
Self::Black => "\x1b[30m".to_string(),
Self::Red => "\x1b[31m".to_string(),
Self::Green => "\x1b[32m".to_string(),
Self::Yellow => "\x1b[33m".to_string(),
Self::Blue => "\x1b[34m".to_string(),
Self::Magenta => "\x1b[35m".to_string(),
Self::Cyan => "\x1b[36m".to_string(),
Self::White => "\x1b[37m".to_string(),
Self::BrightBlack => "\x1b[90m".to_string(),
Self::BrightRed => "\x1b[91m".to_string(),
Self::BrightGreen => "\x1b[92m".to_string(),
Self::BrightYellow => "\x1b[93m".to_string(),
Self::BrightBlue => "\x1b[94m".to_string(),
Self::BrightMagenta => "\x1b[95m".to_string(),
Self::BrightCyan => "\x1b[96m".to_string(),
Self::BrightWhite => "\x1b[97m".to_string(),
Self::Color256(n) => format!("\x1b[38;5;{n}m"),
Self::Rgb(r, g, b) => format!("\x1b[38;2;{r};{g};{b}m"),
}
}
pub fn render_bg(&self) -> String {
match self {
Self::Black => "\x1b[40m".to_string(),
Self::Red => "\x1b[41m".to_string(),
Self::Green => "\x1b[42m".to_string(),
Self::Yellow => "\x1b[43m".to_string(),
Self::Blue => "\x1b[44m".to_string(),
Self::Magenta => "\x1b[45m".to_string(),
Self::Cyan => "\x1b[46m".to_string(),
Self::White => "\x1b[47m".to_string(),
Self::BrightBlack => "\x1b[100m".to_string(),
Self::BrightRed => "\x1b[101m".to_string(),
Self::BrightGreen => "\x1b[102m".to_string(),
Self::BrightYellow => "\x1b[103m".to_string(),
Self::BrightBlue => "\x1b[104m".to_string(),
Self::BrightMagenta => "\x1b[105m".to_string(),
Self::BrightCyan => "\x1b[106m".to_string(),
Self::BrightWhite => "\x1b[107m".to_string(),
Self::Color256(n) => format!("\x1b[48;5;{n}m"),
Self::Rgb(r, g, b) => format!("\x1b[48;2;{r};{g};{b}m"),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ColorSpec {
pub fg: Option<Color>,
pub bg: Option<Color>,
pub bold: bool,
pub dim: bool,
pub italic: bool,
pub underline: bool,
pub blink: bool,
pub reverse: bool,
pub strikethrough: bool,
}
impl ColorSpec {
pub fn new() -> Self {
Self::default()
}
pub fn set_fg(mut self, color: Color) -> Self {
self.fg = Some(color);
self
}
pub fn set_bg(mut self, color: Color) -> Self {
self.bg = Some(color);
self
}
pub fn set_bold(mut self, val: bool) -> Self {
self.bold = val;
self
}
pub fn set_dim(mut self, val: bool) -> Self {
self.dim = val;
self
}
pub fn set_italic(mut self, val: bool) -> Self {
self.italic = val;
self
}
pub fn set_underline(mut self, val: bool) -> Self {
self.underline = val;
self
}
pub fn set_blink(mut self, val: bool) -> Self {
self.blink = val;
self
}
pub fn set_reverse(mut self, val: bool) -> Self {
self.reverse = val;
self
}
pub fn set_strikethrough(mut self, val: bool) -> Self {
self.strikethrough = val;
self
}
pub fn render(&self, text: &str) -> String {
if !ansi_enabled() {
return text.to_string();
}
let mut out = String::new();
if self.bold {
out.push_str("\x1b[1m");
}
if self.dim {
out.push_str("\x1b[2m");
}
if self.italic {
out.push_str("\x1b[3m");
}
if self.underline {
out.push_str("\x1b[4m");
}
if self.blink {
out.push_str("\x1b[5m");
}
if self.reverse {
out.push_str("\x1b[7m");
}
if self.strikethrough {
out.push_str("\x1b[9m");
}
if let Some(fg) = &self.fg {
out.push_str(&fg.render_fg());
}
if let Some(bg) = &self.bg {
out.push_str(&bg.render_bg());
}
out.push_str(text);
out.push_str(Self::reset_code());
out
}
pub fn reset_code() -> &'static str {
"\x1b[0m"
}
}
pub fn ansi_enabled() -> bool {
std::env::var_os("NO_COLOR").is_none() && color_support() != ColorSupport::None
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
fn with_env_lock(f: impl FnOnce()) {
match ENV_LOCK.lock() {
Ok(_guard) => f(),
Err(poisoned) => {
let _guard = poisoned.into_inner();
f();
}
}
}
#[test]
fn test_color16_fg_code() {
assert_eq!(Color::Red.render_fg(), "\x1b[31m");
}
#[test]
fn test_color16_bg_code() {
assert_eq!(Color::Blue.render_bg(), "\x1b[44m");
}
#[test]
fn test_color256_fg_code() {
assert_eq!(Color::Color256(42).render_fg(), "\x1b[38;5;42m");
}
#[test]
fn test_rgb_fg_code() {
assert_eq!(Color::Rgb(1, 2, 3).render_fg(), "\x1b[38;2;1;2;3m");
}
#[test]
fn test_colorspec_bold() {
let rendered = ColorSpec::new().set_bold(true).render("x");
assert!(rendered == "x" || rendered.contains("\x1b[1m"));
}
#[test]
fn test_colorspec_combined() {
let spec = ColorSpec::new()
.set_bold(true)
.set_fg(Color::Cyan)
.set_bg(Color::Black);
let rendered = spec.render("x");
assert!(rendered == "x" || rendered.contains("\x1b[36m"));
}
#[test]
fn test_no_color_env_suppresses_ansi() {
with_env_lock(|| {
unsafe {
env::set_var("NO_COLOR", "1");
}
assert_eq!(ColorSpec::new().set_bold(true).render("x"), "x");
unsafe {
env::remove_var("NO_COLOR");
}
});
}
#[test]
fn test_render_wraps_text() {
let rendered = ColorSpec::new().set_fg(Color::Green).render("ok");
assert!(rendered == "ok" || rendered.ends_with("\x1b[0m"));
}
#[test]
fn test_reset_code_value() {
assert_eq!(ColorSpec::reset_code(), "\x1b[0m");
}
}