Skip to main content

winnow_args/
color.rs

1//! Colors for help and errors, for the terminal at hand.
2//!
3//! Terminals show 16, 256 or 16 million colors. [`Depth::detect`] reads the
4//! environment, and a [`Theme`] offers a [`Palette`] for each depth: the richest one
5//! the terminal can show is used, and a color deeper than the terminal is
6//! mapped to the nearest one it has. The default theme uses only the 16 basic
7//! colors, which the terminal's own theme defines — so they suit light and
8//! dark backgrounds alike.
9
10use std::fmt::Write as _;
11
12/// How many colors a terminal shows. Ordered: a deeper terminal shows
13/// everything a shallower one does.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
15pub enum Depth {
16    /// No color and no attributes: a pipe, a file, `NO_COLOR`.
17    None,
18    /// The 16 basic colors, bold, underline: every color terminal.
19    Ansi16,
20    /// The xterm 256-color palette: `TERM=*-256color`.
21    Ansi256,
22    /// 24-bit color: `COLORTERM=truecolor`.
23    TrueColor,
24}
25
26impl Depth {
27    /// The depth for a stream that is (or is not) a terminal, from the
28    /// environment:
29    ///
30    /// - a non-empty `NO_COLOR` means none, and it wins over `FORCE_COLOR`;
31    /// - `FORCE_COLOR=0` or `false` means none too;
32    /// - `FORCE_COLOR` (`1`–`3`, or empty/`true` for 1) is a minimum depth,
33    ///   not the depth: a terminal that can show more still does.
34    ///   `CLICOLOR_FORCE` (not `0`) gives color even to a pipe;
35    /// - otherwise none for a pipe, `CLICOLOR=0`, `TERM=dumb`, or no `TERM`
36    ///   at all;
37    /// - then 24-bit for `COLORTERM=truecolor`/`24bit`, a `TERM` ending in
38    ///   `direct` or `truecolor`, or iTerm2; 256 for a `TERM` ending in
39    ///   `256`/`256color` or Apple's Terminal; else 16.
40    ///
41    /// Under [`with_env`](crate::with_env) the stream never counts as a
42    /// terminal, so the answer depends only on the variables given.
43    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            // A refusal, not "unset": nothing below turns color back on.
52            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/// The 16 basic colors. What they look like is the terminal theme's choice.
94#[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    /// Its index, 0–15, as in the 256-color palette.
136    pub const fn index(self) -> u8 {
137        self as u8
138    }
139
140    /// The SGR foreground code: 30–37, 90–97.
141    const fn code(self) -> u8 {
142        let i = self.index();
143        if i < 8 { 30 + i } else { 90 + i - 8 }
144    }
145
146    /// xterm's default RGB for it, to measure other colors against.
147    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/// A foreground color at any depth.
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
171pub enum Color {
172    /// One of the 16 basic colors.
173    Ansi(Ansi),
174    /// An entry of the xterm 256-color palette.
175    Ansi256(u8),
176    /// 24-bit color.
177    Rgb(u8, u8, u8),
178}
179
180/// The six levels of each channel in the 256-color cube (entries 16–231).
181const CUBE: [u8; 6] = [0, 95, 135, 175, 215, 255];
182
183impl Color {
184    /// The depth it needs.
185    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    /// Its approximate RGB (xterm's defaults for the palette colors).
194    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    /// The nearest color a terminal of `depth` can show; `None` for
212    /// [`Depth::None`].
213    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    /// Append the SGR parameters that select it as the foreground.
223    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
232/// Squared distance between two colors, weighted for how the eye sees them.
233fn 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
238/// The nearest basic color: by hue when the color has one, since a muted
239/// tint is nearer to gray by distance and a palette of tints would all come
240/// out as the same gray; a gray by distance.
241fn 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    // The hue in degrees, then the nearest of the six at 0, 60, … 300.
252    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
274/// The nearest entry of the cube or the gray ramp (16–255): the basic colors
275/// are left out, since their look is the terminal theme's.
276fn 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/// How to paint one kind of text: a color and attributes.
296#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
297pub struct Paint {
298    /// The foreground color, if any.
299    pub fg: Option<Color>,
300    /// Bold (or bright, on some terminals).
301    pub bold: bool,
302    /// Dim.
303    pub dim: bool,
304    /// Italic.
305    pub italic: bool,
306    /// Underlined.
307    pub underline: bool,
308}
309
310impl Paint {
311    /// Nothing: the text as the terminal shows it.
312    pub const NONE: Paint = Paint {
313        fg: None,
314        bold: false,
315        dim: false,
316        italic: false,
317        underline: false,
318    };
319
320    /// In `color`.
321    pub const fn fg(color: Color) -> Paint {
322        Paint {
323            fg: Some(color),
324            ..Paint::NONE
325        }
326    }
327
328    /// In one of the basic colors.
329    pub const fn ansi(color: Ansi) -> Paint {
330        Paint::fg(Color::Ansi(color))
331    }
332
333    /// And bold.
334    pub const fn bold(self) -> Paint {
335        Paint { bold: true, ..self }
336    }
337
338    /// And dim.
339    pub const fn dim(self) -> Paint {
340        Paint { dim: true, ..self }
341    }
342
343    /// And italic.
344    pub const fn italic(self) -> Paint {
345        Paint {
346            italic: true,
347            ..self
348        }
349    }
350
351    /// And underlined.
352    pub const fn underline(self) -> Paint {
353        Paint {
354            underline: true,
355            ..self
356        }
357    }
358
359    /// The deepest color it uses.
360    pub const fn depth(self) -> Depth {
361        match self.fg {
362            Some(color) => color.depth(),
363            None => Depth::Ansi16,
364        }
365    }
366
367    /// Append `text` painted for a terminal of `depth`: one SGR sequence,
368    /// the text, a reset. Plain when `depth` is [`Depth::None`] or there is
369    /// nothing to paint.
370    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/// A paint for each kind of text in help and errors.
405#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
406pub struct Palette {
407    /// `Usage:`, `Options:` and the other headings.
408    pub header: Paint,
409    /// The program's name in the usage line.
410    pub program: Paint,
411    /// Flags as typed: `-v`, `--verbose`, `--no-cache`, and `--` in the usage line.
412    pub flag: Paint,
413    /// Subcommand names and their aliases.
414    pub command: Paint,
415    /// Value names: all of `<PATH>`, the name in `[FILE]`.
416    pub placeholder: Paint,
417    /// `error:`.
418    pub error: Paint,
419    /// The part of the command line an error is about.
420    pub invalid: Paint,
421    /// What an error says is missing: something to type.
422    pub valid: Paint,
423    /// The environment variable in `[env: …]`.
424    pub env: Paint,
425    /// The value in `[default: …]`.
426    pub default: Paint,
427    /// Each value in `[possible values: …]`.
428    pub choice: Paint,
429    /// What frames the rest: the labels of `[env: …]`, `[default: …]` and
430    /// `[possible values: …]`, the brackets of an optional `[NAME]`, and `...`.
431    pub dim: Paint,
432    /// A `` `quoted` `` span in a description.
433    pub code: Paint,
434    /// An `*emphasized*` span in a description.
435    pub emphasis: Paint,
436    /// A `**strong**` span in a description.
437    pub strong: Paint,
438}
439
440impl Palette {
441    /// Nothing painted.
442    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    /// The default in the 16 basic colors: greens for what is typed, cyans
461    /// for structure and values, no yellow. Bold cyan headings, bold green
462    /// flags and subcommands, cyan value names, the program plain; cyan
463    /// environment variables, green defaults, bright green possible values,
464    /// dim labels and brackets, bold `` `code` ``, italic `*emphasis*`, bold
465    /// `**strong**`;
466    /// bold red `error:`, red for what was typed wrong, green for what is
467    /// missing.
468    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    /// [`Palette::DEFAULT`] in tamer tints of the 256-color palette, readable
487    /// on dark and light backgrounds: bold teal 73 (`#5fafaf`) headings, bold
488    /// green 71 (`#5faf5f`) flags and subcommands, slate teal 109 (`#87afaf`)
489    /// value names; teal 37 (`#00afaf`) environment variables, sea green 72
490    /// (`#5faf87`) defaults, green 71 possible values, gray 245 labels and
491    /// brackets, bold `` `code` ``, italic `*emphasis*`, bold `**strong**`;
492    /// bold red 167
493    /// (`#d75f5f`) `error:`, rose 174 (`#d78787`) for what was typed wrong,
494    /// sage 108 (`#87af87`) for what is missing.
495    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    /// No color in help: bold underlined headings and bold program, flags and
514    /// commands, everything else plain. Color only in errors: bold red
515    /// `error:`, yellow for what was typed wrong, green for what is missing.
516    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    /// The deepest color any role uses.
535    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/// Palettes for each depth: the richest one a terminal can show is used.
567#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
568pub struct Theme {
569    /// For every color terminal; the fallback for the others.
570    pub ansi16: Palette,
571    /// For 256-color terminals, if different.
572    pub ansi256: Option<Palette>,
573    /// For 24-bit terminals, if different.
574    pub truecolor: Option<Palette>,
575}
576
577impl Theme {
578    /// [`Palette::DEFAULT`] on 16-color terminals, [`Palette::DEFAULT_256`] on
579    /// 256-color and 24-bit ones.
580    pub const DEFAULT: Theme = Theme {
581        ansi16: Palette::DEFAULT,
582        ansi256: Some(Palette::DEFAULT_256),
583        truecolor: None,
584    };
585
586    /// One palette at every depth (its colors mapped down where deeper than
587    /// the terminal).
588    pub const fn uniform(palette: Palette) -> Theme {
589        Theme {
590            ansi16: palette,
591            ansi256: None,
592            truecolor: None,
593        }
594    }
595
596    /// The palette for a terminal of `depth`.
597    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        // Pure hues and grays land on their exact entries.
619        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        // A basic color is shown as itself everywhere.
632        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        // In 16 colors each role keeps its hue: none collapses to gray.
662        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}