use std::ops::{BitOr, BitOrAssign};
use crossterm::style::Color as CtColor;
use crate::core::terminal::ColorSupport;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Color {
Reset,
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
Gray,
DarkGray,
LightRed,
LightGreen,
LightYellow,
LightBlue,
LightMagenta,
LightCyan,
White,
Rgb(u8, u8, u8),
Indexed(u8),
}
impl Color {
pub fn from_name(name: &str) -> Option<Self> {
let color = match name.trim().to_ascii_lowercase().as_str() {
"reset" | "default" => Self::Reset,
"black" => Self::Black,
"red" => Self::Red,
"green" => Self::Green,
"yellow" => Self::Yellow,
"blue" => Self::Blue,
"magenta" | "purple" => Self::Magenta,
"cyan" => Self::Cyan,
"gray" | "grey" | "white" => Self::Gray,
"darkgray" | "darkgrey" => Self::DarkGray,
"lightred" => Self::LightRed,
"lightgreen" => Self::LightGreen,
"lightyellow" => Self::LightYellow,
"lightblue" => Self::LightBlue,
"lightmagenta" => Self::LightMagenta,
"lightcyan" => Self::LightCyan,
"brightwhite" => Self::White,
_ => return None,
};
Some(color)
}
pub fn from_hex(hex: &str) -> Option<Self> {
let digits = hex.strip_prefix('#').unwrap_or(hex);
if digits.len() != 6 {
return None;
}
let r = u8::from_str_radix(&digits[0..2], 16).ok()?;
let g = u8::from_str_radix(&digits[2..4], 16).ok()?;
let b = u8::from_str_radix(&digits[4..6], 16).ok()?;
Some(Self::Rgb(r, g, b))
}
pub(crate) fn resolve(self, support: ColorSupport) -> Option<CtColor> {
match support {
ColorSupport::None => None,
ColorSupport::TrueColor => Some(self.to_crossterm()),
ColorSupport::Ansi16 => Some(self.downgrade_to_ansi16()),
}
}
fn to_crossterm(self) -> CtColor {
match self {
Self::Reset => CtColor::Reset,
Self::Black => CtColor::Black,
Self::Red => CtColor::DarkRed,
Self::Green => CtColor::DarkGreen,
Self::Yellow => CtColor::DarkYellow,
Self::Blue => CtColor::DarkBlue,
Self::Magenta => CtColor::DarkMagenta,
Self::Cyan => CtColor::DarkCyan,
Self::Gray => CtColor::Grey,
Self::DarkGray => CtColor::DarkGrey,
Self::LightRed => CtColor::Red,
Self::LightGreen => CtColor::Green,
Self::LightYellow => CtColor::Yellow,
Self::LightBlue => CtColor::Blue,
Self::LightMagenta => CtColor::Magenta,
Self::LightCyan => CtColor::Cyan,
Self::White => CtColor::White,
Self::Rgb(r, g, b) => CtColor::Rgb { r, g, b },
Self::Indexed(i) => CtColor::AnsiValue(i),
}
}
fn downgrade_to_ansi16(self) -> CtColor {
match self {
Self::Rgb(r, g, b) => nearest_ansi16(r, g, b).to_crossterm(),
Self::Indexed(i) => indexed_to_named(i).to_crossterm(),
other => other.to_crossterm(),
}
}
}
fn nearest_ansi16(r: u8, g: u8, b: u8) -> Color {
const THRESHOLD: u8 = 0x60;
const BRIGHT: u8 = 0xc0;
let bright = r >= BRIGHT || g >= BRIGHT || b >= BRIGHT;
let bit = |channel: u8| channel >= THRESHOLD;
match (bit(r), bit(g), bit(b), bright) {
(false, false, false, false) => Color::Black,
(false, false, false, true) => Color::DarkGray,
(true, false, false, false) => Color::Red,
(true, false, false, true) => Color::LightRed,
(false, true, false, false) => Color::Green,
(false, true, false, true) => Color::LightGreen,
(true, true, false, false) => Color::Yellow,
(true, true, false, true) => Color::LightYellow,
(false, false, true, false) => Color::Blue,
(false, false, true, true) => Color::LightBlue,
(true, false, true, false) => Color::Magenta,
(true, false, true, true) => Color::LightMagenta,
(false, true, true, false) => Color::Cyan,
(false, true, true, true) => Color::LightCyan,
(true, true, true, false) => Color::Gray,
(true, true, true, true) => Color::White,
}
}
fn indexed_to_named(index: u8) -> Color {
match index {
0 => Color::Black,
1 => Color::Red,
2 => Color::Green,
3 => Color::Yellow,
4 => Color::Blue,
5 => Color::Magenta,
6 => Color::Cyan,
7 => Color::Gray,
8 => Color::DarkGray,
9 => Color::LightRed,
10 => Color::LightGreen,
11 => Color::LightYellow,
12 => Color::LightBlue,
13 => Color::LightMagenta,
14 => Color::LightCyan,
15 => Color::White,
i if i >= 244 => Color::White,
i if i >= 232 => Color::DarkGray,
_ => Color::Gray,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Attribute(u8);
impl Attribute {
pub const NONE: Self = Self(0);
pub const BOLD: Self = Self(1 << 0);
pub const DIM: Self = Self(1 << 1);
pub const ITALIC: Self = Self(1 << 2);
pub const UNDERLINED: Self = Self(1 << 3);
pub const STRIKETHROUGH: Self = Self(1 << 4);
pub fn contains(self, other: Self) -> bool {
self.0 & other.0 == other.0
}
pub fn is_empty(self) -> bool {
self.0 == 0
}
}
impl BitOr for Attribute {
type Output = Self;
fn bitor(self, rhs: Self) -> Self {
Self(self.0 | rhs.0)
}
}
impl BitOrAssign for Attribute {
fn bitor_assign(&mut self, rhs: Self) {
self.0 |= rhs.0;
}
}
pub type Modifier = Attribute;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Style {
pub fg: Option<Color>,
pub bg: Option<Color>,
pub attrs: Attribute,
}
impl Style {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn fg(mut self, color: Color) -> Self {
self.fg = Some(color);
self
}
#[must_use]
pub fn bg(mut self, color: Color) -> Self {
self.bg = Some(color);
self
}
#[must_use]
pub fn add_modifier(mut self, attr: Attribute) -> Self {
self.attrs |= attr;
self
}
#[must_use]
pub fn bold(self) -> Self {
self.add_modifier(Attribute::BOLD)
}
#[must_use]
pub fn dim(self) -> Self {
self.add_modifier(Attribute::DIM)
}
#[must_use]
pub fn italic(self) -> Self {
self.add_modifier(Attribute::ITALIC)
}
#[must_use]
pub fn underlined(self) -> Self {
self.add_modifier(Attribute::UNDERLINED)
}
#[must_use]
pub fn strikethrough(self) -> Self {
self.add_modifier(Attribute::STRIKETHROUGH)
}
#[must_use]
pub fn patch(mut self, other: Style) -> Self {
if other.fg.is_some() {
self.fg = other.fg;
}
if other.bg.is_some() {
self.bg = other.bg;
}
self.attrs |= other.attrs;
self
}
}
impl From<Color> for Style {
fn from(color: Color) -> Self {
Style::new().fg(color)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_hex_parses_six_digit_colors() {
assert_eq!(Color::from_hex("#ff8800"), Some(Color::Rgb(255, 136, 0)));
assert_eq!(Color::from_hex("8899aa"), Some(Color::Rgb(136, 153, 170)));
}
#[test]
fn from_hex_rejects_malformed_input() {
assert_eq!(Color::from_hex("#fff"), None);
assert_eq!(Color::from_hex("#gggggg"), None);
}
#[test]
fn from_name_is_case_insensitive() {
assert_eq!(Color::from_name("RED"), Some(Color::Red));
assert_eq!(Color::from_name("LightBlue"), Some(Color::LightBlue));
assert_eq!(Color::from_name("nope"), None);
}
#[test]
fn attributes_combine_and_test_membership() {
let attrs = Attribute::BOLD | Attribute::ITALIC;
assert!(attrs.contains(Attribute::BOLD));
assert!(attrs.contains(Attribute::ITALIC));
assert!(!attrs.contains(Attribute::DIM));
}
#[test]
fn patch_overrides_set_fields_only() {
let base = Style::new().fg(Color::Red).bold();
let patched = base.patch(Style::new().bg(Color::Blue).italic());
assert_eq!(patched.fg, Some(Color::Red));
assert_eq!(patched.bg, Some(Color::Blue));
assert!(patched.attrs.contains(Attribute::BOLD));
assert!(patched.attrs.contains(Attribute::ITALIC));
}
#[test]
fn none_support_emits_no_color() {
assert_eq!(Color::Red.resolve(ColorSupport::None), None);
}
#[test]
fn ansi16_downgrades_rgb_to_named() {
let resolved = Color::Rgb(200, 0, 0).resolve(ColorSupport::Ansi16);
assert_eq!(resolved, Some(CtColor::Red));
}
}