1use std::fmt::Write as _;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
15pub enum Depth {
16 None,
18 Ansi16,
20 Ansi256,
22 TrueColor,
24}
25
26impl Depth {
27 pub fn detect(is_terminal: bool) -> Depth {
44 let var = |name| crate::env::var(name);
45 let text = |name| var(name).and_then(|v| v.into_string().ok());
46 if var("NO_COLOR").is_some_and(|v| !v.is_empty()) {
47 return Depth::None;
48 }
49 let forced = match text("FORCE_COLOR").as_deref() {
50 Some("" | "true") => Depth::Ansi16,
51 Some("false" | "0") => return Depth::None,
53 Some(level) => match level.parse::<u8>() {
54 Ok(0) => return Depth::None,
55 Ok(1) | Err(_) => Depth::Ansi16,
56 Ok(2) => Depth::Ansi256,
57 Ok(_) => Depth::TrueColor,
58 },
59 None if var("CLICOLOR_FORCE").is_some_and(|v| !v.is_empty() && v != "0") => {
60 Depth::Ansi16
61 }
62 None => Depth::None,
63 };
64 let term = text("TERM");
65 if forced == Depth::None {
66 let terminal = is_terminal && !crate::env::overridden();
67 let refused = var("CLICOLOR").is_some_and(|v| v == "0");
68 let dumb = term.as_deref().is_none_or(|term| term == "dumb");
69 if !terminal || refused || dumb {
70 return Depth::None;
71 }
72 }
73 let term = term.as_deref().unwrap_or_default();
74 let program = text("TERM_PROGRAM");
75 let detected = if matches!(text("COLORTERM").as_deref(), Some("truecolor" | "24bit"))
76 || term.ends_with("direct")
77 || term.ends_with("truecolor")
78 || program.as_deref() == Some("iTerm.app")
79 {
80 Depth::TrueColor
81 } else if term.ends_with("256")
82 || term.ends_with("256color")
83 || program.as_deref() == Some("Apple_Terminal")
84 {
85 Depth::Ansi256
86 } else {
87 Depth::Ansi16
88 };
89 detected.max(forced)
90 }
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
95#[allow(missing_docs, reason = "the names are the colors")]
96pub enum Ansi {
97 Black,
98 Red,
99 Green,
100 Yellow,
101 Blue,
102 Magenta,
103 Cyan,
104 White,
105 BrightBlack,
106 BrightRed,
107 BrightGreen,
108 BrightYellow,
109 BrightBlue,
110 BrightMagenta,
111 BrightCyan,
112 BrightWhite,
113}
114
115impl Ansi {
116 const ALL: [Ansi; 16] = [
117 Ansi::Black,
118 Ansi::Red,
119 Ansi::Green,
120 Ansi::Yellow,
121 Ansi::Blue,
122 Ansi::Magenta,
123 Ansi::Cyan,
124 Ansi::White,
125 Ansi::BrightBlack,
126 Ansi::BrightRed,
127 Ansi::BrightGreen,
128 Ansi::BrightYellow,
129 Ansi::BrightBlue,
130 Ansi::BrightMagenta,
131 Ansi::BrightCyan,
132 Ansi::BrightWhite,
133 ];
134
135 pub const fn index(self) -> u8 {
137 self as u8
138 }
139
140 const fn code(self) -> u8 {
142 let i = self.index();
143 if i < 8 { 30 + i } else { 90 + i - 8 }
144 }
145
146 const fn rgb(self) -> (u8, u8, u8) {
148 match self {
149 Ansi::Black => (0, 0, 0),
150 Ansi::Red => (205, 0, 0),
151 Ansi::Green => (0, 205, 0),
152 Ansi::Yellow => (205, 205, 0),
153 Ansi::Blue => (0, 0, 238),
154 Ansi::Magenta => (205, 0, 205),
155 Ansi::Cyan => (0, 205, 205),
156 Ansi::White => (229, 229, 229),
157 Ansi::BrightBlack => (127, 127, 127),
158 Ansi::BrightRed => (255, 0, 0),
159 Ansi::BrightGreen => (0, 255, 0),
160 Ansi::BrightYellow => (255, 255, 0),
161 Ansi::BrightBlue => (92, 92, 255),
162 Ansi::BrightMagenta => (255, 0, 255),
163 Ansi::BrightCyan => (0, 255, 255),
164 Ansi::BrightWhite => (255, 255, 255),
165 }
166 }
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
171pub enum Color {
172 Ansi(Ansi),
174 Ansi256(u8),
176 Rgb(u8, u8, u8),
178}
179
180const CUBE: [u8; 6] = [0, 95, 135, 175, 215, 255];
182
183impl Color {
184 pub const fn depth(self) -> Depth {
186 match self {
187 Color::Ansi(_) => Depth::Ansi16,
188 Color::Ansi256(_) => Depth::Ansi256,
189 Color::Rgb(..) => Depth::TrueColor,
190 }
191 }
192
193 pub fn rgb(self) -> (u8, u8, u8) {
195 match self {
196 Color::Ansi(a) => a.rgb(),
197 Color::Ansi256(n @ 0..=15) => Ansi::ALL[usize::from(n)].rgb(),
198 Color::Ansi256(n @ 16..=231) => {
199 let n = n - 16;
200 let level = |i: u8| CUBE[usize::from(i)];
201 (level(n / 36), level(n / 6 % 6), level(n % 6))
202 }
203 Color::Ansi256(n) => {
204 let gray = 8 + (n - 232) * 10;
205 (gray, gray, gray)
206 }
207 Color::Rgb(r, g, b) => (r, g, b),
208 }
209 }
210
211 pub fn at(self, depth: Depth) -> Option<Color> {
214 match depth {
215 Depth::None => None,
216 _ if self.depth() <= depth => Some(self),
217 Depth::Ansi256 => Some(Color::Ansi256(to_256(self.rgb()))),
218 Depth::Ansi16 | Depth::TrueColor => Some(Color::Ansi(to_16(self.rgb()))),
219 }
220 }
221
222 fn write_sgr(self, out: &mut String) {
224 let _ = match self {
225 Color::Ansi(a) => write!(out, "{}", a.code()),
226 Color::Ansi256(n) => write!(out, "38;5;{n}"),
227 Color::Rgb(r, g, b) => write!(out, "38;2;{r};{g};{b}"),
228 };
229 }
230}
231
232fn distance((r1, g1, b1): (u8, u8, u8), (r2, g2, b2): (u8, u8, u8)) -> u32 {
234 let d = |a: u8, b: u8| u32::from(a.abs_diff(b)).pow(2);
235 2 * d(r1, r2) + 4 * d(g1, g2) + 3 * d(b1, b2)
236}
237
238fn to_16(rgb: (u8, u8, u8)) -> Ansi {
242 let (r, g, b) = (i32::from(rgb.0), i32::from(rgb.1), i32::from(rgb.2));
243 let (max, min) = (r.max(g).max(b), r.min(g).min(b));
244 let chroma = max - min;
245 if chroma < 32 {
246 return Ansi::ALL
247 .into_iter()
248 .min_by_key(|a| distance(a.rgb(), rgb))
249 .unwrap_or(Ansi::White);
250 }
251 let hue = if max == r {
253 (60 * (g - b) / chroma + 360) % 360
254 } else if max == g {
255 120 + 60 * (b - r) / chroma
256 } else {
257 240 + 60 * (r - g) / chroma
258 };
259 let (normal, bright) = match (hue + 30) / 60 % 6 {
260 0 => (Ansi::Red, Ansi::BrightRed),
261 1 => (Ansi::Yellow, Ansi::BrightYellow),
262 2 => (Ansi::Green, Ansi::BrightGreen),
263 3 => (Ansi::Cyan, Ansi::BrightCyan),
264 4 => (Ansi::Blue, Ansi::BrightBlue),
265 _ => (Ansi::Magenta, Ansi::BrightMagenta),
266 };
267 if distance(bright.rgb(), rgb) < distance(normal.rgb(), rgb) {
268 bright
269 } else {
270 normal
271 }
272}
273
274fn to_256(rgb: (u8, u8, u8)) -> u8 {
277 let level = |c: u8| {
278 CUBE.iter()
279 .enumerate()
280 .min_by_key(|(_, l)| l.abs_diff(c))
281 .map_or(0, |(i, _)| i as u8)
282 };
283 let (r, g, b) = (level(rgb.0), level(rgb.1), level(rgb.2));
284 let cube = 16 + 36 * r + 6 * g + b;
285 let average = (u16::from(rgb.0) + u16::from(rgb.1) + u16::from(rgb.2)) / 3;
286 let step = (average.saturating_sub(3) / 10).min(23) as u8;
287 let gray = 232 + step;
288 if distance(Color::Ansi256(gray).rgb(), rgb) < distance(Color::Ansi256(cube).rgb(), rgb) {
289 gray
290 } else {
291 cube
292 }
293}
294
295#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
297pub struct Paint {
298 pub fg: Option<Color>,
300 pub bold: bool,
302 pub dim: bool,
304 pub italic: bool,
306 pub underline: bool,
308}
309
310impl Paint {
311 pub const NONE: Paint = Paint {
313 fg: None,
314 bold: false,
315 dim: false,
316 italic: false,
317 underline: false,
318 };
319
320 pub const fn fg(color: Color) -> Paint {
322 Paint {
323 fg: Some(color),
324 ..Paint::NONE
325 }
326 }
327
328 pub const fn ansi(color: Ansi) -> Paint {
330 Paint::fg(Color::Ansi(color))
331 }
332
333 pub const fn bold(self) -> Paint {
335 Paint { bold: true, ..self }
336 }
337
338 pub const fn dim(self) -> Paint {
340 Paint { dim: true, ..self }
341 }
342
343 pub const fn italic(self) -> Paint {
345 Paint {
346 italic: true,
347 ..self
348 }
349 }
350
351 pub const fn underline(self) -> Paint {
353 Paint {
354 underline: true,
355 ..self
356 }
357 }
358
359 pub const fn depth(self) -> Depth {
361 match self.fg {
362 Some(color) => color.depth(),
363 None => Depth::Ansi16,
364 }
365 }
366
367 pub fn write(self, out: &mut String, depth: Depth, text: &str) {
371 if depth == Depth::None || self == Paint::NONE {
372 out.push_str(text);
373 return;
374 }
375 out.push_str("\u{1b}[");
376 let mut first = true;
377 let mut sep = |out: &mut String| {
378 if !first {
379 out.push(';');
380 }
381 first = false;
382 };
383 for (on, code) in [
384 (self.bold, '1'),
385 (self.dim, '2'),
386 (self.italic, '3'),
387 (self.underline, '4'),
388 ] {
389 if on {
390 sep(out);
391 out.push(code);
392 }
393 }
394 if let Some(color) = self.fg.and_then(|c| c.at(depth)) {
395 sep(out);
396 color.write_sgr(out);
397 }
398 out.push('m');
399 out.push_str(text);
400 out.push_str("\u{1b}[0m");
401 }
402}
403
404#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
406pub struct Palette {
407 pub header: Paint,
409 pub program: Paint,
411 pub flag: Paint,
413 pub command: Paint,
415 pub placeholder: Paint,
417 pub error: Paint,
419 pub invalid: Paint,
421 pub valid: Paint,
423 pub env: Paint,
425 pub default: Paint,
427 pub choice: Paint,
429 pub dim: Paint,
432 pub code: Paint,
434 pub emphasis: Paint,
436 pub strong: Paint,
438}
439
440impl Palette {
441 pub const PLAIN: Palette = Palette {
443 header: Paint::NONE,
444 program: Paint::NONE,
445 flag: Paint::NONE,
446 command: Paint::NONE,
447 placeholder: Paint::NONE,
448 error: Paint::NONE,
449 invalid: Paint::NONE,
450 valid: Paint::NONE,
451 env: Paint::NONE,
452 default: Paint::NONE,
453 choice: Paint::NONE,
454 dim: Paint::NONE,
455 code: Paint::NONE,
456 emphasis: Paint::NONE,
457 strong: Paint::NONE,
458 };
459
460 pub const DEFAULT: Palette = Palette {
469 header: Paint::ansi(Ansi::Cyan).bold(),
470 program: Paint::NONE,
471 flag: Paint::ansi(Ansi::Green).bold(),
472 command: Paint::ansi(Ansi::Green).bold(),
473 placeholder: Paint::ansi(Ansi::Cyan),
474 error: Paint::ansi(Ansi::Red).bold(),
475 invalid: Paint::ansi(Ansi::Red),
476 valid: Paint::ansi(Ansi::Green),
477 env: Paint::ansi(Ansi::Cyan),
478 default: Paint::ansi(Ansi::Green),
479 choice: Paint::ansi(Ansi::BrightGreen),
480 dim: Paint::NONE.dim(),
481 code: Paint::NONE.bold(),
482 emphasis: Paint::NONE.italic(),
483 strong: Paint::NONE.bold(),
484 };
485
486 pub const DEFAULT_256: Palette = Palette {
496 header: Paint::fg(Color::Ansi256(73)).bold(),
497 program: Paint::NONE,
498 flag: Paint::fg(Color::Ansi256(71)).bold(),
499 command: Paint::fg(Color::Ansi256(71)).bold(),
500 placeholder: Paint::fg(Color::Ansi256(109)),
501 error: Paint::fg(Color::Ansi256(167)).bold(),
502 invalid: Paint::fg(Color::Ansi256(174)),
503 valid: Paint::fg(Color::Ansi256(108)),
504 env: Paint::fg(Color::Ansi256(37)),
505 default: Paint::fg(Color::Ansi256(72)),
506 choice: Paint::fg(Color::Ansi256(71)),
507 dim: Paint::fg(Color::Ansi256(245)),
508 code: Paint::NONE.bold(),
509 emphasis: Paint::NONE.italic(),
510 strong: Paint::NONE.bold(),
511 };
512
513 pub const CLAP: Palette = Palette {
517 header: Paint::NONE.bold().underline(),
518 program: Paint::NONE.bold(),
519 flag: Paint::NONE.bold(),
520 command: Paint::NONE.bold(),
521 placeholder: Paint::NONE,
522 error: Paint::ansi(Ansi::Red).bold(),
523 invalid: Paint::ansi(Ansi::Yellow),
524 valid: Paint::ansi(Ansi::Green),
525 env: Paint::NONE,
526 default: Paint::NONE,
527 choice: Paint::NONE,
528 dim: Paint::NONE,
529 code: Paint::NONE,
530 emphasis: Paint::NONE,
531 strong: Paint::NONE,
532 };
533
534 pub const fn depth(&self) -> Depth {
536 let roles = [
537 self.header,
538 self.program,
539 self.flag,
540 self.command,
541 self.placeholder,
542 self.error,
543 self.invalid,
544 self.valid,
545 self.env,
546 self.default,
547 self.choice,
548 self.dim,
549 self.code,
550 self.emphasis,
551 self.strong,
552 ];
553 let mut deepest = Depth::Ansi16;
554 let mut i = 0;
555 while i < roles.len() {
556 let depth = roles[i].depth();
557 if depth as u8 > deepest as u8 {
558 deepest = depth;
559 }
560 i += 1;
561 }
562 deepest
563 }
564}
565
566#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
568pub struct Theme {
569 pub ansi16: Palette,
571 pub ansi256: Option<Palette>,
573 pub truecolor: Option<Palette>,
575}
576
577impl Theme {
578 pub const DEFAULT: Theme = Theme {
581 ansi16: Palette::DEFAULT,
582 ansi256: Some(Palette::DEFAULT_256),
583 truecolor: None,
584 };
585
586 pub const fn uniform(palette: Palette) -> Theme {
589 Theme {
590 ansi16: palette,
591 ansi256: None,
592 truecolor: None,
593 }
594 }
595
596 pub fn palette(&self, depth: Depth) -> Palette {
598 match depth {
599 Depth::None => Palette::PLAIN,
600 Depth::Ansi16 => self.ansi16,
601 Depth::Ansi256 => self.ansi256.unwrap_or(self.ansi16),
602 Depth::TrueColor => self.truecolor.or(self.ansi256).unwrap_or(self.ansi16),
603 }
604 }
605}
606
607#[cfg(test)]
608mod tests {
609 use super::*;
610
611 #[test]
612 fn colors_map_to_the_nearest_the_terminal_has() {
613 let orange = Color::Rgb(255, 135, 0);
614 assert_eq!(orange.at(Depth::TrueColor), Some(orange));
615 assert_eq!(orange.at(Depth::Ansi256), Some(Color::Ansi256(208)));
616 assert_eq!(orange.at(Depth::Ansi16), Some(Color::Ansi(Ansi::Yellow)));
617 assert_eq!(orange.at(Depth::None), None);
618 assert_eq!(
620 Color::Rgb(0, 255, 255).at(Depth::Ansi256),
621 Some(Color::Ansi256(51))
622 );
623 assert_eq!(
624 Color::Rgb(128, 128, 128).at(Depth::Ansi256),
625 Some(Color::Ansi256(244))
626 );
627 assert_eq!(
628 Color::Ansi256(196).at(Depth::Ansi16),
629 Some(Color::Ansi(Ansi::BrightRed))
630 );
631 let cyan = Color::Ansi(Ansi::Cyan);
633 assert_eq!(cyan.at(Depth::Ansi256), Some(cyan));
634 }
635
636 #[test]
637 fn a_paint_is_one_sequence() {
638 let mut out = String::new();
639 Paint::ansi(Ansi::Yellow)
640 .bold()
641 .write(&mut out, Depth::Ansi16, "Usage:");
642 assert_eq!(out, "\u{1b}[1;33mUsage:\u{1b}[0m");
643 out.clear();
644 Paint::fg(Color::Rgb(1, 2, 3))
645 .underline()
646 .write(&mut out, Depth::TrueColor, "x");
647 assert_eq!(out, "\u{1b}[4;38;2;1;2;3mx\u{1b}[0m");
648 out.clear();
649 Paint::ansi(Ansi::BrightCyan).write(&mut out, Depth::Ansi16, "x");
650 assert_eq!(out, "\u{1b}[96mx\u{1b}[0m");
651 out.clear();
652 Paint::ansi(Ansi::Red).write(&mut out, Depth::None, "x");
653 assert_eq!(out, "x");
654 }
655
656 #[test]
657 fn a_uniform_theme_maps_its_palette_down() {
658 let theme = Theme::uniform(Palette::DEFAULT_256);
659 assert_eq!(theme.palette(Depth::TrueColor), Palette::DEFAULT_256);
660 assert_eq!(theme.palette(Depth::Ansi256), Palette::DEFAULT_256);
661 let shown = |paint: Paint| paint.fg.and_then(|c| c.at(Depth::Ansi16));
663 let palette = theme.palette(Depth::Ansi16);
664 let ansi = |a| Some(Color::Ansi(a));
665 assert_eq!(shown(palette.header), ansi(Ansi::Cyan));
666 assert_eq!(shown(palette.flag), ansi(Ansi::Green));
667 assert_eq!(shown(palette.placeholder), ansi(Ansi::Cyan));
668 assert_eq!(shown(palette.error), ansi(Ansi::Red));
669 assert_eq!(shown(palette.invalid), ansi(Ansi::Red));
670 assert_eq!(shown(palette.valid), ansi(Ansi::Green));
671 }
672
673 #[test]
674 fn a_theme_uses_the_richest_palette_the_terminal_shows() {
675 let rich = Palette {
676 header: Paint::fg(Color::Rgb(255, 135, 0)),
677 ..Palette::DEFAULT
678 };
679 let theme = Theme {
680 truecolor: Some(rich),
681 ..Theme::DEFAULT
682 };
683 assert_eq!(theme.palette(Depth::TrueColor), rich);
684 assert_eq!(theme.palette(Depth::Ansi256), Palette::DEFAULT_256);
685 assert_eq!(
686 Theme::DEFAULT.palette(Depth::TrueColor),
687 Palette::DEFAULT_256
688 );
689 assert_eq!(Theme::DEFAULT.palette(Depth::Ansi16), Palette::DEFAULT);
690 assert_eq!(theme.palette(Depth::None), Palette::PLAIN);
691 assert_eq!(rich.depth(), Depth::TrueColor);
692 assert_eq!(Palette::DEFAULT.depth(), Depth::Ansi16);
693 assert_eq!(Palette::DEFAULT_256.depth(), Depth::Ansi256);
694 }
695}