use core::fmt;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum Color {
#[default]
Reset,
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
Gray,
DarkGray,
LightRed,
LightGreen,
LightYellow,
LightBlue,
LightMagenta,
LightCyan,
White,
Rgb(u8, u8, u8),
Indexed(u8),
}
#[derive(Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct Modifier(u16);
impl Modifier {
pub const BOLD: Self = Self(0b0000_0000_0001);
pub const DIM: Self = Self(0b0000_0000_0010);
pub const ITALIC: Self = Self(0b0000_0000_0100);
pub const UNDERLINED: Self = Self(0b0000_0000_1000);
pub const SLOW_BLINK: Self = Self(0b0000_0001_0000);
pub const RAPID_BLINK: Self = Self(0b0000_0010_0000);
pub const REVERSED: Self = Self(0b0000_0100_0000);
pub const HIDDEN: Self = Self(0b0000_1000_0000);
pub const CROSSED_OUT: Self = Self(0b0001_0000_0000);
const NAMED: [(&'static str, Self); 9] = [
("BOLD", Self::BOLD),
("DIM", Self::DIM),
("ITALIC", Self::ITALIC),
("UNDERLINED", Self::UNDERLINED),
("SLOW_BLINK", Self::SLOW_BLINK),
("RAPID_BLINK", Self::RAPID_BLINK),
("REVERSED", Self::REVERSED),
("HIDDEN", Self::HIDDEN),
("CROSSED_OUT", Self::CROSSED_OUT),
];
pub const fn empty() -> Self {
Self(0)
}
pub const fn all() -> Self {
Self(0b0001_1111_1111)
}
pub const fn is_empty(self) -> bool {
self.0 == 0
}
pub const fn contains(self, other: Self) -> bool {
self.0 & other.0 == other.0
}
#[must_use = "returns a new Modifier"]
pub const fn union(self, other: Self) -> Self {
Self(self.0 | other.0)
}
#[must_use = "returns a new Modifier"]
pub const fn intersection(self, other: Self) -> Self {
Self(self.0 & other.0)
}
#[must_use = "returns a new Modifier"]
pub const fn difference(self, other: Self) -> Self {
Self(self.0 & !other.0)
}
pub const fn bits(self) -> u16 {
self.0
}
}
impl core::ops::BitOr for Modifier {
type Output = Self;
fn bitor(self, rhs: Self) -> Self {
self.union(rhs)
}
}
impl core::ops::BitOrAssign for Modifier {
fn bitor_assign(&mut self, rhs: Self) {
*self = self.union(rhs);
}
}
impl core::ops::BitAnd for Modifier {
type Output = Self;
fn bitand(self, rhs: Self) -> Self {
self.intersection(rhs)
}
}
impl core::ops::Sub for Modifier {
type Output = Self;
fn sub(self, rhs: Self) -> Self {
self.difference(rhs)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct Style {
pub fg: Option<Color>,
pub bg: Option<Color>,
pub underline_color: Option<Color>,
pub add_modifier: Modifier,
pub sub_modifier: Modifier,
}
impl Style {
pub const fn new() -> Self {
Self {
fg: None,
bg: None,
underline_color: None,
add_modifier: Modifier::empty(),
sub_modifier: Modifier::empty(),
}
}
pub const fn reset() -> Self {
Self {
fg: Some(Color::Reset),
bg: Some(Color::Reset),
underline_color: Some(Color::Reset),
add_modifier: Modifier::empty(),
sub_modifier: Modifier::all(),
}
}
#[must_use = "returns a new Style"]
pub const fn fg(mut self, color: Color) -> Self {
self.fg = Some(color);
self
}
#[must_use = "returns a new Style"]
pub const fn bg(mut self, color: Color) -> Self {
self.bg = Some(color);
self
}
#[must_use = "returns a new Style"]
pub const fn underline_color(mut self, color: Color) -> Self {
self.underline_color = Some(color);
self
}
#[must_use = "returns a new Style"]
pub const fn add_modifier(mut self, modifier: Modifier) -> Self {
self.sub_modifier = self.sub_modifier.difference(modifier);
self.add_modifier = self.add_modifier.union(modifier);
self
}
#[must_use = "returns a new Style"]
pub const fn remove_modifier(mut self, modifier: Modifier) -> Self {
self.add_modifier = self.add_modifier.difference(modifier);
self.sub_modifier = self.sub_modifier.union(modifier);
self
}
pub const fn has_modifier(self, modifier: Modifier) -> bool {
self.add_modifier.contains(modifier)
}
#[must_use = "returns a new Style"]
pub const fn bold(self) -> Self {
self.add_modifier(Modifier::BOLD)
}
#[must_use = "returns a new Style"]
pub const fn dim(self) -> Self {
self.add_modifier(Modifier::DIM)
}
#[must_use = "returns a new Style"]
pub const fn italic(self) -> Self {
self.add_modifier(Modifier::ITALIC)
}
#[must_use = "returns a new Style"]
pub const fn underlined(self) -> Self {
self.add_modifier(Modifier::UNDERLINED)
}
#[must_use = "returns a new Style"]
pub const fn reversed(self) -> Self {
self.add_modifier(Modifier::REVERSED)
}
#[must_use = "returns a new Style"]
pub const fn crossed_out(self) -> Self {
self.add_modifier(Modifier::CROSSED_OUT)
}
#[must_use = "returns a new Style"]
pub fn patch<S: Into<Self>>(mut self, other: S) -> Self {
let other = other.into();
self.fg = other.fg.or(self.fg);
self.bg = other.bg.or(self.bg);
self.underline_color = other.underline_color.or(self.underline_color);
self.add_modifier = self.add_modifier.difference(other.sub_modifier);
self.add_modifier = self.add_modifier.union(other.add_modifier);
self.sub_modifier = self.sub_modifier.difference(other.add_modifier);
self.sub_modifier = self.sub_modifier.union(other.sub_modifier);
self
}
}
impl From<Color> for Style {
fn from(color: Color) -> Self {
Self::new().fg(color)
}
}
impl From<Modifier> for Style {
fn from(modifier: Modifier) -> Self {
Self::new().add_modifier(modifier)
}
}
impl fmt::Debug for Modifier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_empty() {
return f.write_str("NONE");
}
let mut first = true;
for (name, bit) in Self::NAMED {
if self.contains(bit) {
if !first {
f.write_str(" | ")?;
}
f.write_str(name)?;
first = false;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn patch_keeps_unset_fields_and_replaces_set_ones() {
let base = Style::new().fg(Color::Red).bg(Color::Blue);
let over = Style::new().fg(Color::Green);
let merged = base.patch(over);
assert_eq!(merged.fg, Some(Color::Green));
assert_eq!(merged.bg, Some(Color::Blue));
}
#[test]
fn patch_accumulates_added_modifiers() {
let merged = Style::new()
.add_modifier(Modifier::BOLD)
.patch(Style::new().add_modifier(Modifier::ITALIC));
assert!(merged.add_modifier.contains(Modifier::BOLD));
assert!(merged.add_modifier.contains(Modifier::ITALIC));
}
#[test]
fn a_later_removal_overrides_an_earlier_add() {
let merged = Style::new()
.add_modifier(Modifier::BOLD)
.patch(Style::new().remove_modifier(Modifier::BOLD));
assert!(!merged.add_modifier.contains(Modifier::BOLD));
assert!(merged.sub_modifier.contains(Modifier::BOLD));
}
#[test]
fn add_and_remove_are_mutually_exclusive_on_one_style() {
let removed = Style::new()
.add_modifier(Modifier::DIM)
.remove_modifier(Modifier::DIM);
assert!(!removed.add_modifier.contains(Modifier::DIM));
assert!(removed.sub_modifier.contains(Modifier::DIM));
let added = Style::new()
.remove_modifier(Modifier::DIM)
.add_modifier(Modifier::DIM);
assert!(added.add_modifier.contains(Modifier::DIM));
assert!(!added.sub_modifier.contains(Modifier::DIM));
}
#[test]
fn reset_clears_every_attribute() {
let reset = Style::reset();
assert_eq!(reset.fg, Some(Color::Reset));
assert_eq!(reset.bg, Some(Color::Reset));
assert_eq!(reset.sub_modifier, Modifier::all());
}
#[test]
fn empty_modifier_debugs_as_none() {
assert_eq!(format!("{:?}", Modifier::empty()), "NONE");
assert_eq!(format!("{:?}", Modifier::BOLD), "BOLD");
assert_eq!(
format!("{:?}", Modifier::BOLD | Modifier::ITALIC),
"BOLD | ITALIC"
);
}
#[test]
fn a_color_converts_to_a_foreground_style() {
assert_eq!(Style::from(Color::Red).fg, Some(Color::Red));
}
}