eza/theme/
mod.rs

1// SPDX-FileCopyrightText: 2024 Christina Sørensen
2// SPDX-License-Identifier: EUPL-1.2
3//
4// SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors
5// SPDX-FileCopyrightText: 2014 Benjamin Sago
6// SPDX-License-Identifier: MIT
7use nu_ansi_term::Style;
8
9use std::collections::HashMap;
10
11use crate::fs::File;
12use crate::info::filetype::FileType;
13use crate::options::config::ThemeConfig;
14use crate::output::color_scale::ColorScaleOptions;
15use crate::output::file_name::Colours as FileNameColours;
16use crate::output::render;
17
18mod ui_styles;
19pub(crate) use self::ui_styles::FileType as ThemeFileType;
20pub use self::ui_styles::UiStyles;
21pub(crate) use self::ui_styles::*;
22
23mod lsc;
24pub use self::lsc::LSColors;
25
26mod default_theme;
27
28#[derive(PartialEq, Eq, Debug)]
29pub struct Options {
30    pub use_colours: UseColours,
31
32    pub colour_scale: ColorScaleOptions,
33
34    pub definitions: Definitions,
35
36    pub theme_config: Option<ThemeConfig>,
37}
38
39/// Under what circumstances we should display coloured, rather than plain,
40/// output to the terminal.
41///
42/// By default, we want to display the colours when stdout can display them.
43/// Turning them on when output is going to, say, a pipe, would make programs
44/// such as `grep` or `more` not work properly. So the `Automatic` mode does
45/// this check and only displays colours when they can be truly appreciated.
46#[derive(PartialEq, Eq, Debug, Copy, Clone)]
47pub enum UseColours {
48    /// Display them even when output isn’t going to a terminal.
49    Always,
50
51    /// Display them when output is going to a terminal, but not otherwise.
52    Automatic,
53
54    /// Never display them, even when output is going to a terminal.
55    Never,
56}
57
58#[derive(PartialEq, Eq, Debug, Default)]
59pub struct Definitions {
60    pub ls: Option<String>,
61    pub exa: Option<String>,
62}
63
64pub struct Theme {
65    pub ui: UiStyles,
66    pub exts: Box<dyn FileStyle>,
67}
68
69impl Options {
70    #[must_use]
71    pub fn to_theme(&self, isatty: bool) -> Theme {
72        if self.use_colours == UseColours::Never
73            || (self.use_colours == UseColours::Automatic && !isatty)
74        {
75            let ui = UiStyles::plain();
76            let exts = Box::new(NoFileStyle);
77            return Theme { ui, exts };
78        }
79
80        #[cfg(windows)]
81        if nu_ansi_term::enable_ansi_support().is_err() {
82            // Failed to enable ansi support, probably because legacy mode console.
83            // No need to alert the user unless they explicitly set color=always
84            if self.use_colours == UseColours::Always {
85                eprintln!("eza: Ignoring option color=always in legacy console.");
86            }
87            let ui = UiStyles::plain();
88            let exts = Box::new(NoFileStyle);
89            return Theme { ui, exts };
90        }
91
92        match self.theme_config {
93            Some(ref theme) => {
94                if let Some(mut ui) = theme.to_theme() {
95                    let (exts, use_default_filetypes) = self.definitions.parse_color_vars(&mut ui);
96                    let exts: Box<dyn FileStyle> =
97                        match (exts.is_non_empty(), use_default_filetypes) {
98                            (false, false) => Box::new(NoFileStyle),
99                            (false, true) => Box::new(FileTypes),
100                            (true, false) => Box::new(exts),
101                            (true, true) => Box::new((exts, FileTypes)),
102                        };
103                    return Theme { ui, exts };
104                }
105                self.default_theme()
106            }
107            None => self.default_theme(),
108        }
109    }
110
111    fn default_theme(&self) -> Theme {
112        let mut ui = UiStyles::default_theme(self.colour_scale);
113        let (exts, use_default_filetypes) = self.definitions.parse_color_vars(&mut ui);
114        let exts: Box<dyn FileStyle> = match (exts.is_non_empty(), use_default_filetypes) {
115            (false, false) => Box::new(NoFileStyle),
116            (false, true) => Box::new(FileTypes),
117            (true, false) => Box::new(exts),
118            (true, true) => Box::new((exts, FileTypes)),
119        };
120        Theme { ui, exts }
121    }
122}
123
124impl Definitions {
125    /// Parse the environment variables into `LS_COLORS` pairs, putting file glob
126    /// colours into the `ExtensionMappings` that gets returned, and using the
127    /// two-character UI codes to modify the mutable `Colours`.
128    ///
129    /// Also returns if the `EZA_COLORS` variable should reset the existing file
130    /// type mappings or not. The `reset` code needs to be the first one.
131    fn parse_color_vars(&self, colours: &mut UiStyles) -> (ExtensionMappings, bool) {
132        use log::warn;
133
134        let mut exts = ExtensionMappings::default();
135
136        if let Some(lsc) = &self.ls {
137            LSColors(lsc).each_pair(|pair| {
138                if !colours.set_ls(&pair) {
139                    match glob::Pattern::new(pair.key) {
140                        Ok(pat) => {
141                            exts.add(pat, pair.to_style());
142                        }
143                        Err(e) => {
144                            warn!("Couldn't parse glob pattern {:?}: {}", pair.key, e);
145                        }
146                    }
147                }
148            });
149        }
150
151        let mut use_default_filetypes = true;
152
153        if let Some(exa) = &self.exa {
154            // Is this hacky? Yes.
155            if exa == "reset" || exa.starts_with("reset:") {
156                use_default_filetypes = false;
157            }
158
159            LSColors(exa).each_pair(|pair| {
160                if !colours.set_ls(&pair) && !colours.set_exa(&pair) {
161                    match glob::Pattern::new(pair.key) {
162                        Ok(pat) => {
163                            exts.add(pat, pair.to_style());
164                        }
165                        Err(e) => {
166                            warn!("Couldn't parse glob pattern {:?}: {}", pair.key, e);
167                        }
168                    }
169                }
170            });
171        }
172
173        (exts, use_default_filetypes)
174    }
175}
176
177/// Determine the style to paint the text for the filename part of the output.
178pub trait FileStyle: Sync {
179    /// Return the style to paint the filename text for `file` from the given
180    /// `theme`.
181    fn get_style(&self, file: &File<'_>, theme: &Theme) -> Option<Style>;
182}
183
184#[derive(PartialEq, Debug)]
185struct NoFileStyle;
186
187impl FileStyle for NoFileStyle {
188    fn get_style(&self, _file: &File<'_>, _theme: &Theme) -> Option<Style> {
189        None
190    }
191}
192
193// When getting the colour of a file from a *pair* of colourisers, try the
194// first one then try the second one. This lets the user provide their own
195// file type associations, while falling back to the default set if not set
196// explicitly.
197impl<A, B> FileStyle for (A, B)
198where
199    A: FileStyle,
200    B: FileStyle,
201{
202    fn get_style(&self, file: &File<'_>, theme: &Theme) -> Option<Style> {
203        self.0
204            .get_style(file, theme)
205            .or_else(|| self.1.get_style(file, theme))
206    }
207}
208
209#[derive(PartialEq, Debug, Default)]
210struct ExtensionMappings {
211    mappings: Vec<GlobPattern>,
212}
213
214#[derive(PartialEq, Debug)]
215/// Using a hashmap here for "simple" patterns (plain extensions like '*.txt')
216/// improves performance drastically for complex `LS_COLORS` usage (see
217/// <https://github.com/eza-community/eza/pull/1421#issuecomment-2816666661>).
218///
219/// It doesn't change highlighting behavior, as we still walk the
220/// [`ExtensionMappings`] in reverse order, and the hashmap will only consist of
221/// disjoint sets (it doesn't matter in which order we search *.txt or *.pdf).
222///
223/// In the event that a pattern shows up twice, we will use the later one (since
224/// .insert overrides any entry that exists), which is the correct behavior.
225enum GlobPattern {
226    Complex(glob::Pattern, Style),
227    Simple(HashMap<String, Style>),
228}
229
230impl ExtensionMappings {
231    fn is_non_empty(&self) -> bool {
232        !self.mappings.is_empty()
233    }
234
235    fn add(&mut self, pattern: glob::Pattern, style: Style) {
236        match (self.mappings.last_mut(), is_simple_pattern(pattern)) {
237            (Some(GlobPattern::Simple(h)), Ok(s)) => {
238                h.insert(s, style);
239            }
240            (_, Ok(s)) => {
241                self.mappings
242                    .push(GlobPattern::Simple(HashMap::from([(s, style)])));
243            }
244            (_, Err(p)) => {
245                self.mappings.push(GlobPattern::Complex(p, style));
246            }
247        }
248    }
249}
250
251fn is_simple_pattern(pattern: glob::Pattern) -> Result<String, glob::Pattern> {
252    match pattern.as_str().strip_prefix("*.") {
253        // Maybe too pessimistic here, some of these might be valid.
254        //
255        // For example, '*.[*]' is a simple pattern with ext '*'
256        // (the [*] is treated as a literal *, not as a glob).
257        //
258        // Ideally we'd inspect pattern.tokens, but it's not public.
259        None => Err(pattern),
260        Some(ext) if ext.contains(['?', '*', '[', ']', '.']) => Err(pattern),
261        Some(ext) => Ok(ext.to_string()),
262    }
263}
264
265// Loop through backwards so that colours specified later in the list override
266// colours specified earlier, like we do with options and strict mode
267
268impl FileStyle for ExtensionMappings {
269    fn get_style(&self, file: &File<'_>, _theme: &Theme) -> Option<Style> {
270        let maybe_ext = file.name.rsplit_once('.').map(|x| x.1);
271
272        for mapping in self.mappings.iter().rev() {
273            match mapping {
274                GlobPattern::Complex(pat, style) => {
275                    if pat.matches(&file.name) {
276                        return Some(*style);
277                    }
278                }
279                GlobPattern::Simple(map) => {
280                    if let Some(ext) = maybe_ext {
281                        if let Some(style) = map.get(ext) {
282                            return Some(*style);
283                        }
284                    }
285                }
286            }
287        }
288
289        None
290    }
291}
292
293#[derive(Debug)]
294struct FileTypes;
295
296impl FileStyle for FileTypes {
297    fn get_style(&self, file: &File<'_>, theme: &Theme) -> Option<Style> {
298        #[rustfmt::skip]
299        return match FileType::get_file_type(file) {
300            Some(FileType::Image)      => theme.ui.file_type.unwrap_or_default().image,
301            Some(FileType::Video)      => theme.ui.file_type.unwrap_or_default().video,
302            Some(FileType::Music)      => theme.ui.file_type.unwrap_or_default().music,
303            Some(FileType::Lossless)   => theme.ui.file_type.unwrap_or_default().lossless,
304            Some(FileType::Crypto)     => theme.ui.file_type.unwrap_or_default().crypto,
305            Some(FileType::Document)   => theme.ui.file_type.unwrap_or_default().document,
306            Some(FileType::Compressed) => theme.ui.file_type.unwrap_or_default().compressed,
307            Some(FileType::Temp)       => theme.ui.file_type.unwrap_or_default().temp,
308            Some(FileType::Compiled)   => theme.ui.file_type.unwrap_or_default().compiled,
309            Some(FileType::Build)      => theme.ui.file_type.unwrap_or_default().build,
310            Some(FileType::Source)     => theme.ui.file_type.unwrap_or_default().source,
311            None                       => None
312    };
313    }
314}
315
316#[cfg(unix)]
317impl render::BlocksColours for Theme {
318    fn blocksize(&self, prefix: Option<number_prefix::Prefix>) -> Style {
319        use number_prefix::Prefix::{Gibi, Giga, Kibi, Kilo, Mebi, Mega};
320
321        #[rustfmt::skip]
322        let style = match prefix {
323            Some(Kilo | Kibi) => self.ui.size.unwrap_or_default().number_kilo,
324            Some(Mega | Mebi) => self.ui.size.unwrap_or_default().number_mega,
325            Some(Giga | Gibi) => self.ui.size.unwrap_or_default().number_giga,
326            Some(_)           => self.ui.size.unwrap_or_default().number_huge,
327            None              => self.ui.size.unwrap_or_default().number_byte,
328        };
329        style.unwrap_or_default()
330    }
331
332    fn unit(&self, prefix: Option<number_prefix::Prefix>) -> Style {
333        use number_prefix::Prefix::{Gibi, Giga, Kibi, Kilo, Mebi, Mega};
334
335        #[rustfmt::skip]
336           let style = match prefix {
337            Some(Kilo | Kibi) => self.ui.size.unwrap_or_default().unit_kilo,
338            Some(Mega | Mebi) => self.ui.size.unwrap_or_default().unit_mega,
339            Some(Giga | Gibi) => self.ui.size.unwrap_or_default().unit_giga,
340            Some(_)           => self.ui.size.unwrap_or_default().unit_huge,
341            None              => self.ui.size.unwrap_or_default().unit_byte,
342        };
343        style.unwrap_or_default()
344    }
345
346    fn no_blocksize(&self) -> Style {
347        self.ui.punctuation.unwrap_or_default()
348    }
349}
350
351#[rustfmt::skip]
352impl render::FiletypeColours for Theme {
353    fn normal(&self)       -> Style { self.ui.filekinds.unwrap_or_default().normal() }
354    fn directory(&self)    -> Style { self.ui.filekinds.unwrap_or_default().directory() }
355    fn pipe(&self)         -> Style { self.ui.filekinds.unwrap_or_default().pipe() }
356    fn symlink(&self)      -> Style { self.ui.filekinds.unwrap_or_default().symlink() }
357    fn block_device(&self) -> Style { self.ui.filekinds.unwrap_or_default().block_device() }
358    fn char_device(&self)  -> Style { self.ui.filekinds.unwrap_or_default().char_device() }
359    fn socket(&self)       -> Style { self.ui.filekinds.unwrap_or_default().socket() }
360    fn special(&self)      -> Style { self.ui.filekinds.unwrap_or_default().special() }
361}
362
363#[rustfmt::skip]
364impl render::GitColours for Theme {
365    fn not_modified(&self)  -> Style { self.ui.punctuation() }
366    #[allow(clippy::new_ret_no_self)]
367    fn new(&self)           -> Style { self.ui.git.unwrap_or_default().new() }
368    fn modified(&self)      -> Style { self.ui.git.unwrap_or_default().modified() }
369    fn deleted(&self)       -> Style { self.ui.git.unwrap_or_default().deleted() }
370    fn renamed(&self)       -> Style { self.ui.git.unwrap_or_default().renamed() }
371    fn type_change(&self)   -> Style { self.ui.git.unwrap_or_default().typechange() }
372    fn ignored(&self)       -> Style { self.ui.git.unwrap_or_default().ignored() }
373    fn conflicted(&self)    -> Style { self.ui.git.unwrap_or_default().conflicted() }
374}
375
376#[rustfmt::skip]
377impl render::GitRepoColours for Theme {
378    fn branch_main(&self)  -> Style { self.ui.git_repo.unwrap_or_default().branch_main() }
379    fn branch_other(&self) -> Style { self.ui.git_repo.unwrap_or_default().branch_other() }
380    fn no_repo(&self)      -> Style { self.ui.punctuation() }
381    fn git_clean(&self)    -> Style { self.ui.git_repo.unwrap_or_default().git_clean() }
382    fn git_dirty(&self)    -> Style { self.ui.git_repo.unwrap_or_default().git_dirty() }
383}
384
385#[rustfmt::skip]
386#[cfg(unix)]
387impl render::GroupColours for Theme {
388    fn yours(&self)      -> Style { self.ui.users.unwrap_or_default().group_yours() }
389    fn not_yours(&self)  -> Style { self.ui.users.unwrap_or_default().group_other() }
390    fn root_group(&self) -> Style { self.ui.users.unwrap_or_default().group_root() }
391    fn no_group(&self)   -> Style { self.ui.punctuation() }
392}
393
394#[rustfmt::skip]
395impl render::LinksColours for Theme {
396    fn normal(&self)           -> Style { self.ui.links.unwrap_or_default().normal() }
397    fn multi_link_file(&self)  -> Style { self.ui.links.unwrap_or_default().multi_link_file() }
398}
399
400#[rustfmt::skip]
401impl render::PermissionsColours for Theme {
402    fn dash(&self)               -> Style { self.ui.punctuation() }
403    fn user_read(&self)          -> Style { self.ui.perms.unwrap_or_default().user_read() }
404    fn user_write(&self)         -> Style { self.ui.perms.unwrap_or_default().user_write() }
405    fn user_execute_file(&self)  -> Style { self.ui.perms.unwrap_or_default().user_execute_file() }
406    fn user_execute_other(&self) -> Style { self.ui.perms.unwrap_or_default().user_execute_other() }
407    fn group_read(&self)         -> Style { self.ui.perms.unwrap_or_default().group_read() }
408    fn group_write(&self)        -> Style { self.ui.perms.unwrap_or_default().group_write() }
409    fn group_execute(&self)      -> Style { self.ui.perms.unwrap_or_default().group_execute() }
410    fn other_read(&self)         -> Style { self.ui.perms.unwrap_or_default().other_read() }
411    fn other_write(&self)        -> Style { self.ui.perms.unwrap_or_default().other_write() }
412    fn other_execute(&self)      -> Style { self.ui.perms.unwrap_or_default().other_execute() }
413    fn special_user_file(&self)  -> Style { self.ui.perms.unwrap_or_default().special_user_file() }
414    fn special_other(&self)      -> Style { self.ui.perms.unwrap_or_default().special_other() }
415    fn attribute(&self)          -> Style { self.ui.perms.unwrap_or_default().attribute() }
416}
417
418impl render::SizeColours for Theme {
419    fn size(&self, prefix: Option<number_prefix::Prefix>) -> Style {
420        use number_prefix::Prefix::{Gibi, Giga, Kibi, Kilo, Mebi, Mega};
421
422        #[rustfmt::skip]
423        return match prefix {
424            Some(Kilo | Kibi) => self.ui.size.unwrap_or_default().number_kilo(),
425            Some(Mega | Mebi) => self.ui.size.unwrap_or_default().number_mega(),
426            Some(Giga | Gibi) => self.ui.size.unwrap_or_default().number_giga(),
427            Some(_)           => self.ui.size.unwrap_or_default().number_huge(),
428            None              => self.ui.size.unwrap_or_default().number_byte(),
429        };
430    }
431
432    fn unit(&self, prefix: Option<number_prefix::Prefix>) -> Style {
433        use number_prefix::Prefix::{Gibi, Giga, Kibi, Kilo, Mebi, Mega};
434
435        #[rustfmt::skip]
436        return match prefix {
437            Some(Kilo | Kibi) => self.ui.size.unwrap_or_default().unit_kilo(),
438            Some(Mega | Mebi) => self.ui.size.unwrap_or_default().unit_mega(),
439            Some(Giga | Gibi) => self.ui.size.unwrap_or_default().unit_giga(),
440            Some(_)           => self.ui.size.unwrap_or_default().unit_huge(),
441            None              => self.ui.size.unwrap_or_default().unit_byte(),
442        };
443    }
444
445    #[rustfmt::skip]
446    fn no_size(&self) -> Style { self.ui.punctuation() }
447    #[rustfmt::skip]
448    fn major(&self)   -> Style { self.ui.size.unwrap_or_default().major() }
449    #[rustfmt::skip]
450    fn comma(&self)   -> Style { self.ui.punctuation() }
451    #[rustfmt::skip]
452    fn minor(&self)   -> Style { self.ui.size.unwrap_or_default().minor() }
453}
454
455#[rustfmt::skip]
456#[cfg(unix)]
457impl render::UserColours for Theme {
458    fn you(&self)           -> Style { self.ui.users.unwrap_or_default().user_you() }
459    fn other(&self)         -> Style { self.ui.users.unwrap_or_default().user_other() }
460    fn root(&self)          -> Style { self.ui.users.unwrap_or_default().user_root() }
461    fn no_user(&self)       -> Style { self.ui.punctuation() }
462}
463
464#[rustfmt::skip]
465impl FileNameColours for Theme {
466    fn symlink_path(&self)        -> Style { self.ui.symlink_path() }
467    fn normal_arrow(&self)        -> Style { self.ui.punctuation() }
468    fn broken_symlink(&self)      -> Style { self.ui.broken_symlink() }
469    fn broken_filename(&self)     -> Style { apply_overlay(self.ui.broken_symlink(), self.ui.broken_path_overlay()) }
470    fn control_char(&self)        -> Style { self.ui.control_char() }
471    fn broken_control_char(&self) -> Style { apply_overlay(self.ui.control_char(),   self.ui.broken_path_overlay()) }
472    fn executable_file(&self)     -> Style { self.ui.filekinds.unwrap_or_default().executable() }
473    fn mount_point(&self)         -> Style { self.ui.filekinds.unwrap_or_default().mount_point() }
474
475    fn colour_file(&self, file: &File<'_>) -> Style {
476        self.exts
477            .get_style(file, self)
478            .unwrap_or(self.ui.filekinds.unwrap_or_default().normal())
479    }
480
481 fn style_override(&self, file: &File<'_>) -> Option<FileNameStyle> {
482        if let Some(ref name_overrides) = self.ui.filenames {
483            if let Some(file_override) = name_overrides.get(&file.name) {
484                return Some(*file_override);
485            }
486        }
487
488        if let Some(ref ext_overrides) = self.ui.extensions {
489            if let Some(ext) = file.ext.clone() {
490                if let Some(file_override) = ext_overrides.get(&ext) {
491                    return Some(*file_override);
492                }
493            }
494        }
495
496        None
497    }
498}
499
500#[rustfmt::skip]
501impl render::SecurityCtxColours for Theme {
502    fn none(&self)          -> Style { self.ui.security_context.unwrap_or_default().none() }
503    fn selinux_colon(&self) -> Style { self.ui.security_context.unwrap_or_default().selinux().colon() }
504    fn selinux_user(&self)  -> Style { self.ui.security_context.unwrap_or_default().selinux().user() }
505    fn selinux_role(&self)  -> Style { self.ui.security_context.unwrap_or_default().selinux().role() }
506    fn selinux_type(&self)  -> Style { self.ui.security_context.unwrap_or_default().selinux().typ() }
507    fn selinux_range(&self) -> Style { self.ui.security_context.unwrap_or_default().selinux().range() }
508}
509
510/// Some of the styles are **overlays**: although they have the same attribute
511/// set as regular styles (foreground and background colours, bold, underline,
512/// etc), they’re intended to be used to *amend* existing styles.
513///
514/// For example, the target path of a broken symlink is displayed in a red,
515/// underlined style by default. Paths can contain control characters, so
516/// these control characters need to be underlined too, otherwise it looks
517/// weird. So instead of having four separate configurable styles for “link
518/// path”, “broken link path”, “control character” and “broken control
519/// character”, there are styles for “link path”, “control character”, and
520/// “broken link overlay”, the latter of which is just set to override the
521/// underline attribute on the other two.
522#[rustfmt::skip]
523fn apply_overlay(mut base: Style, overlay: Style) -> Style {
524    if let Some(fg) = overlay.foreground { base.foreground = Some(fg); }
525    if let Some(bg) = overlay.background { base.background = Some(bg); }
526
527    if overlay.is_bold          { base.is_bold          = true; }
528    if overlay.is_dimmed        { base.is_dimmed        = true; }
529    if overlay.is_italic        { base.is_italic        = true; }
530    if overlay.is_underline     { base.is_underline     = true; }
531    if overlay.is_blink         { base.is_blink         = true; }
532    if overlay.is_reverse       { base.is_reverse       = true; }
533    if overlay.is_hidden        { base.is_hidden        = true; }
534    if overlay.is_strikethrough { base.is_strikethrough = true; }
535
536    base
537}
538
539#[cfg(test)]
540#[cfg(unix)]
541mod customs_test {
542    use super::*;
543    use crate::theme::ui_styles::UiStyles;
544    use nu_ansi_term::Color::*;
545
546    impl ExtensionMappings {
547        // helper for test suite
548        fn to_vec_pat_style(&self) -> Vec<(glob::Pattern, Style)> {
549            let mut out = Vec::new();
550            for map in &self.mappings {
551                match map {
552                    GlobPattern::Complex(p, s) => {
553                        out.push((p.clone(), *s));
554                    }
555                    GlobPattern::Simple(h) => {
556                        let mut simple_pats = h
557                            .iter()
558                            .map(|(k, v)| (glob::Pattern::new(&format!("*.{k}")).unwrap(), *v))
559                            .collect::<Vec<(glob::Pattern, Style)>>();
560
561                        simple_pats.sort_by_key(|x| x.0.clone());
562
563                        out.extend(simple_pats);
564                    }
565                }
566            }
567            out
568        }
569    }
570
571    macro_rules! test {
572        ($name:ident:  ls $ls:expr, exa $exa:expr  =>  colours $expected:ident -> $process_expected:expr) => {
573            #[allow(non_snake_case)]
574            #[test]
575            fn $name() {
576                let mut $expected = UiStyles::default();
577                $process_expected();
578
579                let definitions = Definitions {
580                    ls: Some($ls.into()),
581                    exa: Some($exa.into()),
582                };
583
584                let mut result = UiStyles::default();
585                let (_, _) = definitions.parse_color_vars(&mut result);
586                assert_eq!($expected, result);
587            }
588        };
589        ($name:ident:  ls $ls:expr, exa $exa:expr  =>  exts $mappings:expr) => {
590            #[test]
591            fn $name() {
592                let mappings: Vec<(glob::Pattern, Style)> = $mappings
593                    .iter()
594                    .map(|t| (glob::Pattern::new(t.0).unwrap(), t.1))
595                    .collect();
596
597                let definitions = Definitions {
598                    ls: Some($ls.into()),
599                    exa: Some($exa.into()),
600                };
601
602                let (result, _) = definitions.parse_color_vars(&mut UiStyles::default());
603                assert_eq!(mappings, result.to_vec_pat_style());
604            }
605        };
606        ($name:ident:  ls $ls:expr, exa $exa:expr  =>  colours $expected:ident -> $process_expected:expr, exts $mappings:expr) => {
607            #[test]
608            fn $name() {
609                let mut $expected = UiStyles::default();
610                $process_expected();
611
612                let mappings: Vec<(glob::Pattern, Style)> = $mappings
613                    .iter()
614                    .map(|t| (glob::Pattern::new(t.0).unwrap(), t.1))
615                    .collect();
616
617                let definitions = Definitions {
618                    ls: Some($ls.into()),
619                    exa: Some($exa.into()),
620                };
621
622                let mut result = UiStyles::default();
623                let (exts, _) = definitions.parse_color_vars(&mut result);
624
625                assert_eq!(mappings, exts.to_vec_pat_style());
626                assert_eq!($expected, result);
627            }
628        };
629    }
630
631    // LS_COLORS can affect all of these colours:
632    test!(ls_di:   ls "di=31", exa ""  =>  colours c -> { c.filekinds().directory    = Some(Red.normal());    });
633    test!(ls_ex:   ls "ex=32", exa ""  =>  colours c -> { c.filekinds().executable   = Some(Green.normal());  });
634    test!(ls_fi:   ls "fi=33", exa ""  =>  colours c -> { c.filekinds().normal       = Some(Yellow.normal()); });
635    test!(ls_pi:   ls "pi=34", exa ""  =>  colours c -> { c.filekinds().pipe         = Some(Blue.normal());   });
636    test!(ls_so:   ls "so=35", exa ""  =>  colours c -> { c.filekinds().socket       = Some(Purple.normal()); });
637    test!(ls_bd:   ls "bd=36", exa ""  =>  colours c -> { c.filekinds().block_device = Some(Cyan.normal());   });
638    test!(ls_cd:   ls "cd=35", exa ""  =>  colours c -> { c.filekinds().char_device  = Some(Purple.normal()); });
639    test!(ls_ln:   ls "ln=34", exa ""  =>  colours c -> { c.filekinds().symlink      = Some(Blue.normal());   });
640    test!(ls_or:   ls "or=33", exa ""  =>  colours c -> { c.broken_symlink         = Some(Yellow.normal()); });
641
642    // EZA_COLORS can affect all those colours too:
643    test!(exa_di:  ls "", exa "di=32"  =>  colours c -> { c.filekinds().directory    = Some(Green.normal());  });
644    test!(exa_ex:  ls "", exa "ex=33"  =>  colours c -> { c.filekinds().executable   = Some(Yellow.normal()); });
645    test!(exa_fi:  ls "", exa "fi=34"  =>  colours c -> { c.filekinds().normal       = Some(Blue.normal());   });
646    test!(exa_pi:  ls "", exa "pi=35"  =>  colours c -> { c.filekinds().pipe         = Some(Purple.normal()); });
647    test!(exa_so:  ls "", exa "so=36"  =>  colours c -> { c.filekinds().socket       = Some(Cyan.normal());   });
648    test!(exa_bd:  ls "", exa "bd=35"  =>  colours c -> { c.filekinds().block_device = Some(Purple.normal()); });
649    test!(exa_cd:  ls "", exa "cd=34"  =>  colours c -> { c.filekinds().char_device  = Some(Blue.normal());   });
650    test!(exa_ln:  ls "", exa "ln=33"  =>  colours c -> { c.filekinds().symlink      = Some(Yellow.normal()); });
651    test!(exa_or:  ls "", exa "or=32"  =>  colours c -> { c.broken_symlink         = Some(Green.normal());  });
652
653    // EZA_COLORS will even override options from LS_COLORS:
654    test!(ls_exa_di: ls "di=31", exa "di=32"  =>  colours c -> { c.filekinds().directory  = Some(Green.normal());  });
655    test!(ls_exa_ex: ls "ex=32", exa "ex=33"  =>  colours c -> { c.filekinds().executable = Some(Yellow.normal()); });
656    test!(ls_exa_fi: ls "fi=33", exa "fi=34"  =>  colours c -> { c.filekinds().normal     = Some(Blue.normal());   });
657
658    // But more importantly, EZA_COLORS has its own, special list of colours:
659    test!(exa_ur:  ls "", exa "ur=38;5;100"  =>  colours c -> { c.perms().user_read           = Some(Fixed(100).normal()); });
660    test!(exa_uw:  ls "", exa "uw=38;5;101"  =>  colours c -> { c.perms().user_write          = Some(Fixed(101).normal()); });
661    test!(exa_ux:  ls "", exa "ux=38;5;102"  =>  colours c -> { c.perms().user_execute_file   = Some(Fixed(102).normal()); });
662    test!(exa_ue:  ls "", exa "ue=38;5;103"  =>  colours c -> { c.perms().user_execute_other  = Some(Fixed(103).normal()); });
663    test!(exa_gr:  ls "", exa "gr=38;5;104"  =>  colours c -> { c.perms().group_read          = Some(Fixed(104).normal()); });
664    test!(exa_gw:  ls "", exa "gw=38;5;105"  =>  colours c -> { c.perms().group_write         = Some(Fixed(105).normal()); });
665    test!(exa_gx:  ls "", exa "gx=38;5;106"  =>  colours c -> { c.perms().group_execute       = Some(Fixed(106).normal()); });
666    test!(exa_tr:  ls "", exa "tr=38;5;107"  =>  colours c -> { c.perms().other_read          = Some(Fixed(107).normal()); });
667    test!(exa_tw:  ls "", exa "tw=38;5;108"  =>  colours c -> { c.perms().other_write         = Some(Fixed(108).normal()); });
668    test!(exa_tx:  ls "", exa "tx=38;5;109"  =>  colours c -> { c.perms().other_execute       = Some(Fixed(109).normal()); });
669    test!(exa_su:  ls "", exa "su=38;5;110"  =>  colours c -> { c.perms().special_user_file   = Some(Fixed(110).normal()); });
670    test!(exa_sf:  ls "", exa "sf=38;5;111"  =>  colours c -> { c.perms().special_other       = Some(Fixed(111).normal()); });
671    test!(exa_xa:  ls "", exa "xa=38;5;112"  =>  colours c -> { c.perms().attribute           = Some(Fixed(112).normal()); });
672
673    test!(exa_sn:  ls "", exa "sn=38;5;113" => colours c -> {
674        c.size().number_byte = Some(Fixed(113).normal());
675        c.size().number_kilo = Some(Fixed(113).normal());
676        c.size().number_mega = Some(Fixed(113).normal());
677        c.size().number_giga = Some(Fixed(113).normal());
678        c.size().number_huge = Some(Fixed(113).normal());
679    });
680    test!(exa_sb:  ls "", exa "sb=38;5;114" => colours c -> {
681        c.size().unit_byte = Some(Fixed(114).normal());
682        c.size().unit_kilo = Some(Fixed(114).normal());
683        c.size().unit_mega = Some(Fixed(114).normal());
684        c.size().unit_giga = Some(Fixed(114).normal());
685        c.size().unit_huge = Some(Fixed(114).normal());
686    });
687
688    test!(exa_nb:  ls "", exa "nb=38;5;115"  =>  colours c -> { c.size().number_byte                      = Some(Fixed(115).normal()); });
689    test!(exa_nk:  ls "", exa "nk=38;5;116"  =>  colours c -> { c.size().number_kilo                      = Some(Fixed(116).normal()); });
690    test!(exa_nm:  ls "", exa "nm=38;5;117"  =>  colours c -> { c.size().number_mega                      = Some(Fixed(117).normal()); });
691    test!(exa_ng:  ls "", exa "ng=38;5;118"  =>  colours c -> { c.size().number_giga                      = Some(Fixed(118).normal()); });
692    test!(exa_nt:  ls "", exa "nt=38;5;119"  =>  colours c -> { c.size().number_huge                      = Some(Fixed(119).normal()); });
693
694    test!(exa_ub:  ls "", exa "ub=38;5;115"  =>  colours c -> { c.size().unit_byte                        = Some(Fixed(115).normal()); });
695    test!(exa_uk:  ls "", exa "uk=38;5;116"  =>  colours c -> { c.size().unit_kilo                        = Some(Fixed(116).normal()); });
696    test!(exa_um:  ls "", exa "um=38;5;117"  =>  colours c -> { c.size().unit_mega                        = Some(Fixed(117).normal()); });
697    test!(exa_ug:  ls "", exa "ug=38;5;118"  =>  colours c -> { c.size().unit_giga                        = Some(Fixed(118).normal()); });
698    test!(exa_ut:  ls "", exa "ut=38;5;119"  =>  colours c -> { c.size().unit_huge                        = Some(Fixed(119).normal()); });
699
700    test!(exa_df:  ls "", exa "df=38;5;115"  =>  colours c -> { c.size().major                            = Some(Fixed(115).normal()); });
701    test!(exa_ds:  ls "", exa "ds=38;5;116"  =>  colours c -> { c.size().minor                            = Some(Fixed(116).normal()); });
702
703    test!(exa_uu:  ls "", exa "uu=38;5;117"  =>  colours c -> { c.users().user_you                        = Some(Fixed(117).normal()); });
704    test!(exa_un:  ls "", exa "un=38;5;118"  =>  colours c -> { c.users().user_other                      = Some(Fixed(118).normal()); });
705    test!(exa_gu:  ls "", exa "gu=38;5;119"  =>  colours c -> { c.users().group_yours                     = Some(Fixed(119).normal()); });
706    test!(exa_gn:  ls "", exa "gn=38;5;120"  =>  colours c -> { c.users().group_other                     = Some(Fixed(120).normal()); });
707
708    test!(exa_lc:  ls "", exa "lc=38;5;121"  =>  colours c -> { c.links().normal                          = Some(Fixed(121).normal()); });
709    test!(exa_lm:  ls "", exa "lm=38;5;122"  =>  colours c -> { c.links().multi_link_file                 = Some(Fixed(122).normal()); });
710
711    test!(exa_ga:  ls "", exa "ga=38;5;123"  =>  colours c -> { c.git().new                               = Some(Fixed(123).normal()); });
712    test!(exa_gm:  ls "", exa "gm=38;5;124"  =>  colours c -> { c.git().modified                          = Some(Fixed(124).normal()); });
713    test!(exa_gd:  ls "", exa "gd=38;5;125"  =>  colours c -> { c.git().deleted                           = Some(Fixed(125).normal()); });
714    test!(exa_gv:  ls "", exa "gv=38;5;126"  =>  colours c -> { c.git().renamed                           = Some(Fixed(126).normal()); });
715    test!(exa_gt:  ls "", exa "gt=38;5;127"  =>  colours c -> { c.git().typechange                        = Some(Fixed(127).normal()); });
716    test!(exa_gi:  ls "", exa "gi=38;5;128"  =>  colours c -> { c.git().ignored                           = Some(Fixed(128).normal()); });
717    test!(exa_gc:  ls "", exa "gc=38;5;129"  =>  colours c -> { c.git().conflicted                        = Some(Fixed(129).normal()); });
718
719    test!(exa_xx:  ls "", exa "xx=38;5;128"  =>  colours c -> { c.punctuation                           = Some(Fixed(128).normal()); });
720    test!(exa_da:  ls "", exa "da=38;5;129"  =>  colours c -> { c.date                                  = Some(Fixed(129).normal()); });
721    test!(exa_in:  ls "", exa "in=38;5;130"  =>  colours c -> { c.inode                                 = Some(Fixed(130).normal()); });
722    test!(exa_bl:  ls "", exa "bl=38;5;131"  =>  colours c -> { c.blocks                                = Some(Fixed(131).normal()); });
723    test!(exa_hd:  ls "", exa "hd=38;5;132"  =>  colours c -> { c.header                                = Some(Fixed(132).normal()); });
724    test!(exa_lp:  ls "", exa "lp=38;5;133"  =>  colours c -> { c.symlink_path                          = Some(Fixed(133).normal()); });
725    test!(exa_cc:  ls "", exa "cc=38;5;134"  =>  colours c -> { c.control_char                          = Some(Fixed(134).normal()); });
726    test!(exa_oc:  ls "", exa "oc=38;5;135"  =>  colours c -> { c.octal                                 = Some(Fixed(135).normal()); });
727    test!(exa_ff:  ls "", exa "ff=38;5;136"  =>  colours c -> { c.flags                                 = Some(Fixed(136).normal()); });
728    test!(exa_bo:  ls "", exa "bO=4"         =>  colours c -> { c.broken_path_overlay                   = Some(Style::default().underline()); });
729
730    test!(exa_mp:  ls "", exa "mp=1;34;4"    =>  colours c -> { c.filekinds().mount_point                 = Some(Blue.bold().underline()); });
731    test!(exa_sp:  ls "", exa "sp=1;35;4"    =>  colours c -> { c.filekinds().special                     = Some(Purple.bold().underline()); });
732
733    test!(exa_im:  ls "", exa "im=38;5;128"  =>  colours c -> { c.file_type().image                       = Some(Fixed(128).normal()); });
734    test!(exa_vi:  ls "", exa "vi=38;5;129"  =>  colours c -> { c.file_type().video                       = Some(Fixed(129).normal()); });
735    test!(exa_mu:  ls "", exa "mu=38;5;130"  =>  colours c -> { c.file_type().music                       = Some(Fixed(130).normal()); });
736    test!(exa_lo:  ls "", exa "lo=38;5;131"  =>  colours c -> { c.file_type().lossless                    = Some(Fixed(131).normal()); });
737    test!(exa_cr:  ls "", exa "cr=38;5;132"  =>  colours c -> { c.file_type().crypto                      = Some(Fixed(132).normal()); });
738    test!(exa_do:  ls "", exa "do=38;5;133"  =>  colours c -> { c.file_type().document                    = Some(Fixed(133).normal()); });
739    test!(exa_co:  ls "", exa "co=38;5;134"  =>  colours c -> { c.file_type().compressed                  = Some(Fixed(134).normal()); });
740    test!(exa_tm:  ls "", exa "tm=38;5;135"  =>  colours c -> { c.file_type().temp                        = Some(Fixed(135).normal()); });
741    test!(exa_cm:  ls "", exa "cm=38;5;136"  =>  colours c -> { c.file_type().compiled                    = Some(Fixed(136).normal()); });
742    test!(exa_ie:  ls "", exa "bu=38;5;137"  =>  colours c -> { c.file_type().build                       = Some(Fixed(137).normal()); });
743    test!(exa_bu:  ls "", exa "bu=38;5;137"  =>  colours c -> { c.file_type().build                       = Some(Fixed(137).normal()); });
744    test!(exa_sc:  ls "", exa "sc=38;5;138"  =>  colours c -> { c.file_type().source                      = Some(Fixed(138).normal()); });
745
746    test!(exa_Sn:  ls "", exa "Sn=38;5;128"  =>  colours c -> { c.security_context().none                   = Some(Fixed(128).normal()); });
747    test!(exa_Su:  ls "", exa "Su=38;5;129"  =>  colours c -> { c.security_context().selinux().user         = Some(Fixed(129).normal()); });
748    test!(exa_Sr:  ls "", exa "Sr=38;5;130"  =>  colours c -> { c.security_context().selinux().role         = Some(Fixed(130).normal()); });
749    test!(exa_St:  ls "", exa "St=38;5;131"  =>  colours c -> { c.security_context().selinux().typ          = Some(Fixed(131).normal()); });
750    test!(exa_Sl:  ls "", exa "Sl=38;5;132"  =>  colours c -> { c.security_context().selinux().range        = Some(Fixed(132).normal()); });
751
752    // All the while, LS_COLORS treats them as filenames:
753    test!(ls_uu:   ls "uu=38;5;117", exa ""  =>  exts [ ("uu", Fixed(117).normal()) ]);
754    test!(ls_un:   ls "un=38;5;118", exa ""  =>  exts [ ("un", Fixed(118).normal()) ]);
755    test!(ls_gu:   ls "gu=38;5;119", exa ""  =>  exts [ ("gu", Fixed(119).normal()) ]);
756    test!(ls_gn:   ls "gn=38;5;120", exa ""  =>  exts [ ("gn", Fixed(120).normal()) ]);
757
758    // Just like all other keys:
759    test!(ls_txt:  ls "*.txt=31",          exa ""  =>  exts [ ("*.txt",      Red.normal())             ]);
760    test!(ls_mp3:  ls "*.mp3=38;5;135",    exa ""  =>  exts [ ("*.mp3",      Fixed(135).normal())      ]);
761    test!(ls_mak:  ls "Makefile=1;32;4",   exa ""  =>  exts [ ("Makefile",   Green.bold().underline()) ]);
762    test!(exa_txt: ls "", exa "*.zip=31"           =>  exts [ ("*.zip",      Red.normal())             ]);
763    test!(exa_mp3: ls "", exa "lev.*=38;5;153"     =>  exts [ ("lev.*",      Fixed(153).normal())      ]);
764    test!(exa_mak: ls "", exa "Cargo.toml=4;32;1"  =>  exts [ ("Cargo.toml", Green.bold().underline()) ]);
765
766    // Testing whether a glob from EZA_COLORS overrides a glob from LS_COLORS
767    // can’t be tested here, because they’ll both be added to the same vec
768
769    // Values get separated by colons:
770    test!(ls_multi:     ls "*.txt=31:*.rtf=32", exa ""  => exts [ ("*.rtf", Green.normal()),   ("*.txt", Red.normal()) ]);
771    test!(exa_multi:    ls "", exa "*.tmp=37:*.log=37"  => exts [ ("*.log", White.normal()), ("*.tmp", White.normal()) ]);
772    test!(ls_exa_multi: ls "*.txt=31", exa "*.rtf=32"   => exts [ ("*.rtf", Green.normal()),   ("*.txt", Red.normal())]);
773
774    test!(ls_five: ls "1*1=31:2*2=32:3*3=1;33:4*4=34;1:5*5=35;4", exa ""  =>  exts [
775        ("1*1", Red.normal()), ("2*2", Green.normal()), ("3*3", Yellow.bold()), ("4*4", Blue.bold()), ("5*5", Purple.underline())
776    ]);
777
778    // Finally, colours get applied right-to-left:
779    test!(ls_overwrite:  ls "pi=31:pi=32:pi=33", exa ""  =>  colours c -> { c.filekinds().pipe = Some(Yellow.normal()); });
780    test!(exa_overwrite: ls "", exa "da=36:da=35:da=34"  =>  colours c -> { c.date = Some(Blue.normal()); });
781
782    // Parse keys and extensions
783    test!(ls_fi_ls_txt:   ls "fi=33:*.txt=31", exa "" => colours c -> { c.filekinds().normal = Some(Yellow.normal()); }, exts [ ("*.txt", Red.normal()) ]);
784    test!(ls_fi_exa_txt:  ls "fi=33", exa "*.txt=31"  => colours c -> { c.filekinds().normal = Some(Yellow.normal()); }, exts [ ("*.txt", Red.normal()) ]);
785    test!(ls_txt_exa_fi:  ls "*.txt=31", exa "fi=33"  => colours c -> { c.filekinds().normal = Some(Yellow.normal()); }, exts [ ("*.txt", Red.normal()) ]);
786    test!(eza_fi_exa_txt: ls "", exa "fi=33:*.txt=31" => colours c -> { c.filekinds().normal = Some(Yellow.normal()); }, exts [ ("*.txt", Red.normal()) ]);
787}