1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
8pub enum Color {
9 Reset,
11 Black,
13 Red,
15 Green,
17 Yellow,
19 Blue,
21 Magenta,
23 Cyan,
25 White,
27 Rgb(u8, u8, u8),
29 Indexed(u8),
31}
32
33impl Color {
34 fn to_rgb(self) -> (u8, u8, u8) {
39 match self {
40 Color::Rgb(r, g, b) => (r, g, b),
41 Color::Black => (0, 0, 0),
42 Color::Red => (205, 49, 49),
43 Color::Green => (13, 188, 121),
44 Color::Yellow => (229, 229, 16),
45 Color::Blue => (36, 114, 200),
46 Color::Magenta => (188, 63, 188),
47 Color::Cyan => (17, 168, 205),
48 Color::White => (229, 229, 229),
49 Color::Reset => (0, 0, 0),
50 Color::Indexed(idx) => xterm256_to_rgb(idx),
51 }
52 }
53
54 pub fn luminance(self) -> f32 {
72 let (r, g, b) = self.to_rgb();
73 let rf = r as f32 / 255.0;
74 let gf = g as f32 / 255.0;
75 let bf = b as f32 / 255.0;
76 0.2126 * rf + 0.7152 * gf + 0.0722 * bf
77 }
78
79 pub fn contrast_fg(bg: Color) -> Color {
95 if bg.luminance() > 0.5 {
96 Color::Rgb(0, 0, 0)
97 } else {
98 Color::Rgb(255, 255, 255)
99 }
100 }
101
102 pub fn blend(self, other: Color, alpha: f32) -> Color {
118 let alpha = alpha.clamp(0.0, 1.0);
119 let (r1, g1, b1) = self.to_rgb();
120 let (r2, g2, b2) = other.to_rgb();
121 let r = (r1 as f32 * alpha + r2 as f32 * (1.0 - alpha)).round() as u8;
122 let g = (g1 as f32 * alpha + g2 as f32 * (1.0 - alpha)).round() as u8;
123 let b = (b1 as f32 * alpha + b2 as f32 * (1.0 - alpha)).round() as u8;
124 Color::Rgb(r, g, b)
125 }
126
127 pub fn lighten(self, amount: f32) -> Color {
132 Color::Rgb(255, 255, 255).blend(self, 1.0 - amount.clamp(0.0, 1.0))
133 }
134
135 pub fn darken(self, amount: f32) -> Color {
140 Color::Rgb(0, 0, 0).blend(self, 1.0 - amount.clamp(0.0, 1.0))
141 }
142
143 pub fn downsampled(self, depth: ColorDepth) -> Color {
151 match depth {
152 ColorDepth::TrueColor => self,
153 ColorDepth::EightBit => match self {
154 Color::Rgb(r, g, b) => Color::Indexed(rgb_to_ansi256(r, g, b)),
155 other => other,
156 },
157 ColorDepth::Basic => match self {
158 Color::Rgb(r, g, b) => rgb_to_ansi16(r, g, b),
159 Color::Indexed(i) => {
160 let (r, g, b) = xterm256_to_rgb(i);
161 rgb_to_ansi16(r, g, b)
162 }
163 other => other,
164 },
165 }
166 }
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
175#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
176pub enum ColorDepth {
177 TrueColor,
179 EightBit,
181 Basic,
183}
184
185impl ColorDepth {
186 pub fn detect() -> Self {
191 if let Ok(ct) = std::env::var("COLORTERM") {
192 let ct = ct.to_lowercase();
193 if ct == "truecolor" || ct == "24bit" {
194 return Self::TrueColor;
195 }
196 }
197 if let Ok(term) = std::env::var("TERM") {
198 if term.contains("256color") {
199 return Self::EightBit;
200 }
201 }
202 Self::Basic
203 }
204}
205
206fn rgb_to_ansi256(r: u8, g: u8, b: u8) -> u8 {
207 if r == g && g == b {
208 if r < 8 {
209 return 16;
210 }
211 if r > 248 {
212 return 231;
213 }
214 return 232 + (((r as u16 - 8) * 24 / 240) as u8);
215 }
216
217 let ri = if r < 48 {
218 0
219 } else {
220 ((r as u16 - 35) / 40) as u8
221 };
222 let gi = if g < 48 {
223 0
224 } else {
225 ((g as u16 - 35) / 40) as u8
226 };
227 let bi = if b < 48 {
228 0
229 } else {
230 ((b as u16 - 35) / 40) as u8
231 };
232 16 + 36 * ri.min(5) + 6 * gi.min(5) + bi.min(5)
233}
234
235fn rgb_to_ansi16(r: u8, g: u8, b: u8) -> Color {
236 let lum =
237 0.2126 * (r as f32 / 255.0) + 0.7152 * (g as f32 / 255.0) + 0.0722 * (b as f32 / 255.0);
238
239 let max = r.max(g).max(b);
240 let min = r.min(g).min(b);
241 let saturation = if max == 0 {
242 0.0
243 } else {
244 (max - min) as f32 / max as f32
245 };
246
247 if saturation < 0.2 {
248 return if lum < 0.15 {
249 Color::Black
250 } else {
251 Color::White
252 };
253 }
254
255 let rf = r as f32;
256 let gf = g as f32;
257 let bf = b as f32;
258
259 if rf >= gf && rf >= bf {
260 if gf > bf * 1.5 {
261 Color::Yellow
262 } else if bf > gf * 1.5 {
263 Color::Magenta
264 } else {
265 Color::Red
266 }
267 } else if gf >= rf && gf >= bf {
268 if bf > rf * 1.5 {
269 Color::Cyan
270 } else {
271 Color::Green
272 }
273 } else if rf > gf * 1.5 {
274 Color::Magenta
275 } else if gf > rf * 1.5 {
276 Color::Cyan
277 } else {
278 Color::Blue
279 }
280}
281
282fn xterm256_to_rgb(idx: u8) -> (u8, u8, u8) {
283 match idx {
284 0 => (0, 0, 0),
285 1 => (128, 0, 0),
286 2 => (0, 128, 0),
287 3 => (128, 128, 0),
288 4 => (0, 0, 128),
289 5 => (128, 0, 128),
290 6 => (0, 128, 128),
291 7 => (192, 192, 192),
292 8 => (128, 128, 128),
293 9 => (255, 0, 0),
294 10 => (0, 255, 0),
295 11 => (255, 255, 0),
296 12 => (0, 0, 255),
297 13 => (255, 0, 255),
298 14 => (0, 255, 255),
299 15 => (255, 255, 255),
300 16..=231 => {
301 let n = idx - 16;
302 let b_idx = n % 6;
303 let g_idx = (n / 6) % 6;
304 let r_idx = n / 36;
305 let to_val = |i: u8| if i == 0 { 0u8 } else { 55 + 40 * i };
306 (to_val(r_idx), to_val(g_idx), to_val(b_idx))
307 }
308 232..=255 => {
309 let v = 8 + 10 * (idx - 232);
310 (v, v, v)
311 }
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 #[test]
320 fn blend_halfway_rounds_to_128() {
321 assert_eq!(
322 Color::Rgb(255, 255, 255).blend(Color::Rgb(0, 0, 0), 0.5),
323 Color::Rgb(128, 128, 128)
324 );
325 }
326}