use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Color {
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
}
impl Color {
fn fg_code(self) -> u8 {
match self {
Self::Black => 30,
Self::Red => 31,
Self::Green => 32,
Self::Yellow => 33,
Self::Blue => 34,
Self::Magenta => 35,
Self::Cyan => 36,
Self::White => 37,
}
}
}
pub struct Style;
impl Style {
pub const RESET: &str = "\x1b[0m";
}
pub struct Styled<'a> {
text: &'a str,
codes: Vec<u8>,
}
impl<'a> Styled<'a> {
pub fn fg(mut self, color: Color) -> Self {
self.codes.push(color.fg_code());
self
}
pub fn bold(mut self) -> Self {
self.codes.push(1);
self
}
pub fn dim(mut self) -> Self {
self.codes.push(2);
self
}
}
impl fmt::Display for Styled<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.codes.is_empty() {
return f.write_str(self.text);
}
f.write_str("\x1b[")?;
for (i, code) in self.codes.iter().enumerate() {
if i > 0 {
f.write_str(";")?;
}
write!(f, "{code}")?;
}
f.write_str("m")?;
f.write_str(self.text)?;
f.write_str(Style::RESET)
}
}
pub fn style(text: &str) -> Styled<'_> {
Styled {
text,
codes: Vec::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn style_without_codes_returns_plain_text() {
assert_eq!(style("hello").to_string(), "hello");
}
#[test]
fn single_fg_color_wraps_with_sgr() {
let output = style("ok").fg(Color::Green).to_string();
assert_eq!(output, "\x1b[32mok\x1b[0m");
}
#[test]
fn bold_produces_code_1() {
let output = style("title").bold().to_string();
assert_eq!(output, "\x1b[1mtitle\x1b[0m");
}
#[test]
fn combined_styles_join_with_semicolon() {
let output = style("err").fg(Color::Red).bold().to_string();
assert_eq!(output, "\x1b[31;1merr\x1b[0m");
}
#[test]
fn dim_produces_code_2() {
let output = style("faint").dim().to_string();
assert_eq!(output, "\x1b[2mfaint\x1b[0m");
}
#[test]
fn all_colors_produce_distinct_codes() {
let colors = [
(Color::Black, 30),
(Color::Red, 31),
(Color::Green, 32),
(Color::Yellow, 33),
(Color::Blue, 34),
(Color::Magenta, 35),
(Color::Cyan, 36),
(Color::White, 37),
];
for (color, expected) in colors {
let output = style("x").fg(color).to_string();
assert_eq!(output, format!("\x1b[{expected}mx\x1b[0m"));
}
}
#[test]
fn reset_constant_is_sgr_0() {
assert_eq!(Style::RESET, "\x1b[0m");
}
}