1 use ansi_term::Style;
2 
3 use crate::fs::File;
4 use crate::output::file_name::Colours as FileNameColours;
5 use crate::output::render;
6 
7 mod ui_styles;
8 pub use self::ui_styles::UiStyles;
9 pub use self::ui_styles::Size as SizeColours;
10 
11 mod lsc;
12 pub use self::lsc::LSColors;
13 
14 mod default_theme;
15 
16 
17 #[derive(PartialEq, Debug)]
18 pub struct Options {
19 
20     pub use_colours: UseColours,
21 
22     pub colour_scale: ColourScale,
23 
24     pub definitions: Definitions,
25 }
26 
27 /// Under what circumstances we should display coloured, rather than plain,
28 /// output to the terminal.
29 ///
30 /// By default, we want to display the colours when stdout can display them.
31 /// Turning them on when output is going to, say, a pipe, would make programs
32 /// such as `grep` or `more` not work properly. So the `Automatic` mode does
33 /// this check and only displays colours when they can be truly appreciated.
34 #[derive(PartialEq, Debug, Copy, Clone)]
35 pub enum UseColours {
36 
37     /// Display them even when output isn’t going to a terminal.
38     Always,
39 
40     /// Display them when output is going to a terminal, but not otherwise.
41     Automatic,
42 
43     /// Never display them, even when output is going to a terminal.
44     Never,
45 }
46 
47 #[derive(PartialEq, Debug, Copy, Clone)]
48 pub enum ColourScale {
49     Fixed,
50     Gradient,
51 }
52 
53 #[derive(PartialEq, Debug, Default)]
54 pub struct Definitions {
55     pub ls: Option<String>,
56     pub exa: Option<String>,
57 }
58 
59 
60 pub struct Theme {
61     pub ui: UiStyles,
62     pub exts: Box<dyn FileColours>,
63 }
64 
65 impl Options {
66 
67     #[allow(trivial_casts)]   // the `as Box<_>` stuff below warns about this for some reason
to_theme(&self, isatty: bool) -> Theme68     pub fn to_theme(&self, isatty: bool) -> Theme {
69         use crate::info::filetype::FileExtensions;
70 
71         if self.use_colours == UseColours::Never || (self.use_colours == UseColours::Automatic && ! isatty) {
72             let ui = UiStyles::plain();
73             let exts = Box::new(NoFileColours);
74             return Theme { ui, exts };
75         }
76 
77         // Parse the environment variables into colours and extension mappings
78         let mut ui = UiStyles::default_theme(self.colour_scale);
79         let (exts, use_default_filetypes) = self.definitions.parse_color_vars(&mut ui);
80 
81         // Use between 0 and 2 file name highlighters
82         let exts = match (exts.is_non_empty(), use_default_filetypes) {
83             (false, false)  => Box::new(NoFileColours)           as Box<_>,
84             (false,  true)  => Box::new(FileExtensions)          as Box<_>,
85             ( true, false)  => Box::new(exts)                    as Box<_>,
86             ( true,  true)  => Box::new((exts, FileExtensions))  as Box<_>,
87         };
88 
89         Theme { ui, exts }
90     }
91 }
92 
93 impl Definitions {
94 
95     /// Parse the environment variables into `LS_COLORS` pairs, putting file glob
96     /// colours into the `ExtensionMappings` that gets returned, and using the
97     /// two-character UI codes to modify the mutable `Colours`.
98     ///
99     /// Also returns if the `EXA_COLORS` variable should reset the existing file
100     /// type mappings or not. The `reset` code needs to be the first one.
parse_color_vars(&self, colours: &mut UiStyles) -> (ExtensionMappings, bool)101     fn parse_color_vars(&self, colours: &mut UiStyles) -> (ExtensionMappings, bool) {
102         use log::*;
103 
104         let mut exts = ExtensionMappings::default();
105 
106         if let Some(lsc) = &self.ls {
107             LSColors(lsc).each_pair(|pair| {
108                 if ! colours.set_ls(&pair) {
109                     match glob::Pattern::new(pair.key) {
110                         Ok(pat) => {
111                             exts.add(pat, pair.to_style());
112                         }
113                         Err(e) => {
114                             warn!("Couldn't parse glob pattern {:?}: {}", pair.key, e);
115                         }
116                     }
117                 }
118             });
119         }
120 
121         let mut use_default_filetypes = true;
122 
123         if let Some(exa) = &self.exa {
124             // Is this hacky? Yes.
125             if exa == "reset" || exa.starts_with("reset:") {
126                 use_default_filetypes = false;
127             }
128 
129             LSColors(exa).each_pair(|pair| {
130                 if ! colours.set_ls(&pair) && ! colours.set_exa(&pair) {
131                     match glob::Pattern::new(pair.key) {
132                         Ok(pat) => {
133                             exts.add(pat, pair.to_style());
134                         }
135                         Err(e) => {
136                             warn!("Couldn't parse glob pattern {:?}: {}", pair.key, e);
137                         }
138                     }
139                 };
140             });
141         }
142 
143         (exts, use_default_filetypes)
144     }
145 }
146 
147 
148 pub trait FileColours: std::marker::Sync {
colour_file(&self, file: &File<'_>) -> Option<Style>149     fn colour_file(&self, file: &File<'_>) -> Option<Style>;
150 }
151 
152 #[derive(PartialEq, Debug)]
153 struct NoFileColours;
154 impl FileColours for NoFileColours {
colour_file(&self, _file: &File<'_>) -> Option<Style>155     fn colour_file(&self, _file: &File<'_>) -> Option<Style> {
156         None
157     }
158 }
159 
160 // When getting the colour of a file from a *pair* of colourisers, try the
161 // first one then try the second one. This lets the user provide their own
162 // file type associations, while falling back to the default set if not set
163 // explicitly.
164 impl<A, B> FileColours for (A, B)
165 where A: FileColours,
166       B: FileColours,
167 {
colour_file(&self, file: &File<'_>) -> Option<Style>168     fn colour_file(&self, file: &File<'_>) -> Option<Style> {
169         self.0.colour_file(file)
170             .or_else(|| self.1.colour_file(file))
171     }
172 }
173 
174 
175 #[derive(PartialEq, Debug, Default)]
176 struct ExtensionMappings {
177     mappings: Vec<(glob::Pattern, Style)>,
178 }
179 
180 // Loop through backwards so that colours specified later in the list override
181 // colours specified earlier, like we do with options and strict mode
182 
183 impl FileColours for ExtensionMappings {
colour_file(&self, file: &File<'_>) -> Option<Style>184     fn colour_file(&self, file: &File<'_>) -> Option<Style> {
185         self.mappings.iter().rev()
186             .find(|t| t.0.matches(&file.name))
187             .map (|t| t.1)
188     }
189 }
190 
191 impl ExtensionMappings {
is_non_empty(&self) -> bool192     fn is_non_empty(&self) -> bool {
193         ! self.mappings.is_empty()
194     }
195 
add(&mut self, pattern: glob::Pattern, style: Style)196     fn add(&mut self, pattern: glob::Pattern, style: Style) {
197         self.mappings.push((pattern, style))
198     }
199 }
200 
201 
202 
203 
204 impl render::BlocksColours for Theme {
block_count(&self) -> Style205     fn block_count(&self)  -> Style { self.ui.blocks }
no_blocks(&self) -> Style206     fn no_blocks(&self)    -> Style { self.ui.punctuation }
207 }
208 
209 impl render::FiletypeColours for Theme {
normal(&self) -> Style210     fn normal(&self)       -> Style { self.ui.filekinds.normal }
directory(&self) -> Style211     fn directory(&self)    -> Style { self.ui.filekinds.directory }
pipe(&self) -> Style212     fn pipe(&self)         -> Style { self.ui.filekinds.pipe }
symlink(&self) -> Style213     fn symlink(&self)      -> Style { self.ui.filekinds.symlink }
block_device(&self) -> Style214     fn block_device(&self) -> Style { self.ui.filekinds.block_device }
char_device(&self) -> Style215     fn char_device(&self)  -> Style { self.ui.filekinds.char_device }
socket(&self) -> Style216     fn socket(&self)       -> Style { self.ui.filekinds.socket }
special(&self) -> Style217     fn special(&self)      -> Style { self.ui.filekinds.special }
218 }
219 
220 impl render::GitColours for Theme {
not_modified(&self) -> Style221     fn not_modified(&self)  -> Style { self.ui.punctuation }
222     #[allow(clippy::new_ret_no_self)]
new(&self) -> Style223     fn new(&self)           -> Style { self.ui.git.new }
modified(&self) -> Style224     fn modified(&self)      -> Style { self.ui.git.modified }
deleted(&self) -> Style225     fn deleted(&self)       -> Style { self.ui.git.deleted }
renamed(&self) -> Style226     fn renamed(&self)       -> Style { self.ui.git.renamed }
type_change(&self) -> Style227     fn type_change(&self)   -> Style { self.ui.git.typechange }
ignored(&self) -> Style228     fn ignored(&self)       -> Style { self.ui.git.ignored }
conflicted(&self) -> Style229     fn conflicted(&self)    -> Style { self.ui.git.conflicted }
230 }
231 
232 impl render::GroupColours for Theme {
yours(&self) -> Style233     fn yours(&self)      -> Style { self.ui.users.group_yours }
not_yours(&self) -> Style234     fn not_yours(&self)  -> Style { self.ui.users.group_not_yours }
235 }
236 
237 impl render::LinksColours for Theme {
normal(&self) -> Style238     fn normal(&self)           -> Style { self.ui.links.normal }
multi_link_file(&self) -> Style239     fn multi_link_file(&self)  -> Style { self.ui.links.multi_link_file }
240 }
241 
242 impl render::PermissionsColours for Theme {
dash(&self) -> Style243     fn dash(&self)               -> Style { self.ui.punctuation }
user_read(&self) -> Style244     fn user_read(&self)          -> Style { self.ui.perms.user_read }
user_write(&self) -> Style245     fn user_write(&self)         -> Style { self.ui.perms.user_write }
user_execute_file(&self) -> Style246     fn user_execute_file(&self)  -> Style { self.ui.perms.user_execute_file }
user_execute_other(&self) -> Style247     fn user_execute_other(&self) -> Style { self.ui.perms.user_execute_other }
group_read(&self) -> Style248     fn group_read(&self)         -> Style { self.ui.perms.group_read }
group_write(&self) -> Style249     fn group_write(&self)        -> Style { self.ui.perms.group_write }
group_execute(&self) -> Style250     fn group_execute(&self)      -> Style { self.ui.perms.group_execute }
other_read(&self) -> Style251     fn other_read(&self)         -> Style { self.ui.perms.other_read }
other_write(&self) -> Style252     fn other_write(&self)        -> Style { self.ui.perms.other_write }
other_execute(&self) -> Style253     fn other_execute(&self)      -> Style { self.ui.perms.other_execute }
special_user_file(&self) -> Style254     fn special_user_file(&self)  -> Style { self.ui.perms.special_user_file }
special_other(&self) -> Style255     fn special_other(&self)      -> Style { self.ui.perms.special_other }
attribute(&self) -> Style256     fn attribute(&self)          -> Style { self.ui.perms.attribute }
257 }
258 
259 impl render::SizeColours for Theme {
size(&self, prefix: Option<number_prefix::Prefix>) -> Style260     fn size(&self, prefix: Option<number_prefix::Prefix>) -> Style {
261         use number_prefix::Prefix::*;
262 
263         match prefix {
264             None                    => self.ui.size.number_byte,
265             Some(Kilo) | Some(Kibi) => self.ui.size.number_kilo,
266             Some(Mega) | Some(Mebi) => self.ui.size.number_mega,
267             Some(Giga) | Some(Gibi) => self.ui.size.number_giga,
268             Some(_)                 => self.ui.size.number_huge,
269         }
270     }
271 
unit(&self, prefix: Option<number_prefix::Prefix>) -> Style272     fn unit(&self, prefix: Option<number_prefix::Prefix>) -> Style {
273         use number_prefix::Prefix::*;
274 
275         match prefix {
276             None                    => self.ui.size.unit_byte,
277             Some(Kilo) | Some(Kibi) => self.ui.size.unit_kilo,
278             Some(Mega) | Some(Mebi) => self.ui.size.unit_mega,
279             Some(Giga) | Some(Gibi) => self.ui.size.unit_giga,
280             Some(_)                 => self.ui.size.unit_huge,
281         }
282     }
283 
no_size(&self) -> Style284     fn no_size(&self) -> Style { self.ui.punctuation }
major(&self) -> Style285     fn major(&self)   -> Style { self.ui.size.major }
comma(&self) -> Style286     fn comma(&self)   -> Style { self.ui.punctuation }
minor(&self) -> Style287     fn minor(&self)   -> Style { self.ui.size.minor }
288 }
289 
290 impl render::UserColours for Theme {
you(&self) -> Style291     fn you(&self)           -> Style { self.ui.users.user_you }
someone_else(&self) -> Style292     fn someone_else(&self)  -> Style { self.ui.users.user_someone_else }
293 }
294 
295 impl FileNameColours for Theme {
normal_arrow(&self) -> Style296     fn normal_arrow(&self)        -> Style { self.ui.punctuation }
broken_symlink(&self) -> Style297     fn broken_symlink(&self)      -> Style { self.ui.broken_symlink }
broken_filename(&self) -> Style298     fn broken_filename(&self)     -> Style { apply_overlay(self.ui.broken_symlink, self.ui.broken_path_overlay) }
broken_control_char(&self) -> Style299     fn broken_control_char(&self) -> Style { apply_overlay(self.ui.control_char,   self.ui.broken_path_overlay) }
control_char(&self) -> Style300     fn control_char(&self)        -> Style { self.ui.control_char }
symlink_path(&self) -> Style301     fn symlink_path(&self)        -> Style { self.ui.symlink_path }
executable_file(&self) -> Style302     fn executable_file(&self)     -> Style { self.ui.filekinds.executable }
303 
colour_file(&self, file: &File<'_>) -> Style304     fn colour_file(&self, file: &File<'_>) -> Style {
305         self.exts.colour_file(file).unwrap_or(self.ui.filekinds.normal)
306     }
307 }
308 
309 
310 /// Some of the styles are **overlays**: although they have the same attribute
311 /// set as regular styles (foreground and background colours, bold, underline,
312 /// etc), they’re intended to be used to *amend* existing styles.
313 ///
314 /// For example, the target path of a broken symlink is displayed in a red,
315 /// underlined style by default. Paths can contain control characters, so
316 /// these control characters need to be underlined too, otherwise it looks
317 /// weird. So instead of having four separate configurable styles for “link
318 /// path”, “broken link path”, “control character” and “broken control
319 /// character”, there are styles for “link path”, “control character”, and
320 /// “broken link overlay”, the latter of which is just set to override the
321 /// underline attribute on the other two.
apply_overlay(mut base: Style, overlay: Style) -> Style322 fn apply_overlay(mut base: Style, overlay: Style) -> Style {
323     if let Some(fg) = overlay.foreground { base.foreground = Some(fg); }
324     if let Some(bg) = overlay.background { base.background = Some(bg); }
325 
326     if overlay.is_bold          { base.is_bold          = true; }
327     if overlay.is_dimmed        { base.is_dimmed        = true; }
328     if overlay.is_italic        { base.is_italic        = true; }
329     if overlay.is_underline     { base.is_underline     = true; }
330     if overlay.is_blink         { base.is_blink         = true; }
331     if overlay.is_reverse       { base.is_reverse       = true; }
332     if overlay.is_hidden        { base.is_hidden        = true; }
333     if overlay.is_strikethrough { base.is_strikethrough = true; }
334 
335     base
336 }
337 // TODO: move this function to the ansi_term crate
338 
339 
340 #[cfg(test)]
341 mod customs_test {
342     use super::*;
343     use crate::theme::ui_styles::UiStyles;
344     use ansi_term::Colour::*;
345 
346     macro_rules! test {
347         ($name:ident:  ls $ls:expr, exa $exa:expr  =>  colours $expected:ident -> $process_expected:expr) => {
348             #[test]
349             fn $name() {
350                 let mut $expected = UiStyles::default();
351                 $process_expected();
352 
353                 let definitions = Definitions {
354                     ls:  Some($ls.into()),
355                     exa: Some($exa.into()),
356                 };
357 
358                 let mut result = UiStyles::default();
359                 let (_exts, _reset) = definitions.parse_color_vars(&mut result);
360                 assert_eq!($expected, result);
361             }
362         };
363         ($name:ident:  ls $ls:expr, exa $exa:expr  =>  exts $mappings:expr) => {
364             #[test]
365             fn $name() {
366                 let mappings: Vec<(glob::Pattern, Style)>
367                     = $mappings.iter()
368                                .map(|t| (glob::Pattern::new(t.0).unwrap(), t.1))
369                                .collect();
370 
371                 let definitions = Definitions {
372                     ls:  Some($ls.into()),
373                     exa: Some($exa.into()),
374                 };
375 
376                 let (result, _reset) = definitions.parse_color_vars(&mut UiStyles::default());
377                 assert_eq!(ExtensionMappings { mappings }, result);
378             }
379         };
380         ($name:ident:  ls $ls:expr, exa $exa:expr  =>  colours $expected:ident -> $process_expected:expr, exts $mappings:expr) => {
381             #[test]
382             fn $name() {
383                 let mut $expected = UiStyles::colourful(false);
384                 $process_expected();
385 
386                 let mappings: Vec<(glob::Pattern, Style)>
387                     = $mappings.into_iter()
388                                .map(|t| (glob::Pattern::new(t.0).unwrap(), t.1))
389                                .collect();
390 
391                 let definitions = Definitions {
392                     ls:  Some($ls.into()),
393                     exa: Some($exa.into()),
394                 };
395 
396                 let mut meh = UiStyles::colourful(false);
397                 let (result, _reset) = definitions.parse_color_vars(&vars, &mut meh);
398                 assert_eq!(ExtensionMappings { mappings }, result);
399                 assert_eq!($expected, meh);
400             }
401         };
402     }
403 
404 
405     // LS_COLORS can affect all of these colours:
406     test!(ls_di:   ls "di=31", exa ""  =>  colours c -> { c.filekinds.directory    = Red.normal();    });
407     test!(ls_ex:   ls "ex=32", exa ""  =>  colours c -> { c.filekinds.executable   = Green.normal();  });
408     test!(ls_fi:   ls "fi=33", exa ""  =>  colours c -> { c.filekinds.normal       = Yellow.normal(); });
409     test!(ls_pi:   ls "pi=34", exa ""  =>  colours c -> { c.filekinds.pipe         = Blue.normal();   });
410     test!(ls_so:   ls "so=35", exa ""  =>  colours c -> { c.filekinds.socket       = Purple.normal(); });
411     test!(ls_bd:   ls "bd=36", exa ""  =>  colours c -> { c.filekinds.block_device = Cyan.normal();   });
412     test!(ls_cd:   ls "cd=35", exa ""  =>  colours c -> { c.filekinds.char_device  = Purple.normal(); });
413     test!(ls_ln:   ls "ln=34", exa ""  =>  colours c -> { c.filekinds.symlink      = Blue.normal();   });
414     test!(ls_or:   ls "or=33", exa ""  =>  colours c -> { c.broken_symlink         = Yellow.normal(); });
415 
416     // EXA_COLORS can affect all those colours too:
417     test!(exa_di:  ls "", exa "di=32"  =>  colours c -> { c.filekinds.directory    = Green.normal();  });
418     test!(exa_ex:  ls "", exa "ex=33"  =>  colours c -> { c.filekinds.executable   = Yellow.normal(); });
419     test!(exa_fi:  ls "", exa "fi=34"  =>  colours c -> { c.filekinds.normal       = Blue.normal();   });
420     test!(exa_pi:  ls "", exa "pi=35"  =>  colours c -> { c.filekinds.pipe         = Purple.normal(); });
421     test!(exa_so:  ls "", exa "so=36"  =>  colours c -> { c.filekinds.socket       = Cyan.normal();   });
422     test!(exa_bd:  ls "", exa "bd=35"  =>  colours c -> { c.filekinds.block_device = Purple.normal(); });
423     test!(exa_cd:  ls "", exa "cd=34"  =>  colours c -> { c.filekinds.char_device  = Blue.normal();   });
424     test!(exa_ln:  ls "", exa "ln=33"  =>  colours c -> { c.filekinds.symlink      = Yellow.normal(); });
425     test!(exa_or:  ls "", exa "or=32"  =>  colours c -> { c.broken_symlink         = Green.normal();  });
426 
427     // EXA_COLORS will even override options from LS_COLORS:
428     test!(ls_exa_di: ls "di=31", exa "di=32"  =>  colours c -> { c.filekinds.directory  = Green.normal();  });
429     test!(ls_exa_ex: ls "ex=32", exa "ex=33"  =>  colours c -> { c.filekinds.executable = Yellow.normal(); });
430     test!(ls_exa_fi: ls "fi=33", exa "fi=34"  =>  colours c -> { c.filekinds.normal     = Blue.normal();   });
431 
432     // But more importantly, EXA_COLORS has its own, special list of colours:
433     test!(exa_ur:  ls "", exa "ur=38;5;100"  =>  colours c -> { c.perms.user_read           = Fixed(100).normal(); });
434     test!(exa_uw:  ls "", exa "uw=38;5;101"  =>  colours c -> { c.perms.user_write          = Fixed(101).normal(); });
435     test!(exa_ux:  ls "", exa "ux=38;5;102"  =>  colours c -> { c.perms.user_execute_file   = Fixed(102).normal(); });
436     test!(exa_ue:  ls "", exa "ue=38;5;103"  =>  colours c -> { c.perms.user_execute_other  = Fixed(103).normal(); });
437     test!(exa_gr:  ls "", exa "gr=38;5;104"  =>  colours c -> { c.perms.group_read          = Fixed(104).normal(); });
438     test!(exa_gw:  ls "", exa "gw=38;5;105"  =>  colours c -> { c.perms.group_write         = Fixed(105).normal(); });
439     test!(exa_gx:  ls "", exa "gx=38;5;106"  =>  colours c -> { c.perms.group_execute       = Fixed(106).normal(); });
440     test!(exa_tr:  ls "", exa "tr=38;5;107"  =>  colours c -> { c.perms.other_read          = Fixed(107).normal(); });
441     test!(exa_tw:  ls "", exa "tw=38;5;108"  =>  colours c -> { c.perms.other_write         = Fixed(108).normal(); });
442     test!(exa_tx:  ls "", exa "tx=38;5;109"  =>  colours c -> { c.perms.other_execute       = Fixed(109).normal(); });
443     test!(exa_su:  ls "", exa "su=38;5;110"  =>  colours c -> { c.perms.special_user_file   = Fixed(110).normal(); });
444     test!(exa_sf:  ls "", exa "sf=38;5;111"  =>  colours c -> { c.perms.special_other       = Fixed(111).normal(); });
445     test!(exa_xa:  ls "", exa "xa=38;5;112"  =>  colours c -> { c.perms.attribute           = Fixed(112).normal(); });
446 
447     test!(exa_sn:  ls "", exa "sn=38;5;113" => colours c -> {
448         c.size.number_byte = Fixed(113).normal();
449         c.size.number_kilo = Fixed(113).normal();
450         c.size.number_mega = Fixed(113).normal();
451         c.size.number_giga = Fixed(113).normal();
452         c.size.number_huge = Fixed(113).normal();
453     });
454     test!(exa_sb:  ls "", exa "sb=38;5;114" => colours c -> {
455         c.size.unit_byte = Fixed(114).normal();
456         c.size.unit_kilo = Fixed(114).normal();
457         c.size.unit_mega = Fixed(114).normal();
458         c.size.unit_giga = Fixed(114).normal();
459         c.size.unit_huge = Fixed(114).normal();
460     });
461 
462     test!(exa_nb:  ls "", exa "nb=38;5;115"  =>  colours c -> { c.size.number_byte          = Fixed(115).normal(); });
463     test!(exa_nk:  ls "", exa "nk=38;5;116"  =>  colours c -> { c.size.number_kilo          = Fixed(116).normal(); });
464     test!(exa_nm:  ls "", exa "nm=38;5;117"  =>  colours c -> { c.size.number_mega          = Fixed(117).normal(); });
465     test!(exa_ng:  ls "", exa "ng=38;5;118"  =>  colours c -> { c.size.number_giga          = Fixed(118).normal(); });
466     test!(exa_nh:  ls "", exa "nh=38;5;119"  =>  colours c -> { c.size.number_huge          = Fixed(119).normal(); });
467 
468     test!(exa_ub:  ls "", exa "ub=38;5;115"  =>  colours c -> { c.size.unit_byte            = Fixed(115).normal(); });
469     test!(exa_uk:  ls "", exa "uk=38;5;116"  =>  colours c -> { c.size.unit_kilo            = Fixed(116).normal(); });
470     test!(exa_um:  ls "", exa "um=38;5;117"  =>  colours c -> { c.size.unit_mega            = Fixed(117).normal(); });
471     test!(exa_ug:  ls "", exa "ug=38;5;118"  =>  colours c -> { c.size.unit_giga            = Fixed(118).normal(); });
472     test!(exa_uh:  ls "", exa "uh=38;5;119"  =>  colours c -> { c.size.unit_huge            = Fixed(119).normal(); });
473 
474     test!(exa_df:  ls "", exa "df=38;5;115"  =>  colours c -> { c.size.major                = Fixed(115).normal(); });
475     test!(exa_ds:  ls "", exa "ds=38;5;116"  =>  colours c -> { c.size.minor                = Fixed(116).normal(); });
476 
477     test!(exa_uu:  ls "", exa "uu=38;5;117"  =>  colours c -> { c.users.user_you            = Fixed(117).normal(); });
478     test!(exa_un:  ls "", exa "un=38;5;118"  =>  colours c -> { c.users.user_someone_else   = Fixed(118).normal(); });
479     test!(exa_gu:  ls "", exa "gu=38;5;119"  =>  colours c -> { c.users.group_yours         = Fixed(119).normal(); });
480     test!(exa_gn:  ls "", exa "gn=38;5;120"  =>  colours c -> { c.users.group_not_yours     = Fixed(120).normal(); });
481 
482     test!(exa_lc:  ls "", exa "lc=38;5;121"  =>  colours c -> { c.links.normal              = Fixed(121).normal(); });
483     test!(exa_lm:  ls "", exa "lm=38;5;122"  =>  colours c -> { c.links.multi_link_file     = Fixed(122).normal(); });
484 
485     test!(exa_ga:  ls "", exa "ga=38;5;123"  =>  colours c -> { c.git.new                   = Fixed(123).normal(); });
486     test!(exa_gm:  ls "", exa "gm=38;5;124"  =>  colours c -> { c.git.modified              = Fixed(124).normal(); });
487     test!(exa_gd:  ls "", exa "gd=38;5;125"  =>  colours c -> { c.git.deleted               = Fixed(125).normal(); });
488     test!(exa_gv:  ls "", exa "gv=38;5;126"  =>  colours c -> { c.git.renamed               = Fixed(126).normal(); });
489     test!(exa_gt:  ls "", exa "gt=38;5;127"  =>  colours c -> { c.git.typechange            = Fixed(127).normal(); });
490 
491     test!(exa_xx:  ls "", exa "xx=38;5;128"  =>  colours c -> { c.punctuation               = Fixed(128).normal(); });
492     test!(exa_da:  ls "", exa "da=38;5;129"  =>  colours c -> { c.date                      = Fixed(129).normal(); });
493     test!(exa_in:  ls "", exa "in=38;5;130"  =>  colours c -> { c.inode                     = Fixed(130).normal(); });
494     test!(exa_bl:  ls "", exa "bl=38;5;131"  =>  colours c -> { c.blocks                    = Fixed(131).normal(); });
495     test!(exa_hd:  ls "", exa "hd=38;5;132"  =>  colours c -> { c.header                    = Fixed(132).normal(); });
496     test!(exa_lp:  ls "", exa "lp=38;5;133"  =>  colours c -> { c.symlink_path              = Fixed(133).normal(); });
497     test!(exa_cc:  ls "", exa "cc=38;5;134"  =>  colours c -> { c.control_char              = Fixed(134).normal(); });
498     test!(exa_bo:  ls "", exa "bO=4"         =>  colours c -> { c.broken_path_overlay       = Style::default().underline(); });
499 
500     // All the while, LS_COLORS treats them as filenames:
501     test!(ls_uu:   ls "uu=38;5;117", exa ""  =>  exts [ ("uu", Fixed(117).normal()) ]);
502     test!(ls_un:   ls "un=38;5;118", exa ""  =>  exts [ ("un", Fixed(118).normal()) ]);
503     test!(ls_gu:   ls "gu=38;5;119", exa ""  =>  exts [ ("gu", Fixed(119).normal()) ]);
504     test!(ls_gn:   ls "gn=38;5;120", exa ""  =>  exts [ ("gn", Fixed(120).normal()) ]);
505 
506     // Just like all other keys:
507     test!(ls_txt:  ls "*.txt=31",          exa ""  =>  exts [ ("*.txt",      Red.normal())             ]);
508     test!(ls_mp3:  ls "*.mp3=38;5;135",    exa ""  =>  exts [ ("*.mp3",      Fixed(135).normal())      ]);
509     test!(ls_mak:  ls "Makefile=1;32;4",   exa ""  =>  exts [ ("Makefile",   Green.bold().underline()) ]);
510     test!(exa_txt: ls "", exa "*.zip=31"           =>  exts [ ("*.zip",      Red.normal())             ]);
511     test!(exa_mp3: ls "", exa "lev.*=38;5;153"     =>  exts [ ("lev.*",      Fixed(153).normal())      ]);
512     test!(exa_mak: ls "", exa "Cargo.toml=4;32;1"  =>  exts [ ("Cargo.toml", Green.bold().underline()) ]);
513 
514     // Testing whether a glob from EXA_COLORS overrides a glob from LS_COLORS
515     // can’t be tested here, because they’ll both be added to the same vec
516 
517     // Values get separated by colons:
518     test!(ls_multi:   ls "*.txt=31:*.rtf=32", exa ""  =>  exts [ ("*.txt", Red.normal()),   ("*.rtf", Green.normal()) ]);
519     test!(exa_multi:  ls "", exa "*.tmp=37:*.log=37"  =>  exts [ ("*.tmp", White.normal()), ("*.log", White.normal()) ]);
520 
521     test!(ls_five: ls "1*1=31:2*2=32:3*3=1;33:4*4=34;1:5*5=35;4", exa ""  =>  exts [
522         ("1*1", Red.normal()), ("2*2", Green.normal()), ("3*3", Yellow.bold()), ("4*4", Blue.bold()), ("5*5", Purple.underline())
523     ]);
524 
525     // Finally, colours get applied right-to-left:
526     test!(ls_overwrite:  ls "pi=31:pi=32:pi=33", exa ""  =>  colours c -> { c.filekinds.pipe = Yellow.normal(); });
527     test!(exa_overwrite: ls "", exa "da=36:da=35:da=34"  =>  colours c -> { c.date = Blue.normal(); });
528 }
529