1use alloc::string::String;
5
6use super::Color;
7use super::ansi::AnsiColor;
8
9const fn ansi_name(color: AnsiColor) -> &'static str {
11 match color {
12 AnsiColor::Black => "black",
13 AnsiColor::Red => "red",
14 AnsiColor::Green => "green",
15 AnsiColor::Yellow => "yellow",
16 AnsiColor::Blue => "blue",
17 AnsiColor::Magenta => "magenta",
18 AnsiColor::Cyan => "cyan",
19 AnsiColor::White => "white",
20 AnsiColor::BrightBlack => "bright-black",
21 AnsiColor::BrightRed => "bright-red",
22 AnsiColor::BrightGreen => "bright-green",
23 AnsiColor::BrightYellow => "bright-yellow",
24 AnsiColor::BrightBlue => "bright-blue",
25 AnsiColor::BrightMagenta => "bright-magenta",
26 AnsiColor::BrightCyan => "bright-cyan",
27 AnsiColor::BrightWhite => "bright-white",
28 }
29}
30
31fn parse_ansi_name(name: &str) -> Option<AnsiColor> {
34 Some(match name {
35 "black" => AnsiColor::Black,
36 "red" => AnsiColor::Red,
37 "green" => AnsiColor::Green,
38 "yellow" => AnsiColor::Yellow,
39 "blue" => AnsiColor::Blue,
40 "magenta" => AnsiColor::Magenta,
41 "cyan" => AnsiColor::Cyan,
42 "white" => AnsiColor::White,
43 "brightblack" => AnsiColor::BrightBlack,
44 "brightred" => AnsiColor::BrightRed,
45 "brightgreen" => AnsiColor::BrightGreen,
46 "brightyellow" => AnsiColor::BrightYellow,
47 "brightblue" => AnsiColor::BrightBlue,
48 "brightmagenta" => AnsiColor::BrightMagenta,
49 "brightcyan" => AnsiColor::BrightCyan,
50 "brightwhite" => AnsiColor::BrightWhite,
51 _ => return None,
52 })
53}
54
55fn parse_hex(s: &str) -> Option<(u8, u8, u8)> {
62 let hex = s.strip_prefix('#')?;
63 match hex.len() {
64 3 => {
65 let mut chars = hex.chars();
66 let r = u8::try_from(chars.next()?.to_digit(16)?).ok()?;
67 let g = u8::try_from(chars.next()?.to_digit(16)?).ok()?;
68 let b = u8::try_from(chars.next()?.to_digit(16)?).ok()?;
69 Some((r * 17, g * 17, b * 17))
73 }
74 6 => {
75 if !hex.bytes().all(|b| b.is_ascii_hexdigit()) {
76 return None;
77 }
78 let r = u8::from_str_radix(hex.get(0..2)?, 16).ok()?;
79 let g = u8::from_str_radix(hex.get(2..4)?, 16).ok()?;
80 let b = u8::from_str_radix(hex.get(4..6)?, 16).ok()?;
81 Some((r, g, b))
82 }
83 _ => None,
84 }
85}
86
87impl core::fmt::Display for Color {
88 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
103 match *self {
104 Self::Default => write!(f, "default"),
105 Self::Ansi(color) => write!(f, "{}", ansi_name(color)),
106 Self::Indexed(i) => write!(f, "{i}"),
107 Self::Rgb { r, g, b } => write!(f, "#{r:02x}{g:02x}{b:02x}"),
108 }
109 }
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
114pub struct ParseColorError;
115
116impl core::fmt::Display for ParseColorError {
117 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
118 write!(f, "invalid color string")
119 }
120}
121
122impl core::error::Error for ParseColorError {}
123
124impl core::str::FromStr for Color {
125 type Err = ParseColorError;
126
127 fn from_str(s: &str) -> Result<Self, Self::Err> {
149 let trimmed = s.trim();
150 let normalized: String = trimmed
151 .chars()
152 .filter(|c| !matches!(c, '-' | '_' | ' '))
153 .map(|c| c.to_ascii_lowercase())
154 .collect();
155
156 if normalized == "default" || normalized == "reset" {
157 return Ok(Self::Default);
158 }
159 if let Some(color) = parse_ansi_name(&normalized) {
160 return Ok(Self::Ansi(color));
161 }
162 if normalized.bytes().all(|b| b.is_ascii_digit())
163 && !normalized.is_empty()
164 && let Ok(index) = normalized.parse::<u8>()
165 {
166 return Ok(Self::Indexed(index));
167 }
168 if let Some((r, g, b)) = parse_hex(trimmed) {
169 return Ok(Self::Rgb { r, g, b });
170 }
171 Err(ParseColorError)
172 }
173}
174
175#[cfg(feature = "serde")]
176impl serde::Serialize for Color {
177 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
181 where
182 S: serde::Serializer,
183 {
184 use alloc::string::ToString as _;
185
186 serializer.serialize_str(&self.to_string())
187 }
188}
189
190#[cfg(feature = "serde")]
191impl<'de> serde::Deserialize<'de> for Color {
192 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
196 where
197 D: serde::Deserializer<'de>,
198 {
199 struct ColorVisitor;
200
201 impl serde::de::Visitor<'_> for ColorVisitor {
202 type Value = Color;
203
204 fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
205 f.write_str(
206 "a color string: \"default\", an ANSI name, a palette index, or #rrggbb hex",
207 )
208 }
209
210 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
211 where
212 E: serde::de::Error,
213 {
214 v.parse().map_err(E::custom)
215 }
216 }
217
218 deserializer.deserialize_str(ColorVisitor)
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[test]
227 fn test_display() {
228 use alloc::string::ToString as _;
229
230 assert_eq!(Color::Default.to_string(), "default");
231 assert_eq!(Color::Ansi(AnsiColor::Black).to_string(), "black");
232 assert_eq!(
233 Color::Ansi(AnsiColor::BrightYellow).to_string(),
234 "bright-yellow"
235 );
236 assert_eq!(Color::Indexed(7).to_string(), "7");
237 assert_eq!(
238 Color::Rgb {
239 r: 255,
240 g: 128,
241 b: 0
242 }
243 .to_string(),
244 "#ff8000"
245 );
246 }
247
248 #[test]
249 fn test_from_str_round_trips_display() {
250 use alloc::string::ToString as _;
251
252 let colors = [
253 Color::Default,
254 Color::Ansi(AnsiColor::Black),
255 Color::Ansi(AnsiColor::BrightMagenta),
256 Color::Indexed(200),
257 Color::Rgb {
258 r: 18,
259 g: 52,
260 b: 86,
261 },
262 ];
263 for color in colors {
264 let s = color.to_string();
265 assert_eq!(s.parse(), Ok(color), "round trip of {s:?}");
266 }
267 }
268
269 #[test]
270 fn test_from_str_aliases_and_separators() {
271 assert_eq!("reset".parse(), Ok(Color::Default));
272 assert_eq!("Default".parse(), Ok(Color::Default));
273 assert_eq!("BrightRed".parse(), Ok(Color::Ansi(AnsiColor::BrightRed)));
274 assert_eq!("bright red".parse(), Ok(Color::Ansi(AnsiColor::BrightRed)));
275 assert_eq!("bright_red".parse(), Ok(Color::Ansi(AnsiColor::BrightRed)));
276 assert_eq!(
277 "#F80".parse(),
278 Ok(Color::Rgb {
279 r: 255,
280 g: 136,
281 b: 0
282 })
283 );
284 assert_eq!(
285 " #ff8000 ".parse(),
286 Ok(Color::Rgb {
287 r: 255,
288 g: 128,
289 b: 0
290 })
291 );
292 }
293
294 #[test]
295 fn test_from_str_invalid() {
296 assert!("not-a-color".parse::<Color>().is_err());
297 assert!("#gg0000".parse::<Color>().is_err());
298 assert!("#12345".parse::<Color>().is_err());
299 assert!("256".parse::<Color>().is_err());
300 }
301
302 #[test]
303 fn from_str_rejects_plus_signed_index_and_hex() {
304 assert!("+5".parse::<Color>().is_err());
307 assert!("#+f0000".parse::<Color>().is_err());
308 }
309
310 #[cfg(feature = "serde")]
311 #[test]
312 fn test_serialize_then_deserialize() -> Result<(), serde_json::Error> {
313 let colors = [
314 Color::Default,
315 Color::Ansi(AnsiColor::BrightGreen),
316 Color::Indexed(42),
317 Color::Rgb {
318 r: 255,
319 g: 0,
320 b: 255,
321 },
322 ];
323 for color in colors {
324 let json = serde_json::to_string(&color)?;
325 assert_eq!(serde_json::from_str::<Color>(&json)?, color);
326 }
327
328 assert_eq!(
329 serde_json::to_string(&Color::Rgb {
330 r: 255,
331 g: 0,
332 b: 255
333 })?,
334 r##""#ff00ff""##
335 );
336 assert_eq!(
337 serde_json::from_str::<Color>(r#""bright-white""#)?,
338 Color::Ansi(AnsiColor::BrightWhite)
339 );
340 assert!(serde_json::from_str::<Color>(r#""not-a-color""#).is_err());
341
342 Ok(())
343 }
344}