Browse Source

Implement file name colouring in {exa,ls}_colors

This commit adds to the parsing of the LS_COLORS and EXA_COLORS variables so that non-two-letter codes (keys other than things like ‘di’ or ‘ln’ or ‘ex’) will be treated as file name globs, and get used to colour files accordingly.

Fixes #116 for good.
Benjamin Sago 8 years ago
parent
commit
dc45332d7b

+ 236 - 40
src/options/style.rs

@@ -1,9 +1,11 @@
-use style::Colours;
-use output::file_name::{FileStyle, Classify};
+use ansi_term::Style;
+use glob;
 
+use fs::File;
 use options::{flags, Vars, Misfire};
 use options::parser::MatchedFlags;
-
+use output::file_name::{FileStyle, Classify};
+use style::Colours;
 
 
 /// Under what circumstances we should display coloured, rather than plain,
@@ -82,12 +84,12 @@ impl Styles {
     where TW: Fn() -> Option<usize>, V: Vars {
         use self::TerminalColours::*;
         use info::filetype::FileExtensions;
-        use style::LSColors;
-        use options::vars;
         use output::file_name::NoFileColours;
 
         let classify = Classify::deduce(matches)?;
 
+        // Before we do anything else, figure out if we need to consider
+        // custom colours at all
         let tc = TerminalColours::deduce(matches)?;
         if tc == Never || (tc == Automatic && widther().is_none()) {
             return Ok(Styles {
@@ -96,33 +98,104 @@ impl Styles {
             });
         }
 
+        // Parse the environment variables into colours and extension mappings
         let scale = matches.has_where(|f| f.matches(&flags::COLOR_SCALE) || f.matches(&flags::COLOUR_SCALE))?;
         let mut colours = Colours::colourful(scale.is_some());
 
-        if let Some(lsc) = vars.get(vars::LS_COLORS) {
-            let lsc = lsc.to_string_lossy();
-            LSColors(lsc.as_ref()).each_pair(|pair| {
-                colours.set_ls(&pair);
-            });
-        }
+        let (exts, use_default_filetypes) = parse_color_vars(vars, &mut colours);
+
+        // Use between 0 and 2 file name highlighters
+        let exts = match (exts.is_non_empty(), use_default_filetypes) {
+            (false, false)  => Box::new(NoFileColours)           as Box<_>,
+            (false,  true)  => Box::new(FileExtensions)          as Box<_>,
+            ( true, false)  => Box::new(exts)                    as Box<_>,
+            ( true,  true)  => Box::new((exts, FileExtensions))  as Box<_>,
+        };
 
-        if let Some(exa) = vars.get(vars::EXA_COLORS) {
-            let exa = exa.to_string_lossy();
-            LSColors(exa.as_ref()).each_pair(|pair| {
-                colours.set_exa(&pair);
-            });
         let style = FileStyle { classify, exts };
         Ok(Styles { colours, style })
+    }
+}
+
+/// Parse the environment variables into LS_COLORS pairs, putting file glob
+/// colours into the `ExtensionMappings` that gets returned, and using the
+/// two-character UI codes to modify the mutable `Colours`.
+///
+/// Also returns if the EXA_COLORS variable should reset the existing file
+/// type mappings or not. The `reset` code needs to be the first one.
+fn parse_color_vars<V: Vars>(vars: &V, colours: &mut Colours) -> (ExtensionMappings, bool) {
+    use options::vars;
+    use style::LSColors;
+
+    let mut exts = ExtensionMappings::default();
+
+    if let Some(lsc) = vars.get(vars::LS_COLORS) {
+        let lsc = lsc.to_string_lossy();
+        LSColors(lsc.as_ref()).each_pair(|pair| {
+            if !colours.set_ls(&pair) {
+                match glob::Pattern::new(pair.key) {
+                    Ok(pat) => exts.add(pat, pair.to_style()),
+                    Err(e)  => warn!("Couldn't parse glob pattern {:?}: {}", pair.key, e),
+                }
+            }
+        });
+    }
+
+    let mut use_default_filetypes = true;
+
+    if let Some(exa) = vars.get(vars::EXA_COLORS) {
+        let exa = exa.to_string_lossy();
+
+        // Is this hacky? Yes.
+        if exa == "reset" || exa.starts_with("reset:") {
+            use_default_filetypes = false;
         }
 
-        let classify = Classify::deduce(matches)?;
-        let exts = if colours.colourful { Box::new(FileExtensions) as Box<_> }
-                                   else { Box::new(NoFileColours)  as Box<_> };
+        LSColors(exa.as_ref()).each_pair(|pair| {
+            if !colours.set_ls(&pair) && !colours.set_exa(&pair) {
+                match glob::Pattern::new(pair.key) {
+                    Ok(pat) => exts.add(pat, pair.to_style()),
+                    Err(e)  => warn!("Couldn't parse glob pattern {:?}: {}", pair.key, e),
+                }
+            };
+        });
+    }
+
+    (exts, use_default_filetypes)
+}
+
 
+#[derive(PartialEq, Debug, Default)]
+struct ExtensionMappings {
+    mappings: Vec<(glob::Pattern, Style)>
+}
+
+// Loop through backwards so that colours specified later in the list override
+// colours specified earlier, like we do with options and strict mode
+
+use output::file_name::FileColours;
+impl FileColours for ExtensionMappings {
+    fn colour_file(&self, file: &File) -> Option<Style> {
+        self.mappings
+            .iter()
+            .rev()
+            .find(|t| t.0.matches(&file.name))
+            .map (|t| t.1)
+    }
+}
+
+impl ExtensionMappings {
+    fn is_non_empty(&self) -> bool {
+        !self.mappings.is_empty()
+    }
+
+    fn add(&mut self, pattern: glob::Pattern, style: Style) {
+        self.mappings.push((pattern, style))
     }
 }
 
 
+
 impl Classify {
     fn deduce(matches: &MatchedFlags) -> Result<Classify, Misfire> {
         let flagged = matches.has(&flags::CLASSIFY)?;
@@ -219,7 +292,7 @@ mod colour_test {
         ($name:ident:  $inputs:expr, $widther:expr;  $stricts:expr => $result:expr) => {
             #[test]
             fn $name() {
-                for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| Style::deduce(mf, &None, &$widther).map(|s| s.colours)) {
+                for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| Styles::deduce(mf, &None, &$widther).map(|s| s.colours)) {
                     assert_eq!(result, $result);
                 }
             }
@@ -228,7 +301,7 @@ mod colour_test {
         ($name:ident:  $inputs:expr, $widther:expr;  $stricts:expr => err $result:expr) => {
             #[test]
             fn $name() {
-                for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| Style::deduce(mf, &None, &$widther).map(|s| s.colours)) {
+                for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| Styles::deduce(mf, &None, &$widther).map(|s| s.colours)) {
                     assert_eq!(result.unwrap_err(), $result);
                 }
             }
@@ -237,7 +310,7 @@ mod colour_test {
         ($name:ident:  $inputs:expr, $widther:expr;  $stricts:expr => like $pat:pat) => {
             #[test]
             fn $name() {
-                for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| Style::deduce(mf, &None, &$widther).map(|s| s.colours)) {
+                for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| Styles::deduce(mf, &None, &$widther).map(|s| s.colours)) {
                     println!("Testing {:?}", result);
                     match result {
                         $pat => assert!(true),
@@ -276,23 +349,57 @@ mod customs_test {
 
     use super::*;
     use options::Vars;
-    use options::test::parse_for_test;
-    use options::test::Strictnesses::Both;
 
     use ansi_term::Colour::*;
 
     macro_rules! test {
-        ($name:ident:  ls $ls:expr, exa $exa:expr  =>  $resulter:expr) => {
+        ($name:ident:  ls $ls:expr, exa $exa:expr  =>  colours $expected:ident -> $process_expected:expr) => {
             #[test]
+            #[allow(unused_mut)]
             fn $name() {
-                let mut c = Colours::colourful(false);
-                $resulter(&mut c);
+                let mut $expected = Colours::colourful(false);
+                $process_expected();
 
                 let vars = MockVars { ls: $ls, exa: $exa };
 
-                for result in parse_for_test(&[], &[], Both, |mf| Style::deduce(mf, &vars, || Some(80)).map(|s| s.colours)) {
-                    assert_eq!(result.as_ref(), Ok(&c));
-                }
+                let mut result = Colours::colourful(false);
+                let (_exts, _reset) = parse_color_vars(&vars, &mut result);
+                assert_eq!($expected, result);
+            }
+        };
+        ($name:ident:  ls $ls:expr, exa $exa:expr  =>  exts $mappings:expr) => {
+            #[test]
+            fn $name() {
+                let mappings: Vec<(glob::Pattern, Style)>
+                    = $mappings.into_iter()
+                               .map(|t| (glob::Pattern::new(t.0).unwrap(), t.1))
+                               .collect();
+
+                let vars = MockVars { ls: $ls, exa: $exa };
+
+                let mut meh = Colours::colourful(false);
+                let (result, _reset) = parse_color_vars(&vars, &mut meh);
+                assert_eq!(ExtensionMappings { mappings }, result);
+            }
+        };
+        ($name:ident:  ls $ls:expr, exa $exa:expr  =>  colours $expected:ident -> $process_expected:expr, exts $mappings:expr) => {
+            #[test]
+            #[allow(unused_mut)]
+            fn $name() {
+                let mut $expected = Colours::colourful(false);
+                $process_expected();
+
+                let mappings: Vec<(glob::Pattern, Style)>
+                    = $mappings.into_iter()
+                               .map(|t| (glob::Pattern::new(t.0).unwrap(), t.1))
+                               .collect();
+
+                let vars = MockVars { ls: $ls, exa: $exa };
+
+                let mut meh = Colours::colourful(false);
+                let (result, _reset) = parse_color_vars(&vars, &mut meh);
+                assert_eq!(ExtensionMappings { mappings }, result);
+                assert_eq!($expected, meh);
             }
         };
     }
@@ -319,14 +426,103 @@ mod customs_test {
         }
     }
 
-    test!(ls_di:  ls "di=31", exa ""  =>  |c: &mut Colours| { c.filekinds.directory    = Red.normal();    });  // Directory
-    test!(ls_ex:  ls "ex=32", exa ""  =>  |c: &mut Colours| { c.filekinds.executable   = Green.normal();  });  // Executable file
-    test!(ls_fi:  ls "fi=33", exa ""  =>  |c: &mut Colours| { c.filekinds.normal       = Yellow.normal(); });  // Regular file
-    test!(ls_pi:  ls "pi=34", exa ""  =>  |c: &mut Colours| { c.filekinds.pipe         = Blue.normal();   });  // FIFO
-    test!(ls_so:  ls "so=35", exa ""  =>  |c: &mut Colours| { c.filekinds.socket       = Purple.normal(); });  // Socket
-    test!(ls_bd:  ls "bd=36", exa ""  =>  |c: &mut Colours| { c.filekinds.block_device = Cyan.normal();   });  // Block device
-    test!(ls_cd:  ls "cd=35", exa ""  =>  |c: &mut Colours| { c.filekinds.char_device  = Purple.normal(); });  // Character device
-    test!(ls_ln:  ls "ln=34", exa ""  =>  |c: &mut Colours| { c.filekinds.symlink      = Blue.normal();   });  // Symlink
-    test!(ls_or:  ls "or=33", exa ""  =>  |c: &mut Colours| { c.broken_arrow           = Yellow.normal(); });  // Broken link
-    test!(ls_mi:  ls "mi=32", exa ""  =>  |c: &mut Colours| { c.broken_filename        = Green.normal();  });  // Broken link target
+    // LS_COLORS can affect all of these colours:
+    test!(ls_di:   ls "di=31", exa ""  =>  colours c -> { c.filekinds.directory    = Red.normal();    });
+    test!(ls_ex:   ls "ex=32", exa ""  =>  colours c -> { c.filekinds.executable   = Green.normal();  });
+    test!(ls_fi:   ls "fi=33", exa ""  =>  colours c -> { c.filekinds.normal       = Yellow.normal(); });
+    test!(ls_pi:   ls "pi=34", exa ""  =>  colours c -> { c.filekinds.pipe         = Blue.normal();   });
+    test!(ls_so:   ls "so=35", exa ""  =>  colours c -> { c.filekinds.socket       = Purple.normal(); });
+    test!(ls_bd:   ls "bd=36", exa ""  =>  colours c -> { c.filekinds.block_device = Cyan.normal();   });
+    test!(ls_cd:   ls "cd=35", exa ""  =>  colours c -> { c.filekinds.char_device  = Purple.normal(); });
+    test!(ls_ln:   ls "ln=34", exa ""  =>  colours c -> { c.filekinds.symlink      = Blue.normal();   });
+    test!(ls_or:   ls "or=33", exa ""  =>  colours c -> { c.broken_arrow           = Yellow.normal(); });
+    test!(ls_mi:   ls "mi=32", exa ""  =>  colours c -> { c.broken_filename        = Green.normal();  });
+
+    // EXA_COLORS can affect all those colours too:
+    test!(exa_di:  ls "", exa "di=32"  =>  colours c -> { c.filekinds.directory    = Green.normal();  });
+    test!(exa_ex:  ls "", exa "ex=33"  =>  colours c -> { c.filekinds.executable   = Yellow.normal(); });
+    test!(exa_fi:  ls "", exa "fi=34"  =>  colours c -> { c.filekinds.normal       = Blue.normal();   });
+    test!(exa_pi:  ls "", exa "pi=35"  =>  colours c -> { c.filekinds.pipe         = Purple.normal(); });
+    test!(exa_so:  ls "", exa "so=36"  =>  colours c -> { c.filekinds.socket       = Cyan.normal();   });
+    test!(exa_bd:  ls "", exa "bd=35"  =>  colours c -> { c.filekinds.block_device = Purple.normal(); });
+    test!(exa_cd:  ls "", exa "cd=34"  =>  colours c -> { c.filekinds.char_device  = Blue.normal();   });
+    test!(exa_ln:  ls "", exa "ln=33"  =>  colours c -> { c.filekinds.symlink      = Yellow.normal(); });
+    test!(exa_or:  ls "", exa "or=32"  =>  colours c -> { c.broken_arrow           = Green.normal();  });
+    test!(exa_mi:  ls "", exa "mi=31"  =>  colours c -> { c.broken_filename        = Red.normal();    });
+
+    // EXA_COLORS will even override options from LS_COLORS:
+    test!(ls_exa_di: ls "di=31", exa "di=32"  =>  colours c -> { c.filekinds.directory  = Green.normal();  });
+    test!(ls_exa_ex: ls "ex=32", exa "ex=33"  =>  colours c -> { c.filekinds.executable = Yellow.normal(); });
+    test!(ls_exa_fi: ls "fi=33", exa "fi=34"  =>  colours c -> { c.filekinds.normal     = Blue.normal();   });
+
+    // But more importantly, EXA_COLORS has its own, special list of colours:
+    test!(exa_ur:  ls "", exa "ur=38;5;100"  =>  colours c -> { c.perms.user_read           = Fixed(100).normal(); });
+    test!(exa_uw:  ls "", exa "uw=38;5;101"  =>  colours c -> { c.perms.user_write          = Fixed(101).normal(); });
+    test!(exa_ux:  ls "", exa "ux=38;5;102"  =>  colours c -> { c.perms.user_execute_file   = Fixed(102).normal(); });
+    test!(exa_ue:  ls "", exa "ue=38;5;103"  =>  colours c -> { c.perms.user_execute_other  = Fixed(103).normal(); });
+    test!(exa_gr:  ls "", exa "gr=38;5;104"  =>  colours c -> { c.perms.group_read          = Fixed(104).normal(); });
+    test!(exa_gw:  ls "", exa "gw=38;5;105"  =>  colours c -> { c.perms.group_write         = Fixed(105).normal(); });
+    test!(exa_gx:  ls "", exa "gx=38;5;106"  =>  colours c -> { c.perms.group_execute       = Fixed(106).normal(); });
+    test!(exa_tr:  ls "", exa "tr=38;5;107"  =>  colours c -> { c.perms.other_read          = Fixed(107).normal(); });
+    test!(exa_tw:  ls "", exa "tw=38;5;108"  =>  colours c -> { c.perms.other_write         = Fixed(108).normal(); });
+    test!(exa_tx:  ls "", exa "tx=38;5;109"  =>  colours c -> { c.perms.other_execute       = Fixed(109).normal(); });
+    test!(exa_su:  ls "", exa "su=38;5;110"  =>  colours c -> { c.perms.special_user_file   = Fixed(110).normal(); });
+    test!(exa_sf:  ls "", exa "sf=38;5;111"  =>  colours c -> { c.perms.special_other       = Fixed(111).normal(); });
+    test!(exa_xa:  ls "", exa "xa=38;5;112"  =>  colours c -> { c.perms.attribute           = Fixed(112).normal(); });
+
+    test!(exa_sn:  ls "", exa "sn=38;5;113"  =>  colours c -> { c.size.numbers              = Fixed(113).normal(); });
+    test!(exa_sb:  ls "", exa "sb=38;5;114"  =>  colours c -> { c.size.unit                 = Fixed(114).normal(); });
+    test!(exa_df:  ls "", exa "df=38;5;115"  =>  colours c -> { c.size.major                = Fixed(115).normal(); });
+    test!(exa_ds:  ls "", exa "ds=38;5;116"  =>  colours c -> { c.size.minor                = Fixed(116).normal(); });
+
+    test!(exa_uu:  ls "", exa "uu=38;5;117"  =>  colours c -> { c.users.user_you            = Fixed(117).normal(); });
+    test!(exa_un:  ls "", exa "un=38;5;118"  =>  colours c -> { c.users.user_someone_else   = Fixed(118).normal(); });
+    test!(exa_gu:  ls "", exa "gu=38;5;119"  =>  colours c -> { c.users.group_yours         = Fixed(119).normal(); });
+    test!(exa_gn:  ls "", exa "gn=38;5;120"  =>  colours c -> { c.users.group_not_yours     = Fixed(120).normal(); });
+
+    test!(exa_lc:  ls "", exa "lc=38;5;121"  =>  colours c -> { c.links.normal              = Fixed(121).normal(); });
+    test!(exa_lm:  ls "", exa "lm=38;5;122"  =>  colours c -> { c.links.multi_link_file     = Fixed(122).normal(); });
+
+    test!(exa_ga:  ls "", exa "ga=38;5;123"  =>  colours c -> { c.git.new                   = Fixed(123).normal(); });
+    test!(exa_gm:  ls "", exa "gm=38;5;124"  =>  colours c -> { c.git.modified              = Fixed(124).normal(); });
+    test!(exa_gd:  ls "", exa "gd=38;5;125"  =>  colours c -> { c.git.deleted               = Fixed(125).normal(); });
+    test!(exa_gv:  ls "", exa "gv=38;5;126"  =>  colours c -> { c.git.renamed               = Fixed(126).normal(); });
+    test!(exa_gt:  ls "", exa "gt=38;5;127"  =>  colours c -> { c.git.typechange            = Fixed(127).normal(); });
+
+    test!(exa_xx:  ls "", exa "xx=38;5;128"  =>  colours c -> { c.punctuation               = Fixed(128).normal(); });
+    test!(exa_da:  ls "", exa "da=38;5;129"  =>  colours c -> { c.date                      = Fixed(129).normal(); });
+    test!(exa_in:  ls "", exa "in=38;5;130"  =>  colours c -> { c.inode                     = Fixed(130).normal(); });
+    test!(exa_bl:  ls "", exa "bl=38;5;131"  =>  colours c -> { c.blocks                    = Fixed(131).normal(); });
+    test!(exa_hd:  ls "", exa "hd=38;5;132"  =>  colours c -> { c.header                    = Fixed(132).normal(); });
+    test!(exa_lp:  ls "", exa "lp=38;5;133"  =>  colours c -> { c.symlink_path              = Fixed(133).normal(); });
+    test!(exa_cc:  ls "", exa "cc=38;5;134"  =>  colours c -> { c.control_char              = Fixed(134).normal(); });
+
+    // All the while, LS_COLORS treats them as filenames:
+    test!(ls_uu:   ls "uu=38;5;117", exa ""  =>  exts [ ("uu", Fixed(117).normal()) ]);
+    test!(ls_un:   ls "un=38;5;118", exa ""  =>  exts [ ("un", Fixed(118).normal()) ]);
+    test!(ls_gu:   ls "gu=38;5;119", exa ""  =>  exts [ ("gu", Fixed(119).normal()) ]);
+    test!(ls_gn:   ls "gn=38;5;120", exa ""  =>  exts [ ("gn", Fixed(120).normal()) ]);
+
+    // Just like all other keys:
+    test!(ls_txt:  ls "*.txt=31",          exa ""  =>  exts [ ("*.txt",      Red.normal())             ]);
+    test!(ls_mp3:  ls "*.mp3=38;5;135",    exa ""  =>  exts [ ("*.mp3",      Fixed(135).normal())      ]);
+    test!(ls_mak:  ls "Makefile=1;32;4",   exa ""  =>  exts [ ("Makefile",   Green.bold().underline()) ]);
+    test!(exa_txt: ls "", exa "*.zip=31"           =>  exts [ ("*.zip",      Red.normal())             ]);
+    test!(exa_mp3: ls "", exa "lev.*=38;5;153"     =>  exts [ ("lev.*",      Fixed(153).normal())      ]);
+    test!(exa_mak: ls "", exa "Cargo.toml=4;32;1"  =>  exts [ ("Cargo.toml", Green.bold().underline()) ]);
+
+    // Testing whether a glob from EXA_COLORS overrides a glob from LS_COLORS
+    // can’t be tested here, because they’ll both be added to the same vec
+
+    // Values get separated by colons:
+    test!(ls_multi:   ls "*.txt=31:*.rtf=32", exa ""  =>  exts [ ("*.txt", Red.normal()),   ("*.rtf", Green.normal()) ]);
+    test!(exa_multi:  ls "", exa "*.tmp=37:*.log=37"  =>  exts [ ("*.tmp", White.normal()), ("*.log", White.normal()) ]);
+
+    test!(ls_five: ls "1*1=31:2*2=32:3*3=1;33:4*4=34;1:5*5=35;4", exa ""  =>  exts [
+        ("1*1", Red.normal()), ("2*2", Green.normal()), ("3*3", Yellow.bold()), ("4*4", Blue.bold()), ("5*5", Purple.underline())
+    ]);
+
+    // Finally, colours get applied right-to-left:
+    test!(ls_overwrite:  ls "pi=31:pi=32:pi=33", exa ""  =>  colours c -> { c.filekinds.pipe = Yellow.normal(); });
+    test!(exa_overwrite: ls "", exa "da=36:da=35:da=34"  =>  colours c -> { c.date = Blue.normal(); });
 }

+ 10 - 0
src/output/file_name.rs

@@ -285,3 +285,13 @@ impl FileColours for NoFileColours {
     fn colour_file(&self, _file: &File) -> Option<Style> { None }
 }
 
+// When getting the colour of a file from a *pair* of colourisers, try the
+// first one then try the second one. This lets the user provide their own
+// file type associations, while falling back to the default set if not set
+// explicitly.
+impl<A, B> FileColours for (A, B)
+where A: FileColours, B: FileColours {
+    fn colour_file(&self, file: &File) -> Option<Style> {
+        self.0.colour_file(file).or_else(|| self.1.colour_file(file))
+    }
+}

+ 38 - 17
xtests/run.sh

@@ -158,17 +158,17 @@ env LANG=ja_JP.UTF-8  $exa $testcases/dates -l | diff -q - $results/dates_jp  ||
 # These directories are created in the VM user’s home directory (the default
 # location) when a Cargo build is done.
 (cd; mkdir -p target/debug/build
-     $exa -1d target target/debug target/debug/build | diff -q - $results/dir_paths) || exit 1
-     $exa -1d . .. /                                 | diff -q - $results/dirs       || exit 1
+     $exa -1d target target/debug target/debug/build | diff -q - $results/dir_paths)  || exit 1
+     $exa -1d . .. /                                 | diff -q - $results/dirs        || exit 1
 
 
 # Links
-COLUMNS=80 $exa $testcases/links     2>&1 | diff -q - $results/links         || exit 1
-           $exa $testcases/links -1  2>&1 | diff -q - $results/links_1       || exit 1
-           $exa $testcases/links -T  2>&1 | diff -q - $results/links_T       || exit 1
-           $exa $testcases/links -T@ 2>&1 | diff -q - $results/links_T@      || exit 1
-           $exa /proc/1/root     -T  2>&1 | diff -q - $results/proc_1_root   || exit 1
-           $exa /proc/1/root     -T@ 2>&1 | diff -q - $results/proc_1_root_@ || exit 1
+COLUMNS=80 $exa $testcases/links     2>&1 | diff -q - $results/links          || exit 1
+           $exa $testcases/links -1  2>&1 | diff -q - $results/links_1        || exit 1
+           $exa $testcases/links -T  2>&1 | diff -q - $results/links_T        || exit 1
+           $exa $testcases/links -T@ 2>&1 | diff -q - $results/links_T@       || exit 1
+           $exa /proc/1/root     -T  2>&1 | diff -q - $results/proc_1_root    || exit 1
+           $exa /proc/1/root     -T@ 2>&1 | diff -q - $results/proc_1_root_@  || exit 1
 
 # There’ve been bugs where the target file wasn’t printed properly when the
 # symlink file was specified on the command-line directly.
@@ -230,14 +230,35 @@ $exa $testcases/hiddens -l -a  2>&1 | diff -q - $results/hiddens_la   || exit 1
 $exa $testcases/hiddens -l -aa 2>&1 | diff -q - $results/hiddens_laa  || exit 1
 
 
+# Themes
+LS_COLORS="bd=31:cd=32:pi=34"  $exa -1 $testcases/specials 2>&1 | diff -q - $results/themed_specials  || exit 1
+EXA_COLORS="bd=31:cd=32:pi=34" $exa -1 $testcases/specials 2>&1 | diff -q - $results/themed_specials  || exit 1
+
+             LS_COLORS="*.deb=1;37:*.tar.*=1;37" $exa -1 $testcases/file-names-exts/compressed.* 2>&1 | diff -q - $results/themed_compresseds  || exit 1
+EXA_COLORS="*.deb=1;37:*.tar.*=1;37"             $exa -1 $testcases/file-names-exts/compressed.* 2>&1 | diff -q - $results/themed_compresseds  || exit 1
+EXA_COLORS="*.deb=1;37" LS_COLORS="*.tar.*=1;37" $exa -1 $testcases/file-names-exts/compressed.* 2>&1 | diff -q - $results/themed_compresseds  || exit 1
+
+ LS_COLORS="reset:*.deb=1;37:*.tar.*=1;37"       $exa -1 $testcases/file-names-exts/compressed.* 2>&1 | diff -q - $results/themed_compresseds    || exit 1
+EXA_COLORS="reset:*.deb=1;37:*.tar.*=1;37"       $exa -1 $testcases/file-names-exts/compressed.* 2>&1 | diff -q - $results/themed_compresseds_r  || exit 1
+
+EXA_COLORS="or=32:mi=32;1;4:cc=34;1:ln=34:lp=36;4:xx=32" $exa -1 $testcases/file-names/links 2>&1     | diff -q - $results/themed_links  || exit 1
+
+# EXA_COLORS overrides LS_COLORS
+LS_COLORS="bd=32:cd=34:pi=31" EXA_COLORS="bd=31:cd=32:pi=34" $exa -1 $testcases/specials 2>&1 | diff -q - $results/themed_specials  || exit 1
+
+EXA_COLORS="di=38;5;195:fi=38;5;250:xx=38;5;237:ur=38;5;194:uw=38;5;193:ux=38;5;192:gr=38;5;191:gw=38;5;190:gx=38;5;118:tr=38;5;119:tw=38;5;120:tx=38;5;121:su=38;5;51:sf=38;5;50:sn=38;5;49:un=38;5;46:da=38;5;47:ex=38;5;48" \
+    $exa --long $testcases/permissions 2>&1 | diff -q - $results/themed_long  || exit 1
+
+EXA_COLORS="reset" $exa $testcases/file-names-exts -1  2>&1 | diff -q - $results/themed_un  || exit 1
+
 # Errors
-$exa --binary     2>&1 | diff -q - $results/error_useless    || exit 1
-$exa --ternary    2>&1 | diff -q - $results/error_long       || exit 1
-$exa -4           2>&1 | diff -q - $results/error_short      || exit 1
-$exa --time       2>&1 | diff -q - $results/error_value      || exit 1
-$exa --long=time  2>&1 | diff -q - $results/error_overvalued || exit 1
-$exa -l --long    2>&1 | diff -q - $results/error_duplicate  || exit 1
-$exa -ll          2>&1 | diff -q - $results/error_twice      || exit 1
+$exa --binary     2>&1 | diff -q - $results/error_useless     || exit 1
+$exa --ternary    2>&1 | diff -q - $results/error_long        || exit 1
+$exa -4           2>&1 | diff -q - $results/error_short       || exit 1
+$exa --time       2>&1 | diff -q - $results/error_value       || exit 1
+$exa --long=time  2>&1 | diff -q - $results/error_overvalued  || exit 1
+$exa -l --long    2>&1 | diff -q - $results/error_duplicate   || exit 1
+$exa -ll          2>&1 | diff -q - $results/error_twice       || exit 1
 
 
 # Debug mode
@@ -246,8 +267,8 @@ EXA_DEBUG="1" $exa $testcases/attributes/dirs/no-xattrs_empty -lh 2>&1 | tail -n
 
 
 # And finally...
-$exa --help        | diff -q - $results/help      || exit 1
-$exa --help --long | diff -q - $results/help_long || exit 1
+$exa --help        | diff -q - $results/help       || exit 1
+$exa --help --long | diff -q - $results/help_long  || exit 1
 
 
 echo "All the tests passed!"

+ 5 - 0
xtests/themed_compresseds

@@ -0,0 +1,5 @@
+/testcases/file-names-exts/compressed.deb
+/testcases/file-names-exts/compressed.tar.gz
+/testcases/file-names-exts/compressed.tar.xz
+/testcases/file-names-exts/compressed.tgz
+/testcases/file-names-exts/compressed.txz

+ 5 - 0
xtests/themed_compresseds_r

@@ -0,0 +1,5 @@
+/testcases/file-names-exts/compressed.deb
+/testcases/file-names-exts/compressed.tar.gz
+/testcases/file-names-exts/compressed.tar.xz
+/testcases/file-names-exts/compressed.tgz
+/testcases/file-names-exts/compressed.txz

+ 3 - 0
xtests/themed_links

@@ -0,0 +1,3 @@
+another: [\n] -> /testcases/file-names/new-line-dir: [\n]/another: [\n]
+broken -> /testcases/file-names/new-line-dir: [\n]/broken
+subfile -> /testcases/file-names/new-line-dir: [\n]/subfile

+ 22 - 0
xtests/themed_long

@@ -0,0 +1,22 @@
+.--------- 0 cassowary  1 Jan 12:34 000
+.--------x 0 cassowary  1 Jan 12:34 001
+.-------w- 0 cassowary  1 Jan 12:34 002
+.------r-- 0 cassowary  1 Jan 12:34 004
+.-----x--- 0 cassowary  1 Jan 12:34 010
+.----w---- 0 cassowary  1 Jan 12:34 020
+.---r----- 0 cassowary  1 Jan 12:34 040
+.--x------ 0 cassowary  1 Jan 12:34 100
+.-w------- 0 cassowary  1 Jan 12:34 200
+.r-------- 0 cassowary  1 Jan 12:34 400
+.rw-r--r-- 0 cassowary  1 Jan 12:34 644
+.rwxr-xr-x 0 cassowary  1 Jan 12:34 755
+.rwxrwxrwx 0 cassowary  1 Jan 12:34 777
+.--------T 0 cassowary  1 Jan 12:34 1000
+.--------t 0 cassowary  1 Jan 12:34 1001
+.-----S--- 0 cassowary  1 Jan 12:34 2000
+.-----s--- 0 cassowary  1 Jan 12:34 2010
+.--S------ 0 cassowary  1 Jan 12:34 4000
+.--s------ 0 cassowary  1 Jan 12:34 4100
+.rwSrwSrwT 0 cassowary  1 Jan 12:34 7666
+.rwsrwsrwt 0 cassowary  1 Jan 12:34 7777
+d--------- - cassowary  1 Jan 12:34 forbidden-directory

+ 3 - 0
xtests/themed_specials

@@ -0,0 +1,3 @@
+block-device
+char-device
+named-pipe

+ 26 - 0
xtests/themed_un

@@ -0,0 +1,26 @@
+#SAVEFILE#
+backup~
+compiled.class
+compiled.coffee
+compiled.js
+compiled.o
+compressed.deb
+compressed.tar.gz
+compressed.tar.xz
+compressed.tgz
+compressed.txz
+COMPRESSED.ZIP
+crypto.asc
+crypto.signature
+document.pdf
+DOCUMENT.XLSX
+file.tmp
+IMAGE.PNG
+image.svg
+lossless.flac
+lossless.wav
+Makefile
+music.mp3
+MUSIC.OGG
+VIDEO.AVI
+video.wmv