1use thiserror::Error;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7pub struct Color {
8 pub r: u8,
9 pub g: u8,
10 pub b: u8,
11 pub a: u8,
12}
13
14impl Color {
15 pub const RED: Color = Color::rgb(255, 0, 0);
16 pub const GREEN: Color = Color::rgb(0, 200, 0);
17 pub const BLUE: Color = Color::rgb(0, 100, 255);
18 pub const YELLOW: Color = Color::rgb(255, 220, 0);
19 pub const CYAN: Color = Color::rgb(0, 220, 220);
20 pub const MAGENTA: Color = Color::rgb(220, 0, 220);
21 pub const WHITE: Color = Color::rgb(255, 255, 255);
22 pub const BLACK: Color = Color::rgb(0, 0, 0);
23 pub const ORANGE: Color = Color::rgb(255, 140, 0);
24 pub const PURPLE: Color = Color::rgb(160, 32, 240);
25 pub const PINK: Color = Color::rgb(255, 105, 180);
26 pub const GRAY: Color = Color::rgb(128, 128, 128);
27
28 pub const EXPECTED: Color = Color::GREEN;
30 pub const ACTUAL: Color = Color::RED;
32 pub const HINT: Color = Color::YELLOW;
34 pub const ERROR: Color = Color::RED;
36 pub const SUCCESS: Color = Color::GREEN;
38
39 pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
40 Self { r, g, b, a: 255 }
41 }
42
43 pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
44 Self { r, g, b, a }
45 }
46
47 pub fn to_rgba(self) -> [u8; 4] {
48 [self.r, self.g, self.b, self.a]
49 }
50
51 pub fn parse(s: &str) -> Result<Self, ColorParseError> {
54 let s = s.trim();
55 if let Some(hex) = s.strip_prefix('#') {
56 return parse_hex(hex);
57 }
58 if s.starts_with("rgb(") && s.ends_with(')') {
59 return parse_rgb_fn(&s[4..s.len() - 1]);
60 }
61 if s.starts_with("rgba(") && s.ends_with(')') {
62 return parse_rgba_fn(&s[5..s.len() - 1]);
63 }
64 parse_named(s)
65 }
66}
67
68fn parse_hex(hex: &str) -> Result<Color, ColorParseError> {
69 let bytes: Result<Vec<u8>, _> = (0..hex.len())
70 .step_by(2)
71 .map(|i| u8::from_str_radix(&hex[i..i + 2], 16))
72 .collect();
73 let bytes = bytes.map_err(|_| ColorParseError::BadHex(hex.to_string()))?;
74 match bytes.len() {
75 3 => Ok(Color::rgb(bytes[0], bytes[1], bytes[2])),
76 4 => Ok(Color::rgba(bytes[0], bytes[1], bytes[2], bytes[3])),
77 _ => Err(ColorParseError::BadHex(hex.to_string())),
78 }
79}
80
81fn parse_rgb_fn(inner: &str) -> Result<Color, ColorParseError> {
82 let parts: Vec<&str> = inner.split(',').map(|s| s.trim()).collect();
83 if parts.len() != 3 {
84 return Err(ColorParseError::BadRgb(inner.to_string()));
85 }
86 let r = parts[0]
87 .parse()
88 .map_err(|_| ColorParseError::BadRgb(inner.to_string()))?;
89 let g = parts[1]
90 .parse()
91 .map_err(|_| ColorParseError::BadRgb(inner.to_string()))?;
92 let b = parts[2]
93 .parse()
94 .map_err(|_| ColorParseError::BadRgb(inner.to_string()))?;
95 Ok(Color::rgb(r, g, b))
96}
97
98fn parse_rgba_fn(inner: &str) -> Result<Color, ColorParseError> {
99 let parts: Vec<&str> = inner.split(',').map(|s| s.trim()).collect();
100 if parts.len() != 4 {
101 return Err(ColorParseError::BadRgb(inner.to_string()));
102 }
103 let r = parts[0]
104 .parse()
105 .map_err(|_| ColorParseError::BadRgb(inner.to_string()))?;
106 let g = parts[1]
107 .parse()
108 .map_err(|_| ColorParseError::BadRgb(inner.to_string()))?;
109 let b = parts[2]
110 .parse()
111 .map_err(|_| ColorParseError::BadRgb(inner.to_string()))?;
112 let a_f: f32 = parts[3]
113 .parse()
114 .map_err(|_| ColorParseError::BadRgb(inner.to_string()))?;
115 let a = (a_f.clamp(0.0, 1.0) * 255.0) as u8;
116 Ok(Color::rgba(r, g, b, a))
117}
118
119fn parse_named(s: &str) -> Result<Color, ColorParseError> {
120 match s.to_lowercase().as_str() {
121 "red" => Ok(Color::RED),
122 "green" => Ok(Color::GREEN),
123 "blue" => Ok(Color::BLUE),
124 "yellow" => Ok(Color::YELLOW),
125 "cyan" => Ok(Color::CYAN),
126 "magenta" => Ok(Color::MAGENTA),
127 "white" => Ok(Color::WHITE),
128 "black" => Ok(Color::BLACK),
129 "orange" => Ok(Color::ORANGE),
130 "purple" => Ok(Color::PURPLE),
131 "pink" => Ok(Color::PINK),
132 "gray" | "grey" => Ok(Color::GRAY),
133 "expected" | "success" => Ok(Color::GREEN),
134 "actual" | "error" => Ok(Color::RED),
135 "hint" => Ok(Color::YELLOW),
136 other => Err(ColorParseError::UnknownName(other.to_string())),
137 }
138}
139
140#[derive(Debug, Error)]
141pub enum ColorParseError {
142 #[error("unknown color name: {0}")]
143 UnknownName(String),
144 #[error("bad hex color: {0}")]
145 BadHex(String),
146 #[error("bad rgb/rgba color: {0}")]
147 BadRgb(String),
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn named() {
156 assert_eq!(Color::parse("red").unwrap(), Color::RED);
157 assert_eq!(Color::parse("EXPECTED").unwrap(), Color::GREEN);
158 }
159
160 #[test]
161 fn hex_short() {
162 assert_eq!(Color::parse("#FF0000").unwrap(), Color::RED);
163 assert_eq!(Color::parse("#00CC00").unwrap(), Color::rgb(0, 204, 0));
164 }
165
166 #[test]
167 fn hex_with_alpha() {
168 assert_eq!(
169 Color::parse("#FF000080").unwrap(),
170 Color::rgba(255, 0, 0, 128)
171 );
172 }
173
174 #[test]
175 fn rgb_fn() {
176 assert_eq!(Color::parse("rgb(255, 0, 0)").unwrap(), Color::RED);
177 }
178
179 #[test]
180 fn rgba_fn() {
181 let c = Color::parse("rgba(255, 0, 0, 0.5)").unwrap();
182 assert_eq!(c.r, 255);
183 assert_eq!(c.a, 127);
184 }
185
186 #[test]
187 fn unknown_name_errors() {
188 assert!(matches!(
189 Color::parse("neverheard"),
190 Err(ColorParseError::UnknownName(_))
191 ));
192 }
193}