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 DarkGray,
29 LightRed,
31 LightGreen,
33 LightYellow,
35 LightBlue,
37 LightMagenta,
39 LightCyan,
41 LightWhite,
43 Rgb(u8, u8, u8),
45 Indexed(u8),
47}
48
49impl Color {
50 fn to_rgb(self) -> (u8, u8, u8) {
55 match self {
56 Color::Rgb(r, g, b) => (r, g, b),
57 Color::Black => (0, 0, 0),
58 Color::Red => (205, 49, 49),
59 Color::Green => (13, 188, 121),
60 Color::Yellow => (229, 229, 16),
61 Color::Blue => (36, 114, 200),
62 Color::Magenta => (188, 63, 188),
63 Color::Cyan => (17, 168, 205),
64 Color::White => (229, 229, 229),
65 Color::DarkGray => (128, 128, 128),
66 Color::LightRed => (255, 0, 0),
67 Color::LightGreen => (0, 255, 0),
68 Color::LightYellow => (255, 255, 0),
69 Color::LightBlue => (0, 0, 255),
70 Color::LightMagenta => (255, 0, 255),
71 Color::LightCyan => (0, 255, 255),
72 Color::LightWhite => (255, 255, 255),
73 Color::Reset => (0, 0, 0),
74 Color::Indexed(idx) => xterm256_to_rgb(idx),
75 }
76 }
77
78 pub fn luminance(self) -> f32 {
96 let (r, g, b) = self.to_rgb();
97 let rf = r as f32 / 255.0;
98 let gf = g as f32 / 255.0;
99 let bf = b as f32 / 255.0;
100 0.2126 * rf + 0.7152 * gf + 0.0722 * bf
101 }
102
103 pub fn contrast_fg(bg: Color) -> Color {
119 if bg.luminance() > 0.5 {
120 Color::Rgb(0, 0, 0)
121 } else {
122 Color::Rgb(255, 255, 255)
123 }
124 }
125
126 pub fn blend(self, other: Color, alpha: f32) -> Color {
142 let alpha = alpha.clamp(0.0, 1.0);
143 let (r1, g1, b1) = self.to_rgb();
144 let (r2, g2, b2) = other.to_rgb();
145 let r = (r1 as f32 * alpha + r2 as f32 * (1.0 - alpha)).round() as u8;
146 let g = (g1 as f32 * alpha + g2 as f32 * (1.0 - alpha)).round() as u8;
147 let b = (b1 as f32 * alpha + b2 as f32 * (1.0 - alpha)).round() as u8;
148 Color::Rgb(r, g, b)
149 }
150
151 pub fn lighten(self, amount: f32) -> Color {
156 Color::Rgb(255, 255, 255).blend(self, 1.0 - amount.clamp(0.0, 1.0))
157 }
158
159 pub fn darken(self, amount: f32) -> Color {
164 Color::Rgb(0, 0, 0).blend(self, 1.0 - amount.clamp(0.0, 1.0))
165 }
166
167 pub fn downsampled(self, depth: ColorDepth) -> Color {
175 match depth {
176 ColorDepth::TrueColor => self,
177 ColorDepth::EightBit => match self {
178 Color::Rgb(r, g, b) => Color::Indexed(rgb_to_ansi256(r, g, b)),
179 other => other,
180 },
181 ColorDepth::Basic => match self {
182 Color::Rgb(r, g, b) => rgb_to_ansi16(r, g, b),
183 Color::Indexed(i) => {
184 let (r, g, b) = xterm256_to_rgb(i);
185 rgb_to_ansi16(r, g, b)
186 }
187 other => other,
188 },
189 }
190 }
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
199#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
200pub enum ColorDepth {
201 TrueColor,
203 EightBit,
205 Basic,
207}
208
209impl ColorDepth {
210 pub fn detect() -> Self {
215 if let Ok(ct) = std::env::var("COLORTERM") {
216 let ct = ct.to_lowercase();
217 if ct == "truecolor" || ct == "24bit" {
218 return Self::TrueColor;
219 }
220 }
221 if let Ok(term) = std::env::var("TERM") {
222 if term.contains("256color") {
223 return Self::EightBit;
224 }
225 }
226 Self::Basic
227 }
228}
229
230fn rgb_to_ansi256(r: u8, g: u8, b: u8) -> u8 {
231 if r == g && g == b {
232 if r < 8 {
233 return 16;
234 }
235 if r > 248 {
236 return 231;
237 }
238 return 232 + (((r as u16 - 8) * 24 / 240) as u8);
239 }
240
241 let ri = if r < 48 {
242 0
243 } else {
244 ((r as u16 - 35) / 40) as u8
245 };
246 let gi = if g < 48 {
247 0
248 } else {
249 ((g as u16 - 35) / 40) as u8
250 };
251 let bi = if b < 48 {
252 0
253 } else {
254 ((b as u16 - 35) / 40) as u8
255 };
256 16 + 36 * ri.min(5) + 6 * gi.min(5) + bi.min(5)
257}
258
259fn rgb_to_ansi16(r: u8, g: u8, b: u8) -> Color {
260 let lum =
261 0.2126 * (r as f32 / 255.0) + 0.7152 * (g as f32 / 255.0) + 0.0722 * (b as f32 / 255.0);
262
263 let max = r.max(g).max(b);
264 let min = r.min(g).min(b);
265 let saturation = if max == 0 {
266 0.0
267 } else {
268 (max - min) as f32 / max as f32
269 };
270
271 if saturation < 0.2 {
272 return if lum < 0.15 {
273 Color::Black
274 } else {
275 Color::White
276 };
277 }
278
279 let rf = r as f32;
280 let gf = g as f32;
281 let bf = b as f32;
282
283 if rf >= gf && rf >= bf {
284 if gf > bf * 1.5 {
285 Color::Yellow
286 } else if bf > gf * 1.5 {
287 Color::Magenta
288 } else {
289 Color::Red
290 }
291 } else if gf >= rf && gf >= bf {
292 if bf > rf * 1.5 {
293 Color::Cyan
294 } else {
295 Color::Green
296 }
297 } else if rf > gf * 1.5 {
298 Color::Magenta
299 } else if gf > rf * 1.5 {
300 Color::Cyan
301 } else {
302 Color::Blue
303 }
304}
305
306fn xterm256_to_rgb(idx: u8) -> (u8, u8, u8) {
307 match idx {
308 0 => (0, 0, 0),
309 1 => (128, 0, 0),
310 2 => (0, 128, 0),
311 3 => (128, 128, 0),
312 4 => (0, 0, 128),
313 5 => (128, 0, 128),
314 6 => (0, 128, 128),
315 7 => (192, 192, 192),
316 8 => (128, 128, 128),
317 9 => (255, 0, 0),
318 10 => (0, 255, 0),
319 11 => (255, 255, 0),
320 12 => (0, 0, 255),
321 13 => (255, 0, 255),
322 14 => (0, 255, 255),
323 15 => (255, 255, 255),
324 16..=231 => {
325 let n = idx - 16;
326 let b_idx = n % 6;
327 let g_idx = (n / 6) % 6;
328 let r_idx = n / 36;
329 let to_val = |i: u8| if i == 0 { 0u8 } else { 55 + 40 * i };
330 (to_val(r_idx), to_val(g_idx), to_val(b_idx))
331 }
332 232..=255 => {
333 let v = 8 + 10 * (idx - 232);
334 (v, v, v)
335 }
336 }
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342
343 #[test]
344 fn blend_halfway_rounds_to_128() {
345 assert_eq!(
346 Color::Rgb(255, 255, 255).blend(Color::Rgb(0, 0, 0), 0.5),
347 Color::Rgb(128, 128, 128)
348 );
349 }
350}