1use std::fmt;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum Color {
27 Black,
28 Red,
29 Green,
30 Yellow,
31 Blue,
32 Magenta,
33 Cyan,
34 White,
35}
36
37impl Color {
38 fn fg_code(self) -> u8 {
40 match self {
41 Self::Black => 30,
42 Self::Red => 31,
43 Self::Green => 32,
44 Self::Yellow => 33,
45 Self::Blue => 34,
46 Self::Magenta => 35,
47 Self::Cyan => 36,
48 Self::White => 37,
49 }
50 }
51}
52
53pub struct Style;
55
56impl Style {
57 pub const RESET: &str = "\x1b[0m";
59}
60
61pub struct Styled<'a> {
78 text: &'a str,
79 codes: Vec<u8>,
80}
81
82impl<'a> Styled<'a> {
83 pub fn fg(mut self, color: Color) -> Self {
85 self.codes.push(color.fg_code());
86 self
87 }
88
89 pub fn bold(mut self) -> Self {
91 self.codes.push(1);
92 self
93 }
94
95 pub fn dim(mut self) -> Self {
97 self.codes.push(2);
98 self
99 }
100}
101
102impl fmt::Display for Styled<'_> {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 if self.codes.is_empty() {
105 return f.write_str(self.text);
106 }
107 f.write_str("\x1b[")?;
108 for (i, code) in self.codes.iter().enumerate() {
109 if i > 0 {
110 f.write_str(";")?;
111 }
112 write!(f, "{code}")?;
113 }
114 f.write_str("m")?;
115 f.write_str(self.text)?;
116 f.write_str(Style::RESET)
117 }
118}
119
120pub fn style(text: &str) -> Styled<'_> {
131 Styled {
132 text,
133 codes: Vec::new(),
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn style_without_codes_returns_plain_text() {
143 assert_eq!(style("hello").to_string(), "hello");
144 }
145
146 #[test]
147 fn single_fg_color_wraps_with_sgr() {
148 let output = style("ok").fg(Color::Green).to_string();
149 assert_eq!(output, "\x1b[32mok\x1b[0m");
150 }
151
152 #[test]
153 fn bold_produces_code_1() {
154 let output = style("title").bold().to_string();
155 assert_eq!(output, "\x1b[1mtitle\x1b[0m");
156 }
157
158 #[test]
159 fn combined_styles_join_with_semicolon() {
160 let output = style("err").fg(Color::Red).bold().to_string();
161 assert_eq!(output, "\x1b[31;1merr\x1b[0m");
162 }
163
164 #[test]
165 fn dim_produces_code_2() {
166 let output = style("faint").dim().to_string();
167 assert_eq!(output, "\x1b[2mfaint\x1b[0m");
168 }
169
170 #[test]
171 fn all_colors_produce_distinct_codes() {
172 let colors = [
173 (Color::Black, 30),
174 (Color::Red, 31),
175 (Color::Green, 32),
176 (Color::Yellow, 33),
177 (Color::Blue, 34),
178 (Color::Magenta, 35),
179 (Color::Cyan, 36),
180 (Color::White, 37),
181 ];
182 for (color, expected) in colors {
183 let output = style("x").fg(color).to_string();
184 assert_eq!(output, format!("\x1b[{expected}mx\x1b[0m"));
185 }
186 }
187
188 #[test]
189 fn reset_constant_is_sgr_0() {
190 assert_eq!(Style::RESET, "\x1b[0m");
191 }
192}