1use console::Color;
32
33use crate::colorspace::{CubeCoord, ThemePalette};
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum ColorDef {
38 Named(Color),
40 Color256(u8),
42 Rgb(u8, u8, u8),
44 Cube(CubeCoord),
46}
47
48impl ColorDef {
49 pub fn parse_value(value: &serde_yaml::Value) -> Result<Self, String> {
56 match value {
57 serde_yaml::Value::String(s) => Self::parse_string(s),
58 serde_yaml::Value::Number(n) => {
59 let index = n
60 .as_u64()
61 .ok_or_else(|| format!("Invalid color palette index: {}", n))?;
62 if index > 255 {
63 return Err(format!(
64 "Color palette index {} out of range (0-255)",
65 index
66 ));
67 }
68 Ok(ColorDef::Color256(index as u8))
69 }
70 serde_yaml::Value::Sequence(seq) => Self::parse_rgb_tuple(seq),
71 _ => Err(format!("Invalid color value: {:?}", value)),
72 }
73 }
74
75 pub fn parse_string(s: &str) -> Result<Self, String> {
83 let s = s.trim();
84
85 if s.starts_with("cube(") && s.ends_with(')') {
87 return Self::parse_cube(s);
88 }
89
90 if let Some(hex) = s.strip_prefix('#') {
92 return Self::parse_hex(hex);
93 }
94
95 Self::parse_named(s)
97 }
98
99 fn parse_cube(s: &str) -> Result<Self, String> {
103 let inner = &s[5..s.len() - 1]; let parts: Vec<&str> = inner.split(',').map(|p| p.trim()).collect();
105 if parts.len() != 3 {
106 return Err(format!(
107 "cube() requires exactly 3 components, got {}",
108 parts.len()
109 ));
110 }
111
112 let mut values = [0.0f64; 3];
113 for (i, part) in parts.iter().enumerate() {
114 let num_str = part.strip_suffix('%').unwrap_or(part).trim();
115 values[i] = num_str
116 .parse::<f64>()
117 .map_err(|_| format!("Invalid cube component '{}': expected a number", part))?;
118 }
119
120 let coord = CubeCoord::from_percentages(values[0], values[1], values[2])?;
121 Ok(ColorDef::Cube(coord))
122 }
123
124 fn parse_hex(hex: &str) -> Result<Self, String> {
126 match hex.len() {
127 3 => {
129 let r = u8::from_str_radix(&hex[0..1], 16)
130 .map_err(|_| format!("Invalid hex: {}", hex))?
131 * 17;
132 let g = u8::from_str_radix(&hex[1..2], 16)
133 .map_err(|_| format!("Invalid hex: {}", hex))?
134 * 17;
135 let b = u8::from_str_radix(&hex[2..3], 16)
136 .map_err(|_| format!("Invalid hex: {}", hex))?
137 * 17;
138 Ok(ColorDef::Rgb(r, g, b))
139 }
140 6 => {
142 let r = u8::from_str_radix(&hex[0..2], 16)
143 .map_err(|_| format!("Invalid hex: {}", hex))?;
144 let g = u8::from_str_radix(&hex[2..4], 16)
145 .map_err(|_| format!("Invalid hex: {}", hex))?;
146 let b = u8::from_str_radix(&hex[4..6], 16)
147 .map_err(|_| format!("Invalid hex: {}", hex))?;
148 Ok(ColorDef::Rgb(r, g, b))
149 }
150 _ => Err(format!(
151 "Invalid hex color: #{} (must be 3 or 6 digits)",
152 hex
153 )),
154 }
155 }
156
157 fn parse_named(name: &str) -> Result<Self, String> {
159 let name_lower = name.to_lowercase();
160
161 if let Some(base) = name_lower.strip_prefix("bright_") {
163 return Self::parse_bright_color(base);
164 }
165
166 let color = match name_lower.as_str() {
168 "black" => Color::Black,
169 "red" => Color::Red,
170 "green" => Color::Green,
171 "yellow" => Color::Yellow,
172 "blue" => Color::Blue,
173 "magenta" => Color::Magenta,
174 "cyan" => Color::Cyan,
175 "white" => Color::White,
176 "gray" | "grey" => Color::White,
178 _ => return Err(format!("Unknown color name: {}", name)),
179 };
180
181 Ok(ColorDef::Named(color))
182 }
183
184 fn parse_bright_color(base: &str) -> Result<Self, String> {
186 let index = match base {
188 "black" => 8,
189 "red" => 9,
190 "green" => 10,
191 "yellow" => 11,
192 "blue" => 12,
193 "magenta" => 13,
194 "cyan" => 14,
195 "white" => 15,
196 _ => return Err(format!("Unknown bright color: bright_{}", base)),
197 };
198
199 Ok(ColorDef::Color256(index))
200 }
201
202 fn parse_rgb_tuple(seq: &[serde_yaml::Value]) -> Result<Self, String> {
204 if seq.len() != 3 {
205 return Err(format!(
206 "RGB tuple must have exactly 3 values, got {}",
207 seq.len()
208 ));
209 }
210
211 let mut components = [0u8; 3];
212 for (i, val) in seq.iter().enumerate() {
213 let n = val
214 .as_u64()
215 .ok_or_else(|| format!("RGB component {} is not a number", i))?;
216 if n > 255 {
217 return Err(format!("RGB component {} out of range (0-255): {}", i, n));
218 }
219 components[i] = n as u8;
220 }
221
222 Ok(ColorDef::Rgb(components[0], components[1], components[2]))
223 }
224
225 pub fn to_console_color(&self, palette: Option<&ThemePalette>) -> Color {
231 match self {
232 ColorDef::Named(c) => *c,
233 ColorDef::Color256(n) => Color::Color256(*n),
234 ColorDef::Rgb(r, g, b) => Color::Color256(crate::rgb_to_ansi256((*r, *g, *b))),
235 ColorDef::Cube(coord) => {
236 let p;
237 let palette = match palette {
238 Some(pal) => pal,
239 None => {
240 p = ThemePalette::default_xterm();
241 &p
242 }
243 };
244 let rgb = palette.resolve(coord);
245 Color::Color256(crate::rgb_to_ansi256((rgb.0, rgb.1, rgb.2)))
246 }
247 }
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254 use serde_yaml::Value;
255
256 #[test]
261 fn test_parse_named_colors() {
262 assert_eq!(
263 ColorDef::parse_string("red").unwrap(),
264 ColorDef::Named(Color::Red)
265 );
266 assert_eq!(
267 ColorDef::parse_string("green").unwrap(),
268 ColorDef::Named(Color::Green)
269 );
270 assert_eq!(
271 ColorDef::parse_string("blue").unwrap(),
272 ColorDef::Named(Color::Blue)
273 );
274 assert_eq!(
275 ColorDef::parse_string("yellow").unwrap(),
276 ColorDef::Named(Color::Yellow)
277 );
278 assert_eq!(
279 ColorDef::parse_string("magenta").unwrap(),
280 ColorDef::Named(Color::Magenta)
281 );
282 assert_eq!(
283 ColorDef::parse_string("cyan").unwrap(),
284 ColorDef::Named(Color::Cyan)
285 );
286 assert_eq!(
287 ColorDef::parse_string("white").unwrap(),
288 ColorDef::Named(Color::White)
289 );
290 assert_eq!(
291 ColorDef::parse_string("black").unwrap(),
292 ColorDef::Named(Color::Black)
293 );
294 }
295
296 #[test]
297 fn test_parse_named_colors_case_insensitive() {
298 assert_eq!(
299 ColorDef::parse_string("RED").unwrap(),
300 ColorDef::Named(Color::Red)
301 );
302 assert_eq!(
303 ColorDef::parse_string("Red").unwrap(),
304 ColorDef::Named(Color::Red)
305 );
306 }
307
308 #[test]
309 fn test_parse_gray_aliases() {
310 assert_eq!(
311 ColorDef::parse_string("gray").unwrap(),
312 ColorDef::Named(Color::White)
313 );
314 assert_eq!(
315 ColorDef::parse_string("grey").unwrap(),
316 ColorDef::Named(Color::White)
317 );
318 }
319
320 #[test]
321 fn test_parse_unknown_color() {
322 assert!(ColorDef::parse_string("purple").is_err());
323 assert!(ColorDef::parse_string("orange").is_err());
324 }
325
326 #[test]
331 fn test_parse_bright_colors() {
332 assert_eq!(
333 ColorDef::parse_string("bright_red").unwrap(),
334 ColorDef::Color256(9)
335 );
336 assert_eq!(
337 ColorDef::parse_string("bright_green").unwrap(),
338 ColorDef::Color256(10)
339 );
340 assert_eq!(
341 ColorDef::parse_string("bright_blue").unwrap(),
342 ColorDef::Color256(12)
343 );
344 assert_eq!(
345 ColorDef::parse_string("bright_black").unwrap(),
346 ColorDef::Color256(8)
347 );
348 assert_eq!(
349 ColorDef::parse_string("bright_white").unwrap(),
350 ColorDef::Color256(15)
351 );
352 }
353
354 #[test]
355 fn test_parse_unknown_bright_color() {
356 assert!(ColorDef::parse_string("bright_purple").is_err());
357 }
358
359 #[test]
364 fn test_parse_hex_6_digit() {
365 assert_eq!(
366 ColorDef::parse_string("#ff6b35").unwrap(),
367 ColorDef::Rgb(255, 107, 53)
368 );
369 assert_eq!(
370 ColorDef::parse_string("#000000").unwrap(),
371 ColorDef::Rgb(0, 0, 0)
372 );
373 assert_eq!(
374 ColorDef::parse_string("#ffffff").unwrap(),
375 ColorDef::Rgb(255, 255, 255)
376 );
377 }
378
379 #[test]
380 fn test_parse_hex_3_digit() {
381 assert_eq!(
382 ColorDef::parse_string("#fff").unwrap(),
383 ColorDef::Rgb(255, 255, 255)
384 );
385 assert_eq!(
386 ColorDef::parse_string("#000").unwrap(),
387 ColorDef::Rgb(0, 0, 0)
388 );
389 assert_eq!(
390 ColorDef::parse_string("#f80").unwrap(),
391 ColorDef::Rgb(255, 136, 0)
392 );
393 }
394
395 #[test]
396 fn test_parse_hex_case_insensitive() {
397 assert_eq!(
398 ColorDef::parse_string("#FF6B35").unwrap(),
399 ColorDef::Rgb(255, 107, 53)
400 );
401 assert_eq!(
402 ColorDef::parse_string("#FFF").unwrap(),
403 ColorDef::Rgb(255, 255, 255)
404 );
405 }
406
407 #[test]
408 fn test_parse_hex_invalid() {
409 assert!(ColorDef::parse_string("#ff").is_err());
410 assert!(ColorDef::parse_string("#ffff").is_err());
411 assert!(ColorDef::parse_string("#gggggg").is_err());
412 }
413
414 #[test]
419 fn test_parse_value_string() {
420 let val = Value::String("red".into());
421 assert_eq!(
422 ColorDef::parse_value(&val).unwrap(),
423 ColorDef::Named(Color::Red)
424 );
425 }
426
427 #[test]
428 fn test_parse_value_number() {
429 let val = Value::Number(208.into());
430 assert_eq!(
431 ColorDef::parse_value(&val).unwrap(),
432 ColorDef::Color256(208)
433 );
434 }
435
436 #[test]
437 fn test_parse_value_number_out_of_range() {
438 let val = Value::Number(256.into());
439 assert!(ColorDef::parse_value(&val).is_err());
440 }
441
442 #[test]
443 fn test_parse_value_sequence() {
444 let val = Value::Sequence(vec![
445 Value::Number(255.into()),
446 Value::Number(107.into()),
447 Value::Number(53.into()),
448 ]);
449 assert_eq!(
450 ColorDef::parse_value(&val).unwrap(),
451 ColorDef::Rgb(255, 107, 53)
452 );
453 }
454
455 #[test]
456 fn test_parse_value_sequence_wrong_length() {
457 let val = Value::Sequence(vec![Value::Number(255.into()), Value::Number(107.into())]);
458 assert!(ColorDef::parse_value(&val).is_err());
459 }
460
461 #[test]
462 fn test_parse_value_sequence_out_of_range() {
463 let val = Value::Sequence(vec![
464 Value::Number(256.into()),
465 Value::Number(107.into()),
466 Value::Number(53.into()),
467 ]);
468 assert!(ColorDef::parse_value(&val).is_err());
469 }
470
471 #[test]
476 fn test_to_console_color_named() {
477 let c = ColorDef::Named(Color::Red);
478 assert_eq!(c.to_console_color(None), Color::Red);
479 }
480
481 #[test]
482 fn test_to_console_color_256() {
483 let c = ColorDef::Color256(208);
484 assert_eq!(c.to_console_color(None), Color::Color256(208));
485 }
486
487 #[test]
488 fn test_to_console_color_rgb() {
489 let c = ColorDef::Rgb(255, 107, 53);
490 if let Color::Color256(_) = c.to_console_color(None) {
492 } else {
494 panic!("Expected Color256");
495 }
496 }
497
498 #[test]
503 fn test_parse_cube_percentages() {
504 let c = ColorDef::parse_string("cube(60%, 20%, 0%)").unwrap();
505 match c {
506 ColorDef::Cube(coord) => {
507 assert!((coord.r - 0.6).abs() < 0.001);
508 assert!((coord.g - 0.2).abs() < 0.001);
509 assert!((coord.b - 0.0).abs() < 0.001);
510 }
511 _ => panic!("Expected Cube"),
512 }
513 }
514
515 #[test]
516 fn test_parse_cube_without_percent_sign() {
517 let c = ColorDef::parse_string("cube(100, 50, 0)").unwrap();
518 match c {
519 ColorDef::Cube(coord) => {
520 assert!((coord.r - 1.0).abs() < 0.001);
521 assert!((coord.g - 0.5).abs() < 0.001);
522 assert!((coord.b - 0.0).abs() < 0.001);
523 }
524 _ => panic!("Expected Cube"),
525 }
526 }
527
528 #[test]
529 fn test_parse_cube_corners() {
530 let c = ColorDef::parse_string("cube(0%, 0%, 0%)").unwrap();
532 assert!(matches!(c, ColorDef::Cube(_)));
533
534 let c = ColorDef::parse_string("cube(100%, 100%, 100%)").unwrap();
536 assert!(matches!(c, ColorDef::Cube(_)));
537 }
538
539 #[test]
540 fn test_parse_cube_out_of_range() {
541 assert!(ColorDef::parse_string("cube(101%, 0%, 0%)").is_err());
542 assert!(ColorDef::parse_string("cube(-1%, 0%, 0%)").is_err());
543 }
544
545 #[test]
546 fn test_parse_cube_wrong_arg_count() {
547 assert!(ColorDef::parse_string("cube(60%, 20%)").is_err());
548 assert!(ColorDef::parse_string("cube(60%, 20%, 0%, 10%)").is_err());
549 }
550
551 #[test]
552 fn test_parse_cube_invalid_number() {
553 assert!(ColorDef::parse_string("cube(abc, 20%, 0%)").is_err());
554 }
555
556 #[test]
557 fn test_to_console_color_cube() {
558 use crate::colorspace::CubeCoord;
559 let coord = CubeCoord::from_percentages(60.0, 20.0, 0.0).unwrap();
560 let c = ColorDef::Cube(coord);
561 if let Color::Color256(_) = c.to_console_color(None) {
563 } else {
565 panic!("Expected Color256 from cube resolution");
566 }
567 }
568
569 #[test]
570 fn test_to_console_color_cube_with_palette() {
571 use crate::colorspace::{CubeCoord, Rgb, ThemePalette};
572 let palette = ThemePalette::new([
573 Rgb(40, 40, 40),
574 Rgb(204, 36, 29),
575 Rgb(152, 151, 26),
576 Rgb(215, 153, 33),
577 Rgb(69, 133, 136),
578 Rgb(177, 98, 134),
579 Rgb(104, 157, 106),
580 Rgb(168, 153, 132),
581 ]);
582 let coord = CubeCoord::from_percentages(0.0, 0.0, 0.0).unwrap();
583 let c = ColorDef::Cube(coord);
584 if let Color::Color256(_) = c.to_console_color(Some(&palette)) {
586 } else {
588 panic!("Expected Color256");
589 }
590 }
591
592 #[test]
593 fn test_parse_value_cube_string() {
594 let val = Value::String("cube(50%, 50%, 50%)".into());
595 let c = ColorDef::parse_value(&val).unwrap();
596 assert!(matches!(c, ColorDef::Cube(_)));
597 }
598}