Skip to main content

shell_color/
lib.rs

1//! Parsing for terminal color
2
3use std::fmt::Debug;
4
5#[derive(Clone, PartialEq, Eq)]
6pub struct SuggestionColor {
7    pub fg: Option<VTermColor>,
8    pub bg: Option<VTermColor>,
9}
10
11impl SuggestionColor {
12    pub fn fg(&self) -> Option<VTermColor> {
13        self.fg.clone()
14    }
15
16    pub fn bg(&self) -> Option<VTermColor> {
17        self.bg.clone()
18    }
19}
20
21impl Debug for SuggestionColor {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        f.debug_struct("SuggestionColor")
24            .field("fg", &self.fg())
25            .field("bg", &self.bg())
26            .finish()
27    }
28}
29
30#[derive(Clone, PartialEq, Eq, Debug)]
31pub enum VTermColor {
32    Rgb { red: u8, green: u8, blue: u8 },
33    Indexed { idx: u8 },
34}
35
36impl VTermColor {
37    const fn from_idx(idx: u8) -> Self {
38        VTermColor::Indexed { idx }
39    }
40
41    const fn from_rgb(red: u8, green: u8, blue: u8) -> Self {
42        VTermColor::Rgb { red, green, blue }
43    }
44}
45
46impl From<nu_ansi_term::Color> for VTermColor {
47    fn from(color: nu_ansi_term::Color) -> Self {
48        use nu_ansi_term::Color;
49        match color {
50            Color::Black => VTermColor::from_idx(0),
51            Color::Red => VTermColor::from_idx(1),
52            Color::Green => VTermColor::from_idx(2),
53            Color::Yellow => VTermColor::from_idx(3),
54            Color::Blue => VTermColor::from_idx(4),
55            Color::Purple => VTermColor::from_idx(5),
56            Color::Magenta => VTermColor::from_idx(5),
57            Color::Cyan => VTermColor::from_idx(6),
58            Color::White => VTermColor::from_idx(7),
59            Color::DarkGray => VTermColor::from_idx(8),
60            Color::LightRed => VTermColor::from_idx(9),
61            Color::LightGreen => VTermColor::from_idx(10),
62            Color::LightYellow => VTermColor::from_idx(11),
63            Color::LightBlue => VTermColor::from_idx(12),
64            Color::LightPurple => VTermColor::from_idx(13),
65            Color::LightMagenta => VTermColor::from_idx(13),
66            Color::LightCyan => VTermColor::from_idx(14),
67            Color::LightGray => VTermColor::from_idx(16),
68            Color::Fixed(i) => VTermColor::from_idx(i),
69            Color::Rgb(r, g, b) => VTermColor::from_rgb(r, g, b),
70            Color::Default => VTermColor::from_idx(7),
71        }
72    }
73}
74
75bitflags::bitflags! {
76    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
77    pub struct ColorSupport: u32 {
78        const TERM256 = 1 << 1;
79        const TERM24BIT = 1 << 2;
80    }
81}
82
83#[allow(clippy::if_same_then_else)]
84// Updates our idea of whether we support term256 and term24bit (see issue #10222).
85pub fn get_color_support() -> ColorSupport {
86    // Detect or infer term256 support. If fish_term256 is set, we respect it;
87    // otherwise infer it from the TERM variable or use terminfo.
88    let mut support_term256 = false;
89    let mut support_term24bit = false;
90
91    let term = std::env::var("TERM").ok();
92    let fish_term256 = std::env::var("fish_term256").ok();
93
94    if let Some(fish_term256) = fish_term256 {
95        support_term256 = bool_from_string(&fish_term256);
96    } else if term.is_some() && term.as_ref().unwrap().contains("256color") {
97        support_term256 = true;
98    } else if term.is_some() && term.as_ref().unwrap().contains("xterm") {
99        // Assume that all 'xterm's can handle 256, except for Terminal.app from Snow Leopard
100        let term_program = std::env::var("TERM_PROGRAM").ok();
101        if term_program.is_some() && term_program.unwrap() == "Apple_Terminal" {
102            let tpv = std::env::var("TERM_PROGRAM_VERSION").ok();
103            if tpv.is_some() && tpv.unwrap().parse::<i32>().unwrap_or(0) > 299 {
104                support_term256 = true;
105            }
106        } else {
107            support_term256 = true;
108        }
109    }
110
111    let ct = std::env::var("COLORTERM").ok();
112    let it = std::env::var("ITERM_SESSION_ID").ok();
113    let vte = std::env::var("VTE_VERSION").ok();
114    let fish_term24bit = std::env::var("fish_term24bit").ok();
115    // Handle $fish_term24bit
116    if let Some(fish_term24bit) = fish_term24bit {
117        support_term24bit = bool_from_string(&fish_term24bit);
118    } else if std::env::var("STY").is_ok() || (term.is_some() && term.as_ref().unwrap().starts_with("eterm")) {
119        // Screen and emacs' ansi-term swallow truecolor sequences,
120        // so we ignore them unless force-enabled.
121        support_term24bit = false;
122    } else if let Some(ct) = ct {
123        // If someone set $COLORTERM, that's the sort of color they want.
124        if ct == "truecolor" || ct == "24bit" {
125            support_term24bit = true;
126        }
127    } else if std::env::var("KONSOLE_VERSION").is_ok() || std::env::var("KONSOLE_PROFILE_NAME").is_ok() {
128        // All konsole versions that use $KONSOLE_VERSION are new enough to support this,
129        // so no check is necessary.
130        support_term24bit = true;
131    } else if it.is_some() {
132        // Supporting versions of iTerm include a colon here.
133        // We assume that if this is iTerm, it can't also be st, so having this check
134        // inside is okay.
135        if it.unwrap().contains(':') {
136            support_term24bit = true;
137        }
138    } else if term.as_ref().is_some() && term.unwrap().starts_with("st-") {
139        support_term24bit = true;
140    } else if vte.is_some() && vte.unwrap().parse::<i32>().unwrap_or(0) > 3600 {
141        support_term24bit = true;
142    }
143
144    let mut support = ColorSupport::empty();
145
146    if support_term256 {
147        support |= ColorSupport::TERM256;
148    }
149
150    if support_term24bit {
151        support |= ColorSupport::TERM24BIT;
152    }
153
154    support
155}
156
157pub fn parse_suggestion_color_zsh_autosuggest(suggestion_str: &str, color_support: ColorSupport) -> SuggestionColor {
158    let mut sc = SuggestionColor { fg: None, bg: None };
159
160    for mut color_name in suggestion_str.split(',') {
161        let is_fg = color_name.starts_with("fg=");
162        let is_bg = color_name.starts_with("bg=");
163        if is_fg || is_bg {
164            (_, color_name) = color_name.split_at(3);
165            // TODO: currently using fish's parsing logic for named colors.
166            // This can fail in two cases:
167            // 1. false positives from fish colors that aren't supported in zsh (e.g. brblack)
168            // 2. false negatives that aren't supported in fish (e.g. abbreviations like bl for black)
169            // note: this todo was in the old c code - maybe it isn't needed anymore?
170            let mut color = try_parse_named(color_name);
171            if color.is_none() && color_name.starts_with('#') {
172                color = try_parse_rgb(color_name);
173            }
174            if color.is_none() {
175                // custom zsh logic - try 256 indexed colors first.
176                let index = color_name.parse::<u64>().unwrap_or(0);
177                let index_supported = if color_support.is_empty() {
178                    index < 16
179                } else {
180                    index < 256
181                };
182                if index_supported {
183                    let vc = VTermColor::Indexed { idx: index as u8 };
184                    if is_fg {
185                        sc.fg = Some(vc);
186                    } else {
187                        sc.bg = Some(vc);
188                    }
189                }
190            } else {
191                let vc = color_to_vterm_color(color, color_support);
192                if is_fg {
193                    sc.fg = vc;
194                } else {
195                    sc.bg = vc;
196                }
197            }
198        }
199    }
200
201    sc
202}
203
204pub fn parse_suggestion_color_fish(suggestion_str: &str, color_support: ColorSupport) -> Option<SuggestionColor> {
205    let c = parse_fish_color_from_string(suggestion_str, color_support);
206    let vc = color_to_vterm_color(c, color_support)?;
207    Some(SuggestionColor { fg: Some(vc), bg: None })
208}
209
210pub fn parse_hint_color_nu(suggestion_str: impl AsRef<str>) -> SuggestionColor {
211    let color = nu_color_config::lookup_ansi_color_style(suggestion_str.as_ref());
212    SuggestionColor {
213        fg: color.foreground.map(VTermColor::from),
214        bg: color.background.map(VTermColor::from),
215    }
216}
217
218#[derive(PartialEq, Eq, Debug)]
219#[repr(u8)]
220enum ColorType {
221    Named = 1,
222    Rgb   = 2,
223}
224
225#[derive(Debug, PartialEq, Eq)]
226struct Color {
227    kind: ColorType,
228    name_idx: u8,
229    rgb: [u8; 3],
230}
231
232fn bool_from_string(x: &str) -> bool {
233    match x.chars().next() {
234        Some(first) => "YTyt1".contains(first),
235        None => false,
236    }
237}
238
239const fn squared_difference(p1: i64, p2: i64) -> u64 {
240    let diff = (p1 - p2).unsigned_abs();
241    diff * diff
242}
243
244const fn convert_color(rgb: [u8; 3], colors: &[u32]) -> u8 {
245    let r = rgb[0] as i64;
246    let g = rgb[1] as i64;
247    let b = rgb[2] as i64;
248
249    let mut best_distance = u64::MAX;
250    let mut best_index = u8::MAX;
251
252    let mut i = 0;
253    while i < colors.len() {
254        let color = colors[i];
255        let test_r = ((color >> 16) & 0xff) as i64;
256        let test_g = ((color >> 8) & 0xff) as i64;
257        let test_b = (color & 0xff) as i64;
258        let distance = squared_difference(r, test_r) + squared_difference(g, test_g) + squared_difference(b, test_b);
259        if distance <= best_distance {
260            best_index = i as u8;
261            best_distance = distance;
262        }
263        i += 1;
264    }
265
266    best_index
267}
268
269/// We support the following style of rgb formats (case insensitive):
270/// `#FA3`, `#F3A035`, `FA3`, `F3A035`
271fn try_parse_rgb(name: &str) -> Option<Color> {
272    // Skip any leading #.
273    let name = match name.strip_prefix('#') {
274        Some(name) => name,
275        None => name,
276    };
277
278    let mut color = Color {
279        kind: ColorType::Rgb,
280        name_idx: 0,
281        rgb: [0, 0, 0],
282    };
283
284    let name = name.as_bytes();
285
286    match name.len() {
287        // Format: FA3
288        3 => {
289            for (i, c) in name.iter().enumerate().take(3) {
290                let val = char::from(*c).to_digit(16)? as u8;
291                color.rgb[i] = val * 16 + val;
292            }
293            Some(color)
294        },
295        // Format: F3A035
296        6 => {
297            for i in 0..3 {
298                let val_hi = char::from(name[i * 2]).to_digit(16)? as u8;
299                let val_low = char::from(name[i * 2 + 1]).to_digit(16)? as u8;
300                color.rgb[i] = val_hi * 16 + val_low;
301            }
302            Some(color)
303        },
304        _ => None,
305    }
306}
307
308struct NamedColor {
309    name: &'static str,
310    idx: u8,
311    _rgb: [u8; 3],
312}
313
314macro_rules! decl_named_colors {
315    ($({$name: expr, $idx: expr, { $r: expr, $g: expr, $b: expr }}),*,) => {
316        &[
317            $(
318                NamedColor {
319                    name: $name,
320                    idx: $idx,
321                    _rgb: [$r, $g, $b],
322                },
323            )*
324        ]
325    };
326}
327
328// Keep this sorted alphabetically
329static NAMED_COLORS: &[NamedColor] = decl_named_colors! {
330    {"black", 0, {0x00, 0x00, 0x00}},      {"blue", 4, {0x00, 0x00, 0x80}},
331    {"brblack", 8, {0x80, 0x80, 0x80}},    {"brblue", 12, {0x00, 0x00, 0xFF}},
332    {"brbrown", 11, {0xFF, 0xFF, 0x00}},    {"brcyan", 14, {0x00, 0xFF, 0xFF}},
333    {"brgreen", 10, {0x00, 0xFF, 0x00}},   {"brgrey", 8, {0x55, 0x55, 0x55}},
334    {"brmagenta", 13, {0xFF, 0x00, 0xFF}}, {"brown", 3, {0x72, 0x50, 0x00}},
335    {"brpurple", 13, {0xFF, 0x00, 0xFF}},   {"brred", 9, {0xFF, 0x00, 0x00}},
336    {"brwhite", 15, {0xFF, 0xFF, 0xFF}},   {"bryellow", 11, {0xFF, 0xFF, 0x00}},
337    {"cyan", 6, {0x00, 0x80, 0x80}},       {"green", 2, {0x00, 0x80, 0x00}},
338    {"grey", 7, {0xE5, 0xE5, 0xE5}},        {"magenta", 5, {0x80, 0x00, 0x80}},
339    {"purple", 5, {0x80, 0x00, 0x80}},      {"red", 1, {0x80, 0x00, 0x00}},
340    {"white", 7, {0xC0, 0xC0, 0xC0}},      {"yellow", 3, {0x80, 0x80, 0x00}},
341};
342
343fn try_parse_named(s: &str) -> Option<Color> {
344    let idx_res = NAMED_COLORS.binary_search_by(|elem| elem.name.cmp(&s.to_ascii_lowercase()));
345    if let Ok(idx) = idx_res {
346        return Some(Color {
347            kind: ColorType::Named,
348            name_idx: NAMED_COLORS[idx].idx,
349            rgb: [0, 0, 0],
350        });
351    }
352    None
353}
354
355const fn term16_color_for_rgb(rgb: [u8; 3]) -> u8 {
356    const K_COLORS: &[u32] = &[
357        0x000000, // Black
358        0x800000, // Red
359        0x008000, // Green
360        0x808000, // Yellow
361        0x000080, // Blue
362        0x800080, // Magenta
363        0x008080, // Cyan
364        0xc0c0c0, // White
365        0x808080, // Bright Black
366        0xff0000, // Bright Red
367        0x00ff00, // Bright Green
368        0xffff00, // Bright Yellow
369        0x0000ff, // Bright Blue
370        0xff00ff, // Bright Magenta
371        0x00ffff, // Bright Cyan
372        0xffffff, // Bright White
373    ];
374    convert_color(rgb, K_COLORS)
375}
376
377const fn term256_color_for_rgb(rgb: [u8; 3]) -> u8 {
378    const K_COLORS: &[u32] = &[
379        0x000000, 0x00005f, 0x000087, 0x0000af, 0x0000d7, 0x0000ff, 0x005f00, 0x005f5f, 0x005f87, 0x005faf, 0x005fd7,
380        0x005fff, 0x008700, 0x00875f, 0x008787, 0x0087af, 0x0087d7, 0x0087ff, 0x00af00, 0x00af5f, 0x00af87, 0x00afaf,
381        0x00afd7, 0x00afff, 0x00d700, 0x00d75f, 0x00d787, 0x00d7af, 0x00d7d7, 0x00d7ff, 0x00ff00, 0x00ff5f, 0x00ff87,
382        0x00ffaf, 0x00ffd7, 0x00ffff, 0x5f0000, 0x5f005f, 0x5f0087, 0x5f00af, 0x5f00d7, 0x5f00ff, 0x5f5f00, 0x5f5f5f,
383        0x5f5f87, 0x5f5faf, 0x5f5fd7, 0x5f5fff, 0x5f8700, 0x5f875f, 0x5f8787, 0x5f87af, 0x5f87d7, 0x5f87ff, 0x5faf00,
384        0x5faf5f, 0x5faf87, 0x5fafaf, 0x5fafd7, 0x5fafff, 0x5fd700, 0x5fd75f, 0x5fd787, 0x5fd7af, 0x5fd7d7, 0x5fd7ff,
385        0x5fff00, 0x5fff5f, 0x5fff87, 0x5fffaf, 0x5fffd7, 0x5fffff, 0x870000, 0x87005f, 0x870087, 0x8700af, 0x8700d7,
386        0x8700ff, 0x875f00, 0x875f5f, 0x875f87, 0x875faf, 0x875fd7, 0x875fff, 0x878700, 0x87875f, 0x878787, 0x8787af,
387        0x8787d7, 0x8787ff, 0x87af00, 0x87af5f, 0x87af87, 0x87afaf, 0x87afd7, 0x87afff, 0x87d700, 0x87d75f, 0x87d787,
388        0x87d7af, 0x87d7d7, 0x87d7ff, 0x87ff00, 0x87ff5f, 0x87ff87, 0x87ffaf, 0x87ffd7, 0x87ffff, 0xaf0000, 0xaf005f,
389        0xaf0087, 0xaf00af, 0xaf00d7, 0xaf00ff, 0xaf5f00, 0xaf5f5f, 0xaf5f87, 0xaf5faf, 0xaf5fd7, 0xaf5fff, 0xaf8700,
390        0xaf875f, 0xaf8787, 0xaf87af, 0xaf87d7, 0xaf87ff, 0xafaf00, 0xafaf5f, 0xafaf87, 0xafafaf, 0xafafd7, 0xafafff,
391        0xafd700, 0xafd75f, 0xafd787, 0xafd7af, 0xafd7d7, 0xafd7ff, 0xafff00, 0xafff5f, 0xafff87, 0xafffaf, 0xafffd7,
392        0xafffff, 0xd70000, 0xd7005f, 0xd70087, 0xd700af, 0xd700d7, 0xd700ff, 0xd75f00, 0xd75f5f, 0xd75f87, 0xd75faf,
393        0xd75fd7, 0xd75fff, 0xd78700, 0xd7875f, 0xd78787, 0xd787af, 0xd787d7, 0xd787ff, 0xd7af00, 0xd7af5f, 0xd7af87,
394        0xd7afaf, 0xd7afd7, 0xd7afff, 0xd7d700, 0xd7d75f, 0xd7d787, 0xd7d7af, 0xd7d7d7, 0xd7d7ff, 0xd7ff00, 0xd7ff5f,
395        0xd7ff87, 0xd7ffaf, 0xd7ffd7, 0xd7ffff, 0xff0000, 0xff005f, 0xff0087, 0xff00af, 0xff00d7, 0xff00ff, 0xff5f00,
396        0xff5f5f, 0xff5f87, 0xff5faf, 0xff5fd7, 0xff5fff, 0xff8700, 0xff875f, 0xff8787, 0xff87af, 0xff87d7, 0xff87ff,
397        0xffaf00, 0xffaf5f, 0xffaf87, 0xffafaf, 0xffafd7, 0xffafff, 0xffd700, 0xffd75f, 0xffd787, 0xffd7af, 0xffd7d7,
398        0xffd7ff, 0xffff00, 0xffff5f, 0xffff87, 0xffffaf, 0xffffd7, 0xffffff, 0x080808, 0x121212, 0x1c1c1c, 0x262626,
399        0x303030, 0x3a3a3a, 0x444444, 0x4e4e4e, 0x585858, 0x626262, 0x6c6c6c, 0x767676, 0x808080, 0x8a8a8a, 0x949494,
400        0x9e9e9e, 0xa8a8a8, 0xb2b2b2, 0xbcbcbc, 0xc6c6c6, 0xd0d0d0, 0xdadada, 0xe4e4e4, 0xeeeeee,
401    ];
402    16 + convert_color(rgb, K_COLORS)
403}
404
405fn parse_fish_color_from_string(s: &str, color_support: ColorSupport) -> Option<Color> {
406    let mut first_rgb = None;
407    let mut first_named = None;
408
409    for color_name in s.split([' ', '\t']) {
410        if !color_name.starts_with('-') {
411            let mut color = try_parse_named(color_name);
412            if color.is_none() {
413                color = try_parse_rgb(color_name);
414            }
415            if let Some(color) = color {
416                if first_rgb.is_none() && color.kind == ColorType::Rgb {
417                    first_rgb = Some(color);
418                } else if first_named.is_none() && color.kind == ColorType::Named {
419                    first_named = Some(color);
420                }
421            }
422        }
423    }
424
425    if (first_rgb.is_some() && color_support.contains(ColorSupport::TERM24BIT)) || first_named.is_none() {
426        return first_rgb;
427    }
428
429    first_named
430}
431
432fn color_to_vterm_color(c: Option<Color>, color_support: ColorSupport) -> Option<VTermColor> {
433    let c = c?;
434    if c.kind == ColorType::Rgb {
435        if color_support.contains(ColorSupport::TERM24BIT) {
436            Some(VTermColor::from_rgb(c.rgb[0], c.rgb[1], c.rgb[2]))
437        } else if color_support.contains(ColorSupport::TERM256) {
438            Some(VTermColor::from_idx(term256_color_for_rgb(c.rgb)))
439        } else {
440            Some(VTermColor::from_idx(term16_color_for_rgb(c.rgb)))
441        }
442    } else {
443        Some(VTermColor::from_idx(c.name_idx))
444    }
445}
446
447#[cfg(test)]
448mod test {
449    use super::*;
450
451    #[test]
452    fn color_support() {
453        // make sure it doesn't panic
454        get_color_support();
455
456        for (key, _) in std::env::vars() {
457            std::env::remove_var(key);
458        }
459
460        let assert_supports = |vars: &[(&str, &str)], expected: ColorSupport| {
461            for (key, value) in vars {
462                std::env::set_var(key, value);
463            }
464
465            assert_eq!(get_color_support(), expected);
466
467            for (key, _) in vars {
468                std::env::remove_var(key);
469            }
470        };
471
472        // no env
473        assert_supports(&[], ColorSupport::empty());
474
475        // TERM256
476        // fish_term256
477        assert_supports(&[("fish_term256", "y")], ColorSupport::TERM256);
478        assert_supports(&[("fish_term256", "n")], ColorSupport::empty());
479        // TERM=*256color*
480        assert_supports(&[("TERM", "foo_256color_bar")], ColorSupport::TERM256);
481        // xterm
482        assert_supports(&[("TERM", "xterm")], ColorSupport::TERM256);
483        // recent Terminal.app
484        assert_supports(
485            &[
486                ("TERM", "xterm"),
487                ("TERM_PROGRAM", "Apple_Terminal"),
488                ("TERM_PROGRAM_VERSION", "300"),
489            ],
490            ColorSupport::TERM256,
491        );
492        // old Terminal.app
493        assert_supports(
494            &[
495                ("TERM", "xterm"),
496                ("TERM_PROGRAM", "Apple_Terminal"),
497                ("TERM_PROGRAM_VERSION", "200"),
498            ],
499            ColorSupport::empty(),
500        );
501
502        // TERM24BIT
503        // fish_term24bit
504        assert_supports(&[("fish_term24bit", "y")], ColorSupport::TERM24BIT);
505        assert_supports(&[("fish_term24bit", "n")], ColorSupport::empty());
506        // screen/emacs
507        assert_supports(&[("TERM", "eterm"), ("STY", "foo")], ColorSupport::empty());
508        // colorterm
509        assert_supports(&[("COLORTERM", "truecolor")], ColorSupport::TERM24BIT);
510        assert_supports(&[("COLORTERM", "24bit")], ColorSupport::TERM24BIT);
511        assert_supports(&[("COLORTERM", "foo")], ColorSupport::empty());
512        // konsole
513        assert_supports(&[("KONSOLE_VERSION", "foo")], ColorSupport::TERM24BIT);
514        // iterm
515        assert_supports(&[("ITERM_SESSION_ID", "1:2")], ColorSupport::TERM24BIT);
516        // st
517        assert_supports(&[("TERM", "st-foo")], ColorSupport::TERM24BIT);
518        // vte
519        assert_supports(&[("VTE_VERSION", "3500")], ColorSupport::empty());
520        assert_supports(&[("VTE_VERSION", "3700")], ColorSupport::TERM24BIT);
521    }
522
523    #[test]
524    fn assert_named_colors_sort() {
525        NAMED_COLORS
526            .windows(2)
527            .for_each(|elems| assert!(elems[0].name.cmp(elems[1].name).is_lt()));
528    }
529
530    #[test]
531    fn parse_color() {
532        // parse_rgb
533        // Should parse
534        assert!(try_parse_rgb("#ffffff").is_some());
535        assert!(try_parse_rgb("#000000").is_some());
536        assert!(try_parse_rgb("#ababab").is_some());
537        assert!(try_parse_rgb("000000").is_some());
538        assert!(try_parse_rgb("ffffff").is_some());
539        assert!(try_parse_rgb("abcabc").is_some());
540        assert!(try_parse_rgb("#123").is_some());
541        assert!(try_parse_rgb("#fff").is_some());
542        assert!(try_parse_rgb("abc").is_some());
543        assert!(try_parse_rgb("123").is_some());
544        assert!(try_parse_rgb("fff").is_some());
545        assert!(try_parse_rgb("000").is_some());
546
547        // Should not parse
548        assert!(try_parse_rgb("#xyz").is_none());
549        assert!(try_parse_rgb("12").is_none());
550        assert!(try_parse_rgb("abcdeh").is_none());
551        assert!(try_parse_rgb("#ffff").is_none());
552        assert!(try_parse_rgb("12345").is_none());
553        assert!(try_parse_rgb("1234567").is_none());
554
555        // parse_named
556        // Should parse
557        assert!(try_parse_named("blue").is_some());
558        assert!(try_parse_named("white").is_some());
559        assert!(try_parse_named("yellow").is_some());
560        assert!(try_parse_named("brblack").is_some());
561        assert!(try_parse_named("BrBlue").is_some());
562        assert!(try_parse_named("bRYelLow").is_some());
563
564        // Should not parse
565        assert!(try_parse_named("aaa").is_none());
566        assert!(try_parse_named("blu").is_none());
567        assert!(try_parse_named("other").is_none());
568    }
569
570    #[test]
571    fn parse_fish_autosuggest() {
572        assert_eq!(
573            parse_fish_color_from_string("cyan", ColorSupport::TERM256),
574            Some(Color {
575                kind: ColorType::Named,
576                name_idx: 6,
577                rgb: [0, 0, 0]
578            })
579        );
580        assert_eq!(
581            parse_fish_color_from_string("#123", ColorSupport::TERM256),
582            Some(Color {
583                kind: ColorType::Rgb,
584                name_idx: 0,
585                rgb: [0x11, 0x22, 0x33]
586            })
587        );
588        assert_eq!(
589            parse_fish_color_from_string("-ignore\t-white\t-#123\tcyan", ColorSupport::TERM256),
590            Some(Color {
591                kind: ColorType::Named,
592                name_idx: 6,
593                rgb: [0, 0, 0]
594            })
595        );
596        assert_eq!(
597            parse_fish_color_from_string("555 brblack", ColorSupport::TERM256),
598            Some(Color {
599                kind: ColorType::Named,
600                name_idx: 8,
601                rgb: [0, 0, 0]
602            })
603        );
604        assert_eq!(
605            parse_fish_color_from_string("555 brblack", ColorSupport::TERM24BIT),
606            Some(Color {
607                kind: ColorType::Rgb,
608                name_idx: 0,
609                rgb: [0x55, 0x55, 0x55]
610            })
611        );
612        assert_eq!(
613            parse_fish_color_from_string("-ignore -all", ColorSupport::TERM256),
614            None
615        );
616    }
617
618    #[test]
619    fn parse_zsh_autosuggest() {
620        assert_eq!(
621            // color support supports rgb
622            parse_suggestion_color_zsh_autosuggest("fg=#123,bg=#456", ColorSupport::TERM24BIT),
623            SuggestionColor {
624                fg: Some(VTermColor::from_rgb(0x11, 0x22, 0x33)),
625                bg: Some(VTermColor::from_rgb(0x44, 0x55, 0x66)),
626            }
627        );
628        assert_eq!(
629            // color support doesn't support rgb
630            parse_suggestion_color_zsh_autosuggest("fg=#123,bg=#456", ColorSupport::empty()),
631            SuggestionColor {
632                fg: Some(VTermColor::from_idx(0)),
633                bg: Some(VTermColor::from_idx(8)),
634            }
635        );
636        assert_eq!(
637            // default
638            parse_suggestion_color_zsh_autosuggest("fg=8", ColorSupport::empty()),
639            SuggestionColor {
640                fg: Some(VTermColor::from_idx(8)),
641                bg: None,
642            }
643        );
644        assert_eq!(
645            // ignore and recover from invalid data
646            parse_suggestion_color_zsh_autosuggest("invalid=!,,=,bg=cyan", ColorSupport::empty()),
647            SuggestionColor {
648                fg: None,
649                bg: Some(VTermColor::from_idx(6))
650            }
651        );
652    }
653}