1#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
5pub enum Color {
6 Ansi(u8),
8 Ansi256(u8),
10 Rgb(u8, u8, u8),
12}
13
14impl Color {
15 pub const BLACK: Self = Self::Ansi(0);
16 pub const RED: Self = Self::Ansi(1);
17 pub const GREEN: Self = Self::Ansi(2);
18 pub const YELLOW: Self = Self::Ansi(3);
19 pub const BLUE: Self = Self::Ansi(4);
20 pub const MAGENTA: Self = Self::Ansi(5);
21 pub const CYAN: Self = Self::Ansi(6);
22 pub const WHITE: Self = Self::Ansi(7);
23 pub const BRIGHT_BLACK: Self = Self::Ansi(8);
24 pub const BRIGHT_RED: Self = Self::Ansi(9);
25 pub const BRIGHT_GREEN: Self = Self::Ansi(10);
26 pub const BRIGHT_YELLOW: Self = Self::Ansi(11);
27 pub const BRIGHT_BLUE: Self = Self::Ansi(12);
28 pub const BRIGHT_MAGENTA: Self = Self::Ansi(13);
29 pub const BRIGHT_CYAN: Self = Self::Ansi(14);
30 pub const BRIGHT_WHITE: Self = Self::Ansi(15);
31
32 pub fn parse(value: &str) -> Option<Self> {
34 if let Some(hex) = value.strip_prefix('#') {
35 return match hex.len() {
36 3 => {
37 let mut digits = hex
38 .chars()
39 .map(|character| character.to_digit(16).map(|digit| (digit * 17) as u8));
40 Some(Self::Rgb(digits.next()??, digits.next()??, digits.next()??))
41 }
42 6 => Some(Self::Rgb(
43 u8::from_str_radix(&hex[0..2], 16).ok()?,
44 u8::from_str_radix(&hex[2..4], 16).ok()?,
45 u8::from_str_radix(&hex[4..6], 16).ok()?,
46 )),
47 _ => None,
48 };
49 }
50
51 value.parse::<u8>().ok().map(Self::from)
52 }
53}
54
55impl From<u8> for Color {
56 fn from(index: u8) -> Self {
57 if index < 16 {
58 Self::Ansi(index)
59 } else {
60 Self::Ansi256(index)
61 }
62 }
63}
64
65impl From<(u8, u8, u8)> for Color {
66 fn from((red, green, blue): (u8, u8, u8)) -> Self {
67 Self::Rgb(red, green, blue)
68 }
69}
70
71#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
73pub enum TextAttribute {
74 Bold,
75 Dim,
76 Italic,
77 SlowBlink,
78 RapidBlink,
79 Reversed,
80 Hidden,
81 CrossedOut,
82 Fraktur,
83 Framed,
84 Encircled,
85 Overlined,
86}
87
88impl TextAttribute {
89 const COUNT: usize = 12;
90
91 const fn bit(self) -> u16 {
92 1 << self as u16
93 }
94
95 const fn from_index(index: u32) -> Self {
96 match index {
97 0 => Self::Bold,
98 1 => Self::Dim,
99 2 => Self::Italic,
100 3 => Self::SlowBlink,
101 4 => Self::RapidBlink,
102 5 => Self::Reversed,
103 6 => Self::Hidden,
104 7 => Self::CrossedOut,
105 8 => Self::Fraktur,
106 9 => Self::Framed,
107 10 => Self::Encircled,
108 11 => Self::Overlined,
109 _ => unreachable!(),
110 }
111 }
112}
113
114#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
116pub struct TextAttributes(u16);
117
118impl TextAttributes {
119 const ALL_BITS: u16 = (1 << TextAttribute::COUNT) - 1;
120
121 pub const fn empty() -> Self {
122 Self(0)
123 }
124
125 pub const fn all() -> Self {
126 Self(Self::ALL_BITS)
127 }
128
129 pub const fn from_attribute(attribute: TextAttribute) -> Self {
130 Self(attribute.bit())
131 }
132
133 pub const fn union(self, other: Self) -> Self {
134 Self(self.0 | other.0)
135 }
136
137 pub const fn difference(self, other: Self) -> Self {
138 Self(self.0 & (Self::ALL_BITS ^ other.0))
139 }
140
141 pub const fn intersection(self, other: Self) -> Self {
142 Self(self.0 & other.0)
143 }
144
145 pub const fn contains(self, attribute: TextAttribute) -> bool {
146 self.0 & attribute.bit() != 0
147 }
148
149 pub const fn contains_all(self, other: Self) -> bool {
150 self.0 & other.0 == other.0
151 }
152
153 pub const fn is_empty(self) -> bool {
154 self.0 == 0
155 }
156
157 pub const fn iter(self) -> TextAttributeIter {
158 TextAttributeIter { remaining: self.0 }
159 }
160}
161
162impl From<TextAttribute> for TextAttributes {
163 fn from(attribute: TextAttribute) -> Self {
164 Self::from_attribute(attribute)
165 }
166}
167
168impl std::ops::BitOr for TextAttribute {
169 type Output = TextAttributes;
170
171 fn bitor(self, rhs: Self) -> Self::Output {
172 TextAttributes::from_attribute(self).union(TextAttributes::from_attribute(rhs))
173 }
174}
175
176impl std::ops::BitOr<TextAttribute> for TextAttributes {
177 type Output = Self;
178
179 fn bitor(self, rhs: TextAttribute) -> Self::Output {
180 self.union(rhs.into())
181 }
182}
183
184impl std::ops::BitOr for TextAttributes {
185 type Output = Self;
186
187 fn bitor(self, rhs: Self) -> Self::Output {
188 self.union(rhs)
189 }
190}
191
192pub struct TextAttributeIter {
194 remaining: u16,
195}
196
197impl Iterator for TextAttributeIter {
198 type Item = TextAttribute;
199
200 fn next(&mut self) -> Option<Self::Item> {
201 if self.remaining == 0 {
202 return None;
203 }
204 let index = self.remaining.trailing_zeros();
205 self.remaining &= self.remaining - 1;
206 Some(TextAttribute::from_index(index))
207 }
208
209 fn size_hint(&self) -> (usize, Option<usize>) {
210 let remaining = self.remaining.count_ones() as usize;
211 (remaining, Some(remaining))
212 }
213}
214
215impl ExactSizeIterator for TextAttributeIter {}
216impl std::iter::FusedIterator for TextAttributeIter {}
217
218impl IntoIterator for TextAttributes {
219 type Item = TextAttribute;
220 type IntoIter = TextAttributeIter;
221
222 fn into_iter(self) -> Self::IntoIter {
223 self.iter()
224 }
225}
226
227#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
229pub enum UnderlineStyle {
230 #[default]
231 Single,
232 Double,
233 Curly,
234 Dotted,
235 Dashed,
236}
237
238#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
240pub struct Underline {
241 style: UnderlineStyle,
242 color: Option<Color>,
243}
244
245impl Underline {
246 pub const fn new(style: UnderlineStyle) -> Self {
247 Self { style, color: None }
248 }
249
250 pub const fn style(mut self, style: UnderlineStyle) -> Self {
251 self.style = style;
252 self
253 }
254
255 pub const fn reset_style(mut self) -> Self {
256 self.style = UnderlineStyle::Single;
257 self
258 }
259
260 pub fn color(mut self, color: impl Into<Color>) -> Self {
261 self.color = Some(color.into());
262 self
263 }
264
265 pub const fn reset_color(mut self) -> Self {
266 self.color = None;
267 self
268 }
269
270 pub const fn get_style(self) -> UnderlineStyle {
271 self.style
272 }
273
274 pub const fn get_color(self) -> Option<Color> {
275 self.color
276 }
277}
278
279impl From<UnderlineStyle> for Underline {
280 fn from(style: UnderlineStyle) -> Self {
281 Self::new(style)
282 }
283}
284
285#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
287pub struct TerminalStyle {
288 pub foreground: Option<Color>,
289 pub background: Option<Color>,
290 pub attributes: TextAttributes,
291 pub underline: Option<Underline>,
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 #[test]
299 fn parses_colors_without_a_rendering_dependency() {
300 assert_eq!(Color::parse("#fff"), Some(Color::Rgb(255, 255, 255)));
301 assert_eq!(Color::parse("#ff88cc"), Some(Color::Rgb(255, 136, 204)));
302 assert_eq!(Color::parse("1"), Some(Color::Ansi(1)));
303 assert_eq!(Color::parse("212"), Some(Color::Ansi256(212)));
304 assert_eq!(Color::parse("256"), None);
305 }
306
307 #[test]
308 fn attribute_sets_support_style_composition() {
309 let emphasis = TextAttribute::Bold | TextAttribute::Italic;
310
311 assert_eq!(
312 emphasis.difference(TextAttribute::Italic.into()),
313 TextAttribute::Bold.into()
314 );
315 assert_eq!(
316 emphasis.intersection(TextAttribute::Italic | TextAttribute::Dim),
317 TextAttribute::Italic.into()
318 );
319 assert_eq!(
320 emphasis.iter().collect::<Vec<_>>(),
321 [TextAttribute::Bold, TextAttribute::Italic]
322 );
323 }
324}