1#![allow(clippy::cast_sign_loss)]
2#![allow(clippy::cast_possible_truncation)]
3use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8pub enum Color {
14 Rgb { r: u8, g: u8, b: u8 },
16 Rgba { r: u8, g: u8, b: u8, a: u8 },
19}
20
21impl Default for Color {
22 fn default() -> Self {
27 Color::Rgb { r: 0, g: 0, b: 0 }
28 }
29}
30
31impl From<Color> for crossterm::style::Color {
32 fn from(color: Color) -> Self {
40 match color {
41 Color::Rgb { r, g, b } | Color::Rgba { r, g, b, a: _ } => {
42 crossterm::style::Color::Rgb { r, g, b }
43 }
44 }
45 }
46}
47
48impl fmt::Display for Color {
49 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
57 match self {
58 Color::Rgb { r, g, b } => write!(f, "#{r:02x}{g:02x}{b:02x}"),
59 Color::Rgba { r, g, b, a } => write!(f, "#{r:02x}{g:02x}{b:02x}{a:02x}"),
60 }
61 }
62}
63
64pub fn parse_rgb(s: &str) -> anyhow::Result<Color> {
76 if !s.starts_with('#') {
77 anyhow::bail!("Invalid hex string: {s}");
78 }
79
80 let hex = s.trim_start_matches('#');
81 let len = hex.len();
82
83 if len != 6 && len != 8 {
84 anyhow::bail!("Hex string must be in the format #RRGGBB or #RRGGBBAA instead of {s}");
85 }
86
87 let red = u8::from_str_radix(&hex[0..2], 16)?;
88 let green = u8::from_str_radix(&hex[2..4], 16)?;
89 let blue = u8::from_str_radix(&hex[4..6], 16)?;
90
91 if len == 8 {
92 let alpha = u8::from_str_radix(&hex[6..8], 16)?;
93 Ok(Color::Rgba {
94 r: red,
95 g: green,
96 b: blue,
97 a: alpha,
98 })
99 } else {
100 Ok(Color::Rgb {
101 r: red,
102 g: green,
103 b: blue,
104 })
105 }
106}
107
108#[must_use]
109pub fn blend_color(foreground: Color, background: Color) -> Color {
118 match (foreground, background) {
119 (
120 Color::Rgba { r, g, b, a },
121 Color::Rgb {
122 r: background_r,
123 g: background_g,
124 b: background_b,
125 },
126 ) => {
127 let alpha = f32::from(a) / 255.0;
128 let inv_alpha = 1.0 - alpha;
129
130 let r = (f32::from(r) * alpha + f32::from(background_r) * inv_alpha) as u8;
131 let g = (f32::from(g) * alpha + f32::from(background_g) * inv_alpha) as u8;
132 let b = (f32::from(b) * alpha + f32::from(background_b) * inv_alpha) as u8;
133
134 Color::Rgb { r, g, b }
135 }
136 _ => foreground,
137 }
138}
139
140#[cfg(test)]
141mod test {
142
143 use super::*;
144
145 #[test]
146 fn test_parse_rgb_simple() {
147 let hex_str = "#000000";
148 assert_eq!(parse_rgb(hex_str).unwrap(), Color::Rgb { r: 0, g: 0, b: 0 });
149 }
150
151 #[test]
152 fn test_parse_rgb_complex() {
153 let hex_str = "#d35a87";
154 assert_eq!(
155 parse_rgb(hex_str).unwrap(),
156 Color::Rgb {
157 r: 0xd3,
158 g: 0x5a,
159 b: 0x87
160 }
161 );
162 }
163}