use super::attributes::{Attribute, AttributeSet};
use super::color::Color;
use std::fmt;
use thiserror::Error;
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub struct Style {
foreground: Option<Color>,
background: Option<Color>,
enabled_attributes: AttributeSet,
disabled_attributes: AttributeSet,
}
impl Style {
pub const fn new() -> Style {
Style {
foreground: None,
background: None,
enabled_attributes: AttributeSet::EMPTY,
disabled_attributes: AttributeSet::EMPTY,
}
}
pub const fn foreground(mut self, fg: Option<Color>) -> Style {
self.foreground = fg;
self
}
pub const fn background(mut self, bg: Option<Color>) -> Style {
self.background = bg;
self
}
pub fn enabled_attributes<A: Into<AttributeSet>>(mut self, attrs: A) -> Style {
self.enabled_attributes = attrs.into();
self.disabled_attributes -= self.enabled_attributes;
self
}
pub fn disabled_attributes<A: Into<AttributeSet>>(mut self, attrs: A) -> Style {
self.disabled_attributes = attrs.into();
self.enabled_attributes -= self.disabled_attributes;
self
}
pub fn is_empty(self) -> bool {
self.foreground.is_none()
&& self.background.is_none()
&& self.enabled_attributes.is_empty()
&& self.disabled_attributes.is_empty()
}
pub fn is_enabled(self, attr: Attribute) -> bool {
self.enabled_attributes.contains(attr) && !self.disabled_attributes.contains(attr)
}
pub fn is_disabled(self, attr: Attribute) -> bool {
self.disabled_attributes.contains(attr) && !self.enabled_attributes.contains(attr)
}
pub const fn get_foreground(self) -> Option<Color> {
self.foreground
}
pub const fn get_background(self) -> Option<Color> {
self.background
}
pub const fn get_enabled_attributes(self) -> AttributeSet {
self.enabled_attributes
}
pub const fn get_disabled_attributes(self) -> AttributeSet {
self.disabled_attributes
}
pub fn patch(self, other: Style) -> Style {
let foreground = self.foreground.or(other.foreground);
let background = self.background.or(other.background);
let enabled_attributes =
(self.enabled_attributes - other.disabled_attributes) | other.enabled_attributes;
let disabled_attributes =
(self.disabled_attributes - other.enabled_attributes) | other.disabled_attributes;
Style {
foreground,
background,
enabled_attributes,
disabled_attributes,
}
}
pub fn enable<A: Into<AttributeSet>>(mut self, attrs: A) -> Style {
let attrs = attrs.into();
self.enabled_attributes |= attrs;
self.disabled_attributes -= attrs;
self
}
pub fn disable<A: Into<AttributeSet>>(mut self, attrs: A) -> Style {
let attrs = attrs.into();
self.enabled_attributes -= attrs;
self.disabled_attributes |= attrs;
self
}
pub fn bold(self) -> Style {
self.enable(Attribute::Bold)
}
pub fn dim(self) -> Style {
self.enable(Attribute::Dim)
}
pub fn italic(self) -> Style {
self.enable(Attribute::Italic)
}
pub fn underline(self) -> Style {
self.enable(Attribute::Underline)
}
pub fn blink(self) -> Style {
self.enable(Attribute::Blink)
}
pub fn blink2(self) -> Style {
self.enable(Attribute::Blink2)
}
pub fn reverse(self) -> Style {
self.enable(Attribute::Reverse)
}
pub fn conceal(self) -> Style {
self.enable(Attribute::Conceal)
}
pub fn strike(self) -> Style {
self.enable(Attribute::Strike)
}
pub fn underline2(self) -> Style {
self.enable(Attribute::Underline2)
}
pub fn frame(self) -> Style {
self.enable(Attribute::Frame)
}
pub fn encircle(self) -> Style {
self.enable(Attribute::Encircle)
}
pub fn overline(self) -> Style {
self.enable(Attribute::Overline)
}
pub fn not_bold(self) -> Style {
self.disable(Attribute::Bold)
}
pub fn not_dim(self) -> Style {
self.disable(Attribute::Dim)
}
pub fn not_italic(self) -> Style {
self.disable(Attribute::Italic)
}
pub fn not_underline(self) -> Style {
self.disable(Attribute::Underline)
}
pub fn not_blink(self) -> Style {
self.disable(Attribute::Blink)
}
pub fn not_blink2(self) -> Style {
self.disable(Attribute::Blink2)
}
pub fn not_reverse(self) -> Style {
self.disable(Attribute::Reverse)
}
pub fn not_conceal(self) -> Style {
self.disable(Attribute::Conceal)
}
pub fn not_strike(self) -> Style {
self.disable(Attribute::Strike)
}
pub fn not_underline2(self) -> Style {
self.disable(Attribute::Underline2)
}
pub fn not_frame(self) -> Style {
self.disable(Attribute::Frame)
}
pub fn not_encircle(self) -> Style {
self.disable(Attribute::Encircle)
}
pub fn not_overline(self) -> Style {
self.disable(Attribute::Overline)
}
}
impl<C: Into<Color>> From<C> for Style {
fn from(value: C) -> Style {
Style::new().foreground(Some(value.into()))
}
}
impl From<Attribute> for Style {
fn from(value: Attribute) -> Style {
Style::new().enable(value)
}
}
impl From<AttributeSet> for Style {
fn from(value: AttributeSet) -> Style {
Style::new().enabled_attributes(value)
}
}
#[cfg(feature = "anstyle")]
#[cfg_attr(docsrs, doc(cfg(feature = "anstyle")))]
impl From<Style> for anstyle::Style {
fn from(value: Style) -> anstyle::Style {
anstyle::Style::new()
.fg_color(
value
.get_foreground()
.and_then(|c| anstyle::Color::try_from(c).ok()),
)
.bg_color(
value
.get_background()
.and_then(|c| anstyle::Color::try_from(c).ok()),
)
.effects(value.enabled_attributes.into())
}
}
#[cfg(feature = "anstyle")]
#[cfg_attr(docsrs, doc(cfg(feature = "anstyle")))]
impl From<anstyle::Style> for Style {
fn from(value: anstyle::Style) -> Style {
Style::new()
.foreground(value.get_fg_color().map(Color::from))
.background(value.get_bg_color().map(Color::from))
.enabled_attributes(AttributeSet::from(value.get_effects()))
}
}
#[cfg(feature = "crossterm")]
#[cfg_attr(docsrs, doc(cfg(feature = "crossterm")))]
impl From<crossterm::style::Attributes> for Style {
fn from(value: crossterm::style::Attributes) -> Style {
use crossterm::style::Attribute as CrossAttrib;
let mut set = Style::new();
for attr in CrossAttrib::iterator().filter(|&attr| value.has(attr)) {
match attr {
CrossAttrib::Reset => set = Style::new(),
CrossAttrib::Bold => set = set.bold(),
CrossAttrib::Dim => set = set.dim(),
CrossAttrib::Italic => set = set.italic(),
CrossAttrib::Underlined => set = set.underline(),
CrossAttrib::DoubleUnderlined => set = set.underline2(),
CrossAttrib::Undercurled => (),
CrossAttrib::Underdotted => (),
CrossAttrib::Underdashed => (),
CrossAttrib::SlowBlink => set = set.blink(),
CrossAttrib::RapidBlink => set = set.blink2(),
CrossAttrib::Reverse => set = set.reverse(),
CrossAttrib::Hidden => set = set.conceal(),
CrossAttrib::CrossedOut => set = set.strike(),
CrossAttrib::Fraktur => (),
CrossAttrib::NoBold => (),
CrossAttrib::NormalIntensity => set = set.not_bold().not_dim(),
CrossAttrib::NoItalic => set = set.not_italic(),
CrossAttrib::NoUnderline => set = set.not_underline().not_underline2(),
CrossAttrib::NoBlink => set = set.not_blink().not_blink2(),
CrossAttrib::NoReverse => set = set.not_reverse(),
CrossAttrib::NoHidden => set = set.not_conceal(),
CrossAttrib::NotCrossedOut => set = set.not_strike(),
CrossAttrib::Framed => set = set.frame(),
CrossAttrib::Encircled => set = set.encircle(),
CrossAttrib::OverLined => set = set.overline(),
CrossAttrib::NotFramedOrEncircled => set = set.not_frame().not_encircle(),
CrossAttrib::NotOverLined => set = set.not_overline(),
_ => (), }
}
set
}
}
#[cfg(feature = "crossterm")]
#[cfg_attr(docsrs, doc(cfg(feature = "crossterm")))]
impl From<Style> for crossterm::style::ContentStyle {
fn from(value: Style) -> crossterm::style::ContentStyle {
use crossterm::style::Attribute as CrossAttrib;
let foreground_color = value.foreground.map(crossterm::style::Color::from);
let background_color = value.background.map(crossterm::style::Color::from);
let mut attributes = crossterm::style::Attributes::from(value.enabled_attributes);
for attr in value.disabled_attributes {
match attr {
Attribute::Bold => attributes.set(CrossAttrib::NormalIntensity),
Attribute::Dim => attributes.set(CrossAttrib::NormalIntensity),
Attribute::Italic => attributes.set(CrossAttrib::NoItalic),
Attribute::Underline => attributes.set(CrossAttrib::NoUnderline),
Attribute::Blink => attributes.set(CrossAttrib::NoBlink),
Attribute::Blink2 => attributes.set(CrossAttrib::NoBlink),
Attribute::Reverse => attributes.set(CrossAttrib::NoReverse),
Attribute::Conceal => attributes.set(CrossAttrib::NoHidden),
Attribute::Strike => attributes.set(CrossAttrib::NotCrossedOut),
Attribute::Underline2 => attributes.set(CrossAttrib::NoUnderline),
Attribute::Frame => attributes.set(CrossAttrib::NotFramedOrEncircled),
Attribute::Encircle => attributes.set(CrossAttrib::NotFramedOrEncircled),
Attribute::Overline => attributes.set(CrossAttrib::NotOverLined),
}
}
crossterm::style::ContentStyle {
foreground_color,
background_color,
attributes,
underline_color: None,
}
}
}
#[cfg(feature = "crossterm")]
#[cfg_attr(docsrs, doc(cfg(feature = "crossterm")))]
impl From<crossterm::style::ContentStyle> for Style {
fn from(value: crossterm::style::ContentStyle) -> Style {
Style::from(value.attributes)
.foreground(value.foreground_color.map(Color::from))
.background(value.background_color.map(Color::from))
}
}
#[cfg(feature = "ratatui")]
#[cfg_attr(docsrs, doc(cfg(feature = "ratatui")))]
impl From<Style> for ratatui_core::style::Style {
fn from(value: Style) -> ratatui_core::style::Style {
let mut style = ratatui_core::style::Style::new();
if let Some(fg) = value.foreground.map(ratatui_core::style::Color::from) {
style = style.fg(fg);
}
if let Some(bg) = value.background.map(ratatui_core::style::Color::from) {
style = style.bg(bg);
}
style = style.add_modifier(value.enabled_attributes.into());
style = style.remove_modifier(value.disabled_attributes.into());
style
}
}
#[cfg(feature = "ratatui")]
#[cfg_attr(docsrs, doc(cfg(feature = "ratatui")))]
impl From<ratatui_core::style::Style> for Style {
fn from(value: ratatui_core::style::Style) -> Style {
let foreground = value.fg.map(Color::from);
let background = value.bg.map(Color::from);
let enabled_attributes = AttributeSet::from(value.add_modifier);
let disabled_attributes = AttributeSet::from(value.sub_modifier);
Style {
foreground,
background,
enabled_attributes,
disabled_attributes,
}
}
}
impl fmt::Display for Style {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut first = true;
for attr in Attribute::iter() {
if self.is_enabled(attr) {
if !std::mem::replace(&mut first, false) {
write!(f, " ")?;
}
if f.alternate() {
write!(f, "{attr:#}")?;
} else {
write!(f, "{attr}")?;
}
} else if self.is_disabled(attr) {
if !std::mem::replace(&mut first, false) {
write!(f, " ")?;
}
if f.alternate() {
write!(f, "not {attr:#}")?;
} else {
write!(f, "not {attr}")?;
}
}
}
if let Some(fg) = self.foreground {
if !std::mem::replace(&mut first, false) {
write!(f, " ")?;
}
write!(f, "{fg}")?;
}
if let Some(bg) = self.background {
if !std::mem::replace(&mut first, false) {
write!(f, " ")?;
}
write!(f, "on {bg}")?;
}
if first {
write!(f, "none")?;
}
Ok(())
}
}
impl std::str::FromStr for Style {
type Err = ParseStyleError;
fn from_str(s: &str) -> Result<Style, ParseStyleError> {
let mut style = Style::new();
if s.is_empty() || s.trim().eq_ignore_ascii_case("none") {
return Ok(style);
}
let mut words = s.split_whitespace();
while let Some(token) = words.next() {
if token.eq_ignore_ascii_case("on") {
let Some(bg) = words.next().and_then(|s| s.parse::<Color>().ok()) else {
return Err(ParseStyleError::MissingBackground);
};
style.background = Some(bg);
} else if token.eq_ignore_ascii_case("not") {
let Some(attr) = words.next().and_then(|s| s.parse::<Attribute>().ok()) else {
return Err(ParseStyleError::MissingAttribute);
};
style = style.disable(attr);
} else if let Ok(color) = token.parse::<Color>() {
style.foreground = Some(color);
} else if let Ok(attr) = token.parse::<Attribute>() {
style = style.enable(attr);
} else {
return Err(ParseStyleError::Token(token.to_owned()));
}
}
Ok(style)
}
}
#[cfg(feature = "serde")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
impl serde::Serialize for Style {
fn serialize<S: serde::ser::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
#[cfg(feature = "serde")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
impl<'de> serde::Deserialize<'de> for Style {
fn deserialize<D: serde::de::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct Visitor;
impl serde::de::Visitor<'_> for Visitor {
type Value = Style;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a style string")
}
fn visit_str<E>(self, input: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
input
.parse::<Style>()
.map_err(|_| E::invalid_value(serde::de::Unexpected::Str(input), &self))
}
}
deserializer.deserialize_str(Visitor)
}
}
#[derive(Clone, Debug, Eq, Error, PartialEq)]
pub enum ParseStyleError {
#[error("unexpected token in style string: {0:?}")]
Token(
String,
),
#[error(r#""on" not followed by valid color word"#)]
MissingBackground,
#[error(r#""not" not followed by valid attribute name"#)]
MissingAttribute,
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_new_is_default() {
assert_eq!(Style::new(), Style::default());
}
mod display {
use super::*;
use crate::Color256;
#[test]
fn none() {
assert_eq!(Style::new().to_string(), "none");
}
#[test]
fn fg_color() {
let style = Style::from(Color256::RED);
assert_eq!(style.to_string(), "red");
}
#[test]
fn bg_color() {
let style = Color256::RED.as_background();
assert_eq!(style.to_string(), "on red");
}
#[test]
fn fg_on_bg() {
let style = Color256::BLUE.on(Color256::RED);
assert_eq!(style.to_string(), "blue on red");
}
#[test]
fn attr() {
let style = Style::from(Attribute::Bold);
assert_eq!(style.to_string(), "bold");
}
#[test]
fn alt_attr() {
let style = Style::from(Attribute::Bold);
assert_eq!(format!("{style:#}"), "b");
}
#[test]
fn multiple_attrs() {
let style = Style::from(Attribute::Bold | Attribute::Reverse);
assert_eq!(style.to_string(), "bold reverse");
}
#[test]
fn alt_multiple_attrs() {
let style = Style::from(Attribute::Bold | Attribute::Reverse);
assert_eq!(format!("{style:#}"), "b r");
}
#[test]
fn not_attr() {
let style = Style::new().disable(Attribute::Bold);
assert_eq!(style.to_string(), "not bold");
}
#[test]
fn alt_not_attr() {
let style = Style::new().disable(Attribute::Bold);
assert_eq!(format!("{style:#}"), "not b");
}
#[test]
fn multiple_not_attrs() {
let style = Style::new()
.disable(Attribute::Bold)
.disable(Attribute::Reverse);
assert_eq!(style.to_string(), "not bold not reverse");
}
#[test]
fn alt_multiple_not_attrs() {
let style = Style::new()
.disable(Attribute::Bold)
.disable(Attribute::Reverse);
assert_eq!(format!("{style:#}"), "not b not r");
}
#[test]
fn attr_and_not_attr() {
let style = Style::from(Attribute::Bold).disable(Attribute::Blink);
assert_eq!(style.to_string(), "bold not blink");
}
#[test]
fn gamut() {
let style = Color256::YELLOW
.on(Color::Default)
.enable(Attribute::Italic)
.disable(Attribute::Bold);
assert_eq!(style.to_string(), "not bold italic yellow on default");
}
#[test]
fn all_attrs() {
let style = Style::from(AttributeSet::ALL);
assert_eq!(
style.to_string(),
"bold dim italic underline blink blink2 reverse conceal strike underline2 frame encircle overline"
);
}
#[test]
fn not_all_attrs() {
let style = Style::new().disabled_attributes(AttributeSet::ALL);
assert_eq!(
style.to_string(),
"not bold not dim not italic not underline not blink not blink2 not reverse not conceal not strike not underline2 not frame not encircle not overline"
);
}
}
mod parse {
use super::*;
use crate::Color256;
use rstest::rstest;
#[test]
fn none() {
assert_eq!("".parse::<Style>().unwrap(), Style::new());
assert_eq!("none".parse::<Style>().unwrap(), Style::new());
assert_eq!("NONE".parse::<Style>().unwrap(), Style::new());
assert_eq!(" none ".parse::<Style>().unwrap(), Style::new());
}
#[test]
fn fg() {
assert_eq!(
"green".parse::<Style>().unwrap(),
Style::from(Color256::GREEN)
);
}
#[test]
fn bg() {
assert_eq!(
"on green".parse::<Style>().unwrap(),
Color256::GREEN.as_background()
);
assert_eq!(
" on green ".parse::<Style>().unwrap(),
Color256::GREEN.as_background()
);
assert_eq!(
" ON GREEN ".parse::<Style>().unwrap(),
Color256::GREEN.as_background()
);
}
#[test]
fn fg_on_bg() {
assert_eq!(
"blue on white".parse::<Style>().unwrap(),
Color256::BLUE.on(Color256::WHITE)
);
assert_eq!(
"on white blue".parse::<Style>().unwrap(),
Color256::BLUE.on(Color256::WHITE)
);
}
#[test]
fn attr() {
assert_eq!(
"bold".parse::<Style>().unwrap(),
Style::from(Attribute::Bold)
);
}
#[test]
fn multiple_attr() {
assert_eq!(
"bold underline".parse::<Style>().unwrap(),
Style::from(Attribute::Bold | Attribute::Underline)
);
assert_eq!(
"underline bold".parse::<Style>().unwrap(),
Style::from(Attribute::Bold | Attribute::Underline)
);
}
#[test]
fn not_attr() {
assert_eq!(
"not bold".parse::<Style>().unwrap(),
Style::new().disable(Attribute::Bold)
);
assert_eq!(
" NOT BOLD ".parse::<Style>().unwrap(),
Style::new().disable(Attribute::Bold)
);
}
#[test]
fn multiple_not_attrs() {
assert_eq!(
"not bold not s".parse::<Style>().unwrap(),
Style::new().disabled_attributes(Attribute::Bold | Attribute::Strike)
);
assert_eq!(
"not s not bold".parse::<Style>().unwrap(),
Style::new().disabled_attributes(Attribute::Bold | Attribute::Strike)
);
}
#[test]
fn attr_and_not_attr() {
assert_eq!(
"dim not blink2".parse::<Style>().unwrap(),
Style::new()
.enable(Attribute::Dim)
.disable(Attribute::Blink2)
);
assert_eq!(
"not blink2 dim".parse::<Style>().unwrap(),
Style::new()
.enable(Attribute::Dim)
.disable(Attribute::Blink2)
);
}
#[test]
fn gamut() {
for s in [
"bold not underline red on blue",
"not underline red on blue bold",
"on blue red not underline bold",
] {
assert_eq!(
s.parse::<Style>().unwrap(),
Color256::RED.on(Color256::BLUE).bold().not_underline()
);
}
}
#[test]
fn multiple_fg() {
assert_eq!(
"red blue".parse::<Style>().unwrap(),
Style::from(Color256::BLUE)
);
}
#[test]
fn multiple_bg() {
assert_eq!(
"on red on blue".parse::<Style>().unwrap(),
Color256::BLUE.as_background()
);
}
#[test]
fn attr_on_and_off() {
assert_eq!(
"bold magenta not bold".parse::<Style>().unwrap(),
Style::from(Color256::MAGENTA).not_bold()
);
}
#[test]
fn attr_off_and_on() {
assert_eq!(
"not bold magenta bold".parse::<Style>().unwrap(),
Style::from(Color256::MAGENTA).bold()
);
}
#[rstest]
#[case("on bold")]
#[case("on foo")]
#[case("blue on")]
#[case("on")]
#[case("not blue")]
#[case("not foo")]
#[case("bold not")]
#[case("not not bold italic")]
#[case("not")]
#[case("none red")]
#[case("red none")]
#[case("foo")]
#[case("rgb(1, 2, 3)")]
#[case("bright blue")]
fn err(#[case] s: &str) {
assert!(s.parse::<Style>().is_err());
}
}
}