use std::borrow::Cow;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Color {
Rgb(u8, u8, u8),
Indexed(u8),
Named(NamedColor),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NamedColor {
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
BrightBlack,
BrightRed,
BrightGreen,
BrightYellow,
BrightBlue,
BrightMagenta,
BrightCyan,
BrightWhite,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Style {
pub fg: Option<Color>,
pub bg: Option<Color>,
pub bold: bool,
pub dim: bool,
pub italic: bool,
pub underline: bool,
pub strike: bool,
pub reverse: bool,
}
impl Style {
pub const PLAIN: Style = Style {
fg: None,
bg: None,
bold: false,
dim: false,
italic: false,
underline: false,
strike: false,
reverse: false,
};
pub const fn fg(color: Color) -> Self {
Style {
fg: Some(color),
..Style::PLAIN
}
}
pub const fn bold() -> Self {
Style {
bold: true,
..Style::PLAIN
}
}
pub const fn with_bold(mut self) -> Self {
self.bold = true;
self
}
pub const fn with_dim(mut self) -> Self {
self.dim = true;
self
}
pub const fn with_italic(mut self) -> Self {
self.italic = true;
self
}
pub const fn with_underline(mut self) -> Self {
self.underline = true;
self
}
pub const fn with_strike(mut self) -> Self {
self.strike = true;
self
}
pub const fn with_fg(mut self, color: Color) -> Self {
self.fg = Some(color);
self
}
pub fn over(self, other: Style) -> Style {
Style {
fg: other.fg.or(self.fg),
bg: other.bg.or(self.bg),
bold: self.bold || other.bold,
dim: self.dim || other.dim,
italic: self.italic || other.italic,
underline: self.underline || other.underline,
strike: self.strike || other.strike,
reverse: self.reverse || other.reverse,
}
}
pub fn is_plain(&self) -> bool {
*self == Style::PLAIN
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Segment<'a> {
pub text: Cow<'a, str>,
pub style: Style,
pub link: Option<Cow<'a, str>>,
}
impl<'a> Segment<'a> {
pub fn new(text: impl Into<Cow<'a, str>>, style: Style) -> Self {
Segment {
text: text.into(),
style,
link: None,
}
}
pub fn plain(text: impl Into<Cow<'a, str>>) -> Self {
Segment::new(text, Style::PLAIN)
}
pub fn with_link(mut self, href: impl Into<Cow<'a, str>>) -> Self {
self.link = Some(href.into());
self
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Line<'a> {
pub segments: Vec<Segment<'a>>,
pub width: usize,
}
impl<'a> Line<'a> {
pub fn empty() -> Self {
Line::default()
}
pub fn from_segments(segments: Vec<Segment<'a>>, width: usize) -> Self {
Line { segments, width }
}
pub fn to_plain_string(&self) -> String {
self.segments.iter().map(|s| s.text.as_ref()).collect()
}
pub fn is_blank(&self) -> bool {
self.segments
.iter()
.all(|s| s.text.chars().all(char::is_whitespace))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn over_accumulates_attributes_and_prefers_the_new_color() {
let base = Style::bold().with_fg(Color::Named(NamedColor::Red));
let top = Style::PLAIN
.with_italic()
.with_fg(Color::Named(NamedColor::Blue));
let merged = base.over(top);
assert!(merged.bold, "the base's bold must survive");
assert!(merged.italic);
assert_eq!(merged.fg, Some(Color::Named(NamedColor::Blue)));
}
#[test]
fn over_keeps_the_base_color_when_the_new_one_is_unset() {
let base = Style::fg(Color::Named(NamedColor::Red));
let merged = base.over(Style::bold());
assert_eq!(merged.fg, Some(Color::Named(NamedColor::Red)));
assert!(merged.bold);
}
#[test]
fn to_plain_string_concatenates() {
let line = Line::from_segments(
vec![Segment::plain("ab"), Segment::new("cd", Style::bold())],
4,
);
assert_eq!(line.to_plain_string(), "abcd");
}
}