use std::fmt::{self, Display, Write};
use crate::color::Color;
use crate::terminal::{self, ColorLevel};
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub(crate) struct StyleAttrs {
pub(crate) fg: Option<Color>,
pub(crate) bold: bool,
pub(crate) underline: bool,
}
impl StyleAttrs {
pub(crate) const EMPTY: StyleAttrs = StyleAttrs {
fg: None,
bold: false,
underline: false,
};
#[inline]
pub(crate) fn is_empty(self) -> bool {
self.fg.is_none() && !self.bold && !self.underline
}
}
pub(crate) fn write_styled<W: Write>(
w: &mut W,
attrs: StyleAttrs,
text: &str,
level: ColorLevel,
) -> fmt::Result {
if level.is_none() || attrs.is_empty() {
return w.write_str(text);
}
w.write_str("\x1b[")?;
let mut first = true;
if attrs.bold {
w.write_str("1")?;
first = false;
}
if attrs.underline {
if !first {
w.write_char(';')?;
}
w.write_str("4")?;
first = false;
}
if let Some(color) = attrs.fg {
color.write_fg(w, level, &mut first)?;
}
w.write_str("m")?;
w.write_str(text)?;
w.write_str("\x1b[0m")
}
#[derive(Clone, Debug)]
pub struct Style {
text: String,
attrs: StyleAttrs,
}
#[must_use]
pub fn style<S: Into<String>>(text: S) -> Style {
Style {
text: text.into(),
attrs: StyleAttrs::EMPTY,
}
}
macro_rules! named_color_method {
($(#[$meta:meta])* $name:ident => $variant:ident) => {
$(#[$meta])*
#[must_use]
pub fn $name(mut self) -> Style {
self.attrs.fg = Some(Color::$variant);
self
}
};
}
impl Style {
named_color_method!( black => Black);
named_color_method!( red => Red);
named_color_method!( green => Green);
named_color_method!( yellow => Yellow);
named_color_method!( blue => Blue);
named_color_method!( magenta => Magenta);
named_color_method!( cyan => Cyan);
named_color_method!( white => White);
#[must_use]
pub fn hex(mut self, hex: &str) -> Style {
if let Some(color) = Color::from_hex(hex) {
self.attrs.fg = Some(color);
}
self
}
#[must_use]
pub fn rgb(mut self, r: u8, g: u8, b: u8) -> Style {
self.attrs.fg = Some(Color::Rgb(r, g, b));
self
}
#[must_use]
pub fn bold(mut self) -> Style {
self.attrs.bold = true;
self
}
#[must_use]
pub fn underline(mut self) -> Style {
self.attrs.underline = true;
self
}
#[must_use]
pub fn render(&self) -> String {
let mut buf = String::with_capacity(self.text.len() + STYLE_OVERHEAD);
let _ = write_styled(&mut buf, self.attrs, &self.text, terminal::color_level());
buf
}
pub(crate) fn attrs(&self) -> StyleAttrs {
self.attrs
}
}
const STYLE_OVERHEAD: usize = 24;
impl Display for Style {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_styled(f, self.attrs, &self.text, terminal::color_level())
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
fn render_at(attrs: StyleAttrs, text: &str, level: ColorLevel) -> String {
let mut s = String::new();
write_styled(&mut s, attrs, text, level).unwrap();
s
}
#[test]
fn test_empty_style_is_plain_even_with_color() {
let attrs = StyleAttrs::EMPTY;
assert_eq!(render_at(attrs, "hello", ColorLevel::TrueColor), "hello");
}
#[test]
fn test_none_level_strips_all_styling() {
let attrs = StyleAttrs {
fg: Some(Color::Red),
bold: true,
underline: true,
};
assert_eq!(render_at(attrs, "x", ColorLevel::None), "x");
}
#[test]
fn test_canonical_parameter_order_is_bold_underline_color() {
let attrs = StyleAttrs {
fg: Some(Color::Red),
bold: true,
underline: true,
};
assert_eq!(
render_at(attrs, "ERR", ColorLevel::Ansi16),
"\x1b[1;4;31mERR\x1b[0m"
);
}
#[test]
fn test_builder_order_does_not_change_bytes() {
let a = style("ERR").red().bold().underline();
let b = style("ERR").underline().bold().red();
assert_eq!(
render_at(a.attrs(), "ERR", ColorLevel::Ansi16),
render_at(b.attrs(), "ERR", ColorLevel::Ansi16)
);
}
#[test]
fn test_single_attribute_has_no_stray_separator() {
let bold = StyleAttrs {
fg: None,
bold: true,
underline: false,
};
assert_eq!(render_at(bold, "x", ColorLevel::Ansi16), "\x1b[1mx\x1b[0m");
let red = StyleAttrs {
fg: Some(Color::Red),
bold: false,
underline: false,
};
assert_eq!(render_at(red, "x", ColorLevel::Ansi16), "\x1b[31mx\x1b[0m");
}
#[test]
fn test_invalid_hex_leaves_color_unset() {
assert_eq!(style("x").hex("zzzzzz").attrs().fg, None);
assert_eq!(
style("x").hex("#abcdef").attrs().fg,
Some(Color::Rgb(171, 205, 239))
);
}
}