use crate::color::Color;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Style {
pub fg: Option<Color>,
pub bg: Option<Color>,
pub bold: bool,
pub italic: bool,
pub underline: bool,
pub strikethrough: bool,
pub dim: bool,
pub reverse: bool,
pub link: Option<String>,
}
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 bold(mut self, val: bool) -> Self {
self.bold = val;
self
}
#[must_use]
pub fn italic(mut self, val: bool) -> Self {
self.italic = val;
self
}
#[must_use]
pub fn underline(mut self, val: bool) -> Self {
self.underline = val;
self
}
#[must_use]
pub fn strikethrough(mut self, val: bool) -> Self {
self.strikethrough = val;
self
}
#[must_use]
pub fn dim(mut self, val: bool) -> Self {
self.dim = val;
self
}
#[must_use]
pub fn reverse(mut self, val: bool) -> Self {
self.reverse = val;
self
}
#[must_use]
pub fn link(mut self, url: impl Into<String>) -> Self {
self.link = Some(url.into());
self
}
#[must_use]
pub fn merge(&self, other: &Style) -> Style {
Style {
fg: other.fg.clone().or_else(|| self.fg.clone()),
bg: other.bg.clone().or_else(|| self.bg.clone()),
bold: if other.bold { true } else { self.bold },
italic: if other.italic { true } else { self.italic },
underline: if other.underline {
true
} else {
self.underline
},
strikethrough: if other.strikethrough {
true
} else {
self.strikethrough
},
dim: if other.dim { true } else { self.dim },
reverse: if other.reverse { true } else { self.reverse },
link: other.link.clone().or_else(|| self.link.clone()),
}
}
pub fn is_empty(&self) -> bool {
*self == Self::default()
}
}
impl From<&Style> for crossterm::style::ContentStyle {
fn from(style: &Style) -> Self {
use crossterm::style::{Attribute, ContentStyle};
let mut cs = ContentStyle::new();
if let Some(ref fg) = style.fg {
cs.foreground_color = Some(fg.into());
}
if let Some(ref bg) = style.bg {
cs.background_color = Some(bg.into());
}
if style.bold {
cs.attributes.set(Attribute::Bold);
}
if style.italic {
cs.attributes.set(Attribute::Italic);
}
if style.underline {
cs.attributes.set(Attribute::Underlined);
}
if style.strikethrough {
cs.attributes.set(Attribute::CrossedOut);
}
if style.dim {
cs.attributes.set(Attribute::Dim);
}
if style.reverse {
cs.attributes.set(Attribute::Reverse);
}
cs
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::color::NamedColor;
#[test]
fn builder_pattern() {
let s = Style::new()
.fg(Color::Named(NamedColor::Red))
.bold(true)
.italic(true);
assert_eq!(s.fg, Some(Color::Named(NamedColor::Red)));
assert!(s.bold);
assert!(s.italic);
assert!(!s.underline);
}
#[test]
fn default_is_empty() {
assert!(Style::new().is_empty());
}
#[test]
fn non_empty_style() {
assert!(!Style::new().bold(true).is_empty());
}
#[test]
fn merge_fg_override() {
let base = Style::new().fg(Color::Named(NamedColor::Red));
let over = Style::new().fg(Color::Named(NamedColor::Blue));
let merged = base.merge(&over);
assert_eq!(merged.fg, Some(Color::Named(NamedColor::Blue)));
}
#[test]
fn merge_preserves_base() {
let base = Style::new().fg(Color::Named(NamedColor::Red)).bold(true);
let over = Style::new().italic(true);
let merged = base.merge(&over);
assert_eq!(merged.fg, Some(Color::Named(NamedColor::Red)));
assert!(merged.bold);
assert!(merged.italic);
}
#[test]
fn crossterm_conversion() {
let s = Style::new().fg(Color::Rgb { r: 1, g: 2, b: 3 }).bold(true);
let cs: crossterm::style::ContentStyle = (&s).into();
assert_eq!(
cs.foreground_color,
Some(crossterm::style::Color::Rgb { r: 1, g: 2, b: 3 })
);
}
}