use serde::{Deserialize, Serialize};
use std::fmt::Write as _;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Color {
Named(Named),
Indexed(Indexed),
Rgb(Rgb),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Named {
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
BrightBlack,
BrightRed,
BrightGreen,
BrightYellow,
BrightBlue,
BrightMagenta,
BrightCyan,
BrightWhite,
}
impl Named {
fn fg_code(self) -> u8 {
use Named::*;
match self {
Black => 30,
Red => 31,
Green => 32,
Yellow => 33,
Blue => 34,
Magenta => 35,
Cyan => 36,
White => 37,
BrightBlack => 90,
BrightRed => 91,
BrightGreen => 92,
BrightYellow => 93,
BrightBlue => 94,
BrightMagenta => 95,
BrightCyan => 96,
BrightWhite => 97,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Indexed {
pub index: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(from = "RgbRepr", into = "RgbRepr")]
pub struct Rgb {
pub r: u8,
pub g: u8,
pub b: u8,
}
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum RgbRepr {
Hex(String),
Triple { rgb: [u8; 3] },
}
impl From<RgbRepr> for Rgb {
fn from(r: RgbRepr) -> Self {
match r {
RgbRepr::Hex(s) => Rgb::from_hex(&s).unwrap_or(Rgb { r: 0, g: 0, b: 0 }),
RgbRepr::Triple { rgb } => Rgb {
r: rgb[0],
g: rgb[1],
b: rgb[2],
},
}
}
}
impl From<Rgb> for RgbRepr {
fn from(c: Rgb) -> Self {
RgbRepr::Hex(format!("#{:02x}{:02x}{:02x}", c.r, c.g, c.b))
}
}
impl Rgb {
pub fn from_hex(s: &str) -> Option<Rgb> {
let s = s.trim().trim_start_matches('#');
let parse = |h: &str| u8::from_str_radix(h, 16).ok();
match s.len() {
3 => {
let dup = |c: char| {
let d = c.to_digit(16)? as u8;
Some(d * 16 + d)
};
let mut it = s.chars();
Some(Rgb {
r: dup(it.next()?)?,
g: dup(it.next()?)?,
b: dup(it.next()?)?,
})
}
6 => Some(Rgb {
r: parse(&s[0..2])?,
g: parse(&s[2..4])?,
b: parse(&s[4..6])?,
}),
_ => None,
}
}
}
impl Color {
pub fn hex(s: &str) -> Color {
Color::Rgb(Rgb::from_hex(s).expect("valid hex color literal"))
}
pub fn idx(i: u8) -> Color {
Color::Indexed(Indexed { index: i })
}
fn write_fg(self, out: &mut String) {
match self {
Color::Named(n) => {
let _ = write!(out, "{}", n.fg_code());
}
Color::Indexed(i) => {
let _ = write!(out, "38;5;{}", i.index);
}
Color::Rgb(c) => {
let _ = write!(out, "38;2;{};{};{}", c.r, c.g, c.b);
}
}
}
fn write_bg(self, out: &mut String) {
match self {
Color::Named(n) => {
let _ = write!(out, "{}", n.fg_code() + 10);
}
Color::Indexed(i) => {
let _ = write!(out, "48;5;{}", i.index);
}
Color::Rgb(c) => {
let _ = write!(out, "48;2;{};{};{}", c.r, c.g, c.b);
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct Style {
pub fg: Option<Color>,
pub bg: Option<Color>,
pub bold: bool,
pub dim: bool,
pub italic: bool,
pub underline: bool,
pub blink: bool,
pub reverse: bool,
}
impl Style {
pub const RESET: &'static str = "\x1b[0m";
pub fn plain() -> Style {
Style::default()
}
pub fn fg(color: Color) -> Style {
Style {
fg: Some(color),
..Style::default()
}
}
pub fn bold_fg(color: Color) -> Style {
Style {
fg: Some(color),
bold: true,
..Style::default()
}
}
pub fn is_plain(&self) -> bool {
*self == Style::default()
}
pub fn prefix(&self) -> String {
if self.is_plain() {
return String::new();
}
let mut params: Vec<String> = Vec::new();
if self.bold {
params.push("1".into());
}
if self.dim {
params.push("2".into());
}
if self.italic {
params.push("3".into());
}
if self.underline {
params.push("4".into());
}
if self.blink {
params.push("5".into());
}
if self.reverse {
params.push("7".into());
}
if let Some(fg) = self.fg {
let mut s = String::new();
fg.write_fg(&mut s);
params.push(s);
}
if let Some(bg) = self.bg {
let mut s = String::new();
bg.write_bg(&mut s);
params.push(s);
}
format!("\x1b[{}m", params.join(";"))
}
pub fn paint(&self, text: &str) -> String {
if self.is_plain() {
return text.to_string();
}
format!("{}{}{}", self.prefix(), text, Style::RESET)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hex_parsing() {
assert_eq!(
Rgb::from_hex("#ff00aa"),
Some(Rgb {
r: 255,
g: 0,
b: 170
})
);
assert_eq!(Rgb::from_hex("0a0"), Some(Rgb { r: 0, g: 170, b: 0 }));
assert_eq!(Rgb::from_hex("nope"), None);
}
#[test]
fn truecolor_prefix() {
let s = Style::bold_fg(Color::hex("#ff00aa"));
assert_eq!(s.prefix(), "\x1b[1;38;2;255;0;170m");
}
#[test]
fn named_bg_offset() {
let s = Style {
bg: Some(Color::Named(Named::Blue)),
..Style::default()
};
assert_eq!(s.prefix(), "\x1b[44m");
}
#[test]
fn hex_parsing_edge_cases() {
assert_eq!(
Rgb::from_hex("#FF00AA"),
Some(Rgb {
r: 255,
g: 0,
b: 170
})
);
assert_eq!(Rgb::from_hex(" #0a0 "), Some(Rgb { r: 0, g: 170, b: 0 }));
assert_eq!(Rgb::from_hex(""), None);
assert_eq!(Rgb::from_hex("#12345"), None);
assert_eq!(Rgb::from_hex("#1234567"), None);
assert_eq!(Rgb::from_hex("#gg0000"), None);
}
#[test]
fn indexed_fg_and_bg_prefix() {
assert_eq!(Style::fg(Color::idx(196)).prefix(), "\x1b[38;5;196m");
let bg = Style {
bg: Some(Color::idx(21)),
..Style::default()
};
assert_eq!(bg.prefix(), "\x1b[48;5;21m");
}
#[test]
fn truecolor_bg_prefix() {
let s = Style {
bg: Some(Color::hex("#102030")),
..Style::default()
};
assert_eq!(s.prefix(), "\x1b[48;2;16;32;48m");
}
#[test]
fn all_attributes_emit_in_canonical_order() {
let s = Style {
bold: true,
dim: true,
italic: true,
underline: true,
blink: true,
reverse: true,
..Style::default()
};
assert_eq!(s.prefix(), "\x1b[1;2;3;4;5;7m");
}
#[test]
fn attributes_precede_colors() {
let s = Style::bold_fg(Color::idx(9));
assert_eq!(s.prefix(), "\x1b[1;38;5;9m");
}
#[test]
fn plain_style_emits_nothing_and_paint_is_identity() {
let p = Style::plain();
assert!(p.is_plain());
assert_eq!(p.prefix(), "");
assert_eq!(p.paint("hello"), "hello");
}
#[test]
fn paint_wraps_with_prefix_and_reset() {
let s = Style::fg(Color::Named(Named::Red));
assert_eq!(s.paint("x"), "\x1b[31mx\x1b[0m");
}
#[test]
fn idx_and_hex_constructors() {
assert_eq!(Color::idx(213), Color::Indexed(Indexed { index: 213 }));
assert_eq!(
Color::hex("#ff00aa"),
Color::Rgb(Rgb {
r: 255,
g: 0,
b: 170
})
);
}
#[test]
fn named_color_codes_normal_and_bright() {
assert_eq!(Style::fg(Color::Named(Named::Black)).prefix(), "\x1b[30m");
assert_eq!(Style::fg(Color::Named(Named::White)).prefix(), "\x1b[37m");
assert_eq!(Style::fg(Color::Named(Named::Green)).prefix(), "\x1b[32m");
assert_eq!(
Style::fg(Color::Named(Named::BrightGreen)).prefix(),
"\x1b[92m"
);
let bg = Style {
bg: Some(Color::Named(Named::BrightRed)),
..Style::default()
};
assert_eq!(bg.prefix(), "\x1b[101m");
}
#[test]
fn single_attribute_styles() {
assert_eq!(
Style {
dim: true,
..Style::default()
}
.prefix(),
"\x1b[2m"
);
assert_eq!(
Style {
reverse: true,
..Style::default()
}
.prefix(),
"\x1b[7m"
);
assert_eq!(
Style {
underline: true,
..Style::default()
}
.prefix(),
"\x1b[4m"
);
}
#[test]
fn rgb_serializes_back_to_hex_string() {
let c = Rgb {
r: 16,
g: 32,
b: 48,
};
let repr: RgbRepr = c.into();
match repr {
RgbRepr::Hex(s) => assert_eq!(s, "#102030"),
RgbRepr::Triple { .. } => panic!("Rgb should serialize as hex"),
}
}
}