1use std::{error::Error as StdError, fmt, io, num::ParseIntError, str::FromStr};
4
5#[cfg_attr(not(feature = "svg"), allow(unreachable_pub))] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub struct RgbColor(pub u8, pub u8, pub u8);
11
12impl fmt::LowerHex for RgbColor {
13 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
14 write!(formatter, "#{:02x}{:02x}{:02x}", self.0, self.1, self.2)
15 }
16}
17
18#[derive(Debug)]
20#[non_exhaustive]
21#[cfg_attr(not(feature = "svg"), allow(unreachable_pub))] pub enum RgbColorParseError {
23 NotAscii,
25 NoHashPrefix,
27 IncorrectLen(usize),
31 IncorrectDigit(ParseIntError),
33}
34
35impl fmt::Display for RgbColorParseError {
36 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
37 match self {
38 Self::NotAscii => formatter.write_str("color string contains non-ASCII chars"),
39 Self::NoHashPrefix => formatter.write_str("missing '#' prefix"),
40 Self::IncorrectLen(len) => write!(
41 formatter,
42 "unexpected byte length {len} of color string, expected 4 or 7"
43 ),
44 Self::IncorrectDigit(err) => write!(formatter, "error parsing hex digit: {err}"),
45 }
46 }
47}
48
49impl StdError for RgbColorParseError {
50 fn source(&self) -> Option<&(dyn StdError + 'static)> {
51 match self {
52 Self::IncorrectDigit(err) => Some(err),
53 _ => None,
54 }
55 }
56}
57
58impl FromStr for RgbColor {
59 type Err = RgbColorParseError;
60
61 fn from_str(s: &str) -> Result<Self, Self::Err> {
62 if s.is_empty() || s.as_bytes()[0] != b'#' {
63 Err(RgbColorParseError::NoHashPrefix)
64 } else if s.len() == 4 {
65 if !s.is_ascii() {
66 return Err(RgbColorParseError::NotAscii);
67 }
68
69 let r = u8::from_str_radix(&s[1..2], 16).map_err(RgbColorParseError::IncorrectDigit)?;
70 let g = u8::from_str_radix(&s[2..3], 16).map_err(RgbColorParseError::IncorrectDigit)?;
71 let b = u8::from_str_radix(&s[3..], 16).map_err(RgbColorParseError::IncorrectDigit)?;
72 Ok(Self(r * 17, g * 17, b * 17))
73 } else if s.len() == 7 {
74 if !s.is_ascii() {
75 return Err(RgbColorParseError::NotAscii);
76 }
77
78 let r = u8::from_str_radix(&s[1..3], 16).map_err(RgbColorParseError::IncorrectDigit)?;
79 let g = u8::from_str_radix(&s[3..5], 16).map_err(RgbColorParseError::IncorrectDigit)?;
80 let b = u8::from_str_radix(&s[5..], 16).map_err(RgbColorParseError::IncorrectDigit)?;
81 Ok(Self(r, g, b))
82 } else {
83 Err(RgbColorParseError::IncorrectLen(s.len()))
84 }
85 }
86}
87
88#[derive(Debug, Clone, Copy, PartialEq)]
89#[cfg_attr(feature = "svg", derive(serde::Serialize))]
90#[cfg_attr(feature = "svg", serde(untagged))]
91pub(crate) enum Color {
92 Index(u8),
93 Rgb(RgbColor),
94}
95
96impl Color {
97 pub(crate) const BLACK: Self = Self::Index(0);
98 pub(crate) const RED: Self = Self::Index(1);
99 pub(crate) const GREEN: Self = Self::Index(2);
100 pub(crate) const YELLOW: Self = Self::Index(3);
101 pub(crate) const BLUE: Self = Self::Index(4);
102 pub(crate) const MAGENTA: Self = Self::Index(5);
103 pub(crate) const CYAN: Self = Self::Index(6);
104 pub(crate) const WHITE: Self = Self::Index(7);
105
106 pub(crate) const INTENSE_BLACK: Self = Self::Index(8);
107 pub(crate) const INTENSE_RED: Self = Self::Index(9);
108 pub(crate) const INTENSE_GREEN: Self = Self::Index(10);
109 pub(crate) const INTENSE_YELLOW: Self = Self::Index(11);
110 pub(crate) const INTENSE_BLUE: Self = Self::Index(12);
111 pub(crate) const INTENSE_MAGENTA: Self = Self::Index(13);
112 pub(crate) const INTENSE_CYAN: Self = Self::Index(14);
113 pub(crate) const INTENSE_WHITE: Self = Self::Index(15);
114}
115
116#[cfg(any(feature = "svg", feature = "test"))]
117impl Color {
118 fn index(value: u8) -> Self {
119 debug_assert!(value < 16);
120 Self::Index(value)
121 }
122
123 fn normalize(&mut self) {
124 if let Self::Index(index) = *self {
125 if index >= 16 {
126 *self = Color::indexed_color(index);
127 }
128 }
129 }
130
131 pub(crate) fn indexed_color(index: u8) -> Self {
132 match index {
133 0..=15 => Self::index(index),
134
135 16..=231 => {
136 let index = index - 16;
137 let r = Self::color_cube_color(index / 36);
138 let g = Self::color_cube_color((index / 6) % 6);
139 let b = Self::color_cube_color(index % 6);
140 Self::Rgb(RgbColor(r, g, b))
141 }
142
143 _ => {
144 let gray = 10 * (index - 232) + 8;
145 Self::Rgb(RgbColor(gray, gray, gray))
146 }
147 }
148 }
149
150 fn color_cube_color(index: u8) -> u8 {
151 match index {
152 0 => 0,
153 1 => 0x5f,
154 2 => 0x87,
155 3 => 0xaf,
156 4 => 0xd7,
157 5 => 0xff,
158 _ => unreachable!(),
159 }
160 }
161}
162
163#[derive(Debug, Default, Clone, Copy, PartialEq)]
165#[cfg_attr(feature = "svg", derive(serde::Serialize))]
166#[allow(clippy::struct_excessive_bools)] pub(crate) struct Style {
168 #[cfg_attr(feature = "svg", serde(skip_serializing_if = "Style::is_false"))]
169 pub(crate) bold: bool,
170 #[cfg_attr(feature = "svg", serde(skip_serializing_if = "Style::is_false"))]
171 pub(crate) italic: bool,
172 #[cfg_attr(feature = "svg", serde(skip_serializing_if = "Style::is_false"))]
173 pub(crate) underline: bool,
174 #[cfg_attr(feature = "svg", serde(skip_serializing_if = "Style::is_false"))]
175 pub(crate) dimmed: bool,
176 #[cfg_attr(feature = "svg", serde(skip_serializing_if = "Style::is_false"))]
178 pub(crate) strikethrough: bool,
179 #[cfg_attr(feature = "svg", serde(skip_serializing_if = "Style::is_false"))]
181 pub(crate) inverted: bool,
182 #[cfg_attr(feature = "svg", serde(skip_serializing_if = "Style::is_false"))]
184 pub(crate) blink: bool,
185 #[cfg_attr(feature = "svg", serde(skip_serializing_if = "Style::is_false"))]
187 pub(crate) concealed: bool,
188 #[cfg_attr(feature = "svg", serde(skip_serializing_if = "Option::is_none"))]
189 pub(crate) fg: Option<Color>,
190 #[cfg_attr(feature = "svg", serde(skip_serializing_if = "Option::is_none"))]
191 pub(crate) bg: Option<Color>,
192}
193
194impl fmt::Display for Style {
196 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
197 let is_terminal = formatter.alternate();
198 if self.is_none() {
199 Ok(()) } else if is_terminal {
201 write!(formatter, "\u{1b}[0m")
202 } else {
203 if self.bold {
204 write!(formatter, "\u{1b}[1m")?;
205 }
206 if self.dimmed {
207 write!(formatter, "\u{1b}[2m")?;
208 }
209 if self.italic {
210 write!(formatter, "\u{1b}[3m")?;
211 }
212 if self.underline {
213 write!(formatter, "\u{1b}[4m")?;
214 }
215 if self.strikethrough {
216 write!(formatter, "\u{1b}[9m")?;
217 }
218 if self.inverted {
219 write!(formatter, "\u{1b}[7m")?;
220 }
221 if self.blink {
222 write!(formatter, "\u{1b}[5m")?;
223 }
224 if self.concealed {
225 write!(formatter, "\u{1b}[8m")?;
226 }
227
228 if let Some(fg) = &self.fg {
229 fg.write_params(formatter, false)?;
230 }
231 if let Some(bg) = &self.bg {
232 bg.write_params(formatter, true)?;
233 }
234 Ok(())
235 }
236 }
237}
238
239impl Style {
240 #[cfg(feature = "test")]
241 pub(crate) const NONE: Self = Self {
242 bold: false,
243 italic: false,
244 underline: false,
245 dimmed: false,
246 strikethrough: false,
247 inverted: false,
248 blink: false,
249 concealed: false,
250 fg: None,
251 bg: None,
252 };
253
254 #[cfg(feature = "svg")]
255 #[allow(clippy::trivially_copy_pass_by_ref)] fn is_false(&val: &bool) -> bool {
257 !val
258 }
259
260 pub(crate) fn is_none(&self) -> bool {
261 !self.bold
262 && !self.italic
263 && !self.underline
264 && !self.dimmed
265 && !self.strikethrough
266 && !self.inverted
267 && !self.blink
268 && !self.concealed
269 && self.fg.is_none()
270 && self.bg.is_none()
271 }
272
273 #[cfg(any(feature = "svg", feature = "test"))]
274 pub(crate) fn normalize(&mut self) {
275 if let Some(color) = &mut self.fg {
276 color.normalize();
277 }
278 if let Some(color) = &mut self.bg {
279 color.normalize();
280 }
281 }
282}
283
284impl Color {
285 fn write_params(self, formatter: &mut fmt::Formatter<'_>, is_bg: bool) -> fmt::Result {
286 match self {
287 Self::Index(idx) if idx < 8 => {
288 let offset = if is_bg { 40 } else { 30 };
289 write!(formatter, "\u{1b}[{}m", offset + idx)
290 }
291 Self::Index(idx) => {
292 let prefix = if is_bg { 48 } else { 38 };
293 write!(formatter, "\u{1b}[{prefix};5;{idx}m")
294 }
295 Self::Rgb(RgbColor(r, g, b)) => {
296 let prefix = if is_bg { 48 } else { 38 };
297 write!(formatter, "\u{1b}[{prefix};2;{r};{g};{b}m")
298 }
299 }
300 }
301}
302
303#[cfg(any(feature = "svg", feature = "test"))]
305#[derive(Debug, Clone, Copy, Default, PartialEq)]
306#[cfg_attr(feature = "svg", derive(serde::Serialize))]
307pub(crate) struct StyledSpan<T = String> {
308 #[cfg_attr(feature = "svg", serde(flatten))]
309 pub(crate) style: Style,
310 pub(crate) text: T,
311}
312
313pub(crate) trait WriteStyled {
315 fn write_style(&mut self, style: &Style) -> io::Result<()>;
317
318 fn write_text(&mut self, text: &str) -> io::Result<()>;
319
320 #[cfg(test)]
321 fn reset(&mut self) -> io::Result<()> {
322 self.write_style(&Style::default())
323 }
324}
325
326impl WriteStyled for io::Sink {
328 fn write_style(&mut self, _style: &Style) -> io::Result<()> {
329 Ok(())
330 }
331
332 fn write_text(&mut self, _text: &str) -> io::Result<()> {
333 Ok(())
334 }
335}
336
337impl WriteStyled for String {
338 fn write_style(&mut self, _style: &Style) -> io::Result<()> {
339 Ok(())
340 }
341
342 fn write_text(&mut self, text: &str) -> io::Result<()> {
343 self.push_str(text);
344 Ok(())
345 }
346}
347
348impl<T: WriteStyled + ?Sized> WriteStyled for &mut T {
349 fn write_style(&mut self, style: &Style) -> io::Result<()> {
350 (**self).write_style(style)
351 }
352
353 fn write_text(&mut self, text: &str) -> io::Result<()> {
354 (**self).write_text(text)
355 }
356}
357
358#[cfg(test)]
359mod tests {
360 use assert_matches::assert_matches;
361
362 use super::*;
363
364 #[test]
365 fn parsing_color() {
366 let RgbColor(r, g, b) = "#fed".parse().unwrap();
367 assert_eq!((r, g, b), (0xff, 0xee, 0xdd));
368 let RgbColor(r, g, b) = "#c0ffee".parse().unwrap();
369 assert_eq!((r, g, b), (0xc0, 0xff, 0xee));
370 }
371
372 #[test]
373 fn errors_parsing_color() {
374 let err = "123".parse::<RgbColor>().unwrap_err();
375 assert_matches!(err, RgbColorParseError::NoHashPrefix);
376 let err = "#12".parse::<RgbColor>().unwrap_err();
377 assert_matches!(err, RgbColorParseError::IncorrectLen(3));
378 let err = "#тэг".parse::<RgbColor>().unwrap_err();
379 assert_matches!(err, RgbColorParseError::NotAscii);
380 let err = "#coffee".parse::<RgbColor>().unwrap_err();
381 assert_matches!(err, RgbColorParseError::IncorrectDigit(_));
382 }
383}