Browse Source

Merge branch 'ls-colors-finally'

Benjamin Sago 8 years ago
parent
commit
aa2e3a5d9e

+ 28 - 28
Cargo.lock

@@ -6,10 +6,10 @@ dependencies = [
  "datetime 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
  "env_logger 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
  "getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)",
- "git2 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "git2 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
  "lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.30 (registry+https://github.com/rust-lang/crates.io-index)",
  "locale 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "natord 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -53,10 +53,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "cmake"
-version = "0.1.24"
+version = "0.1.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "gcc 0.3.51 (registry+https://github.com/rust-lang/crates.io-index)",
+ "gcc 0.3.53 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -78,7 +78,7 @@ version = "0.4.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "iso8601 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.30 (registry+https://github.com/rust-lang/crates.io-index)",
  "locale 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "num 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
  "pad 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -95,7 +95,7 @@ dependencies = [
 
 [[package]]
 name = "gcc"
-version = "0.3.51"
+version = "0.3.53"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
@@ -105,12 +105,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "git2"
-version = "0.6.6"
+version = "0.6.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)",
- "libgit2-sys 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.30 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libgit2-sys 0.6.14 (registry+https://github.com/rust-lang/crates.io-index)",
  "url 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
@@ -153,17 +153,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "libc"
-version = "0.2.29"
+version = "0.2.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "libgit2-sys"
-version = "0.6.13"
+version = "0.6.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "cmake 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)",
- "gcc 0.3.51 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cmake 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
+ "gcc 0.3.53 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.30 (registry+https://github.com/rust-lang/crates.io-index)",
  "libz-sys 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)",
  "pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
@@ -173,8 +173,8 @@ name = "libz-sys"
 version = "1.0.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "gcc 0.3.51 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)",
+ "gcc 0.3.53 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.30 (registry+https://github.com/rust-lang/crates.io-index)",
  "pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
  "vcpkg 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
@@ -184,7 +184,7 @@ name = "locale"
 version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "libc 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.30 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -219,7 +219,7 @@ name = "memchr"
 version = "0.1.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "libc 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.30 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -303,7 +303,7 @@ name = "num_cpus"
 version = "1.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "libc 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.30 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -337,7 +337,7 @@ name = "rand"
 version = "0.3.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "libc 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.30 (registry+https://github.com/rust-lang/crates.io-index)",
  "magenta 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
@@ -382,7 +382,7 @@ version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.30 (registry+https://github.com/rust-lang/crates.io-index)",
  "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
@@ -392,7 +392,7 @@ version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.30 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -436,7 +436,7 @@ name = "users"
 version = "0.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "libc 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.30 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -474,21 +474,21 @@ dependencies = [
 "checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d"
 "checksum bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5"
 "checksum byteorder 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "96c8b41881888cc08af32d47ac4edd52bc7fa27fef774be47a92443756451304"
-"checksum cmake 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)" = "b8ebbb35d3dc9cd09497168f33de1acb79b265d350ab0ac34133b98f8509af1f"
+"checksum cmake 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)" = "0c8a6541a55bcd72d3de4faee2d101a5a66df29790282c7f797082a7228a9b3d"
 "checksum conv 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "78ff10625fd0ac447827aa30ea8b861fead473bb60aeb73af6c1c58caf0d1299"
 "checksum custom_derive 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "ef8ae57c4978a2acd8b869ce6b9ca1dfe817bff704c220209fdef2c0b75a01b9"
 "checksum datetime 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "2d425bf1f6bbd57cf833081c1e60ac294fd74e7edd66acc91c3fca2e496bcee9"
 "checksum env_logger 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "15abd780e45b3ea4f76b4e9a26ff4843258dd8a3eed2775a0e7368c2e7936c2f"
-"checksum gcc 0.3.51 (registry+https://github.com/rust-lang/crates.io-index)" = "120d07f202dcc3f72859422563522b66fe6463a4c513df062874daad05f85f0a"
+"checksum gcc 0.3.53 (registry+https://github.com/rust-lang/crates.io-index)" = "e8310f7e9c890398b0e80e301c4f474e9918d2b27fca8f48486ca775fa9ffc5a"
 "checksum getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9047cfbd08a437050b363d35ef160452c5fe8ea5187ae0a624708c91581d685"
-"checksum git2 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)" = "aa01936ac96555c083c0e8553f672616274408d9d3fc5b8696603fbf63ff43ee"
+"checksum git2 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)" = "0c1c0203d653f4140241da0c1375a404f0a397249ec818cd2076c6280c50f6fa"
 "checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb"
 "checksum idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "014b298351066f1512874135335d62a789ffe78a9974f94b43ed5621951eaf7d"
 "checksum iso8601 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "11dc464f8c6f17595d191447c9c6559298b2d023d6f846a4a23ac7ea3c46c477"
 "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
 "checksum lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "3b37545ab726dd833ec6420aaba8231c5b320814b9029ad585555d2a03e94fbf"
-"checksum libc 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)" = "8a014d9226c2cc402676fbe9ea2e15dd5222cd1dd57f576b5b283178c944a264"
-"checksum libgit2-sys 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "0f1641ccb55181967a3e5ee4ae2911c0563492f016383ea67a27886181de088c"
+"checksum libc 0.2.30 (registry+https://github.com/rust-lang/crates.io-index)" = "2370ca07ec338939e356443dac2296f581453c35fe1e3a3ed06023c49435f915"
+"checksum libgit2-sys 0.6.14 (registry+https://github.com/rust-lang/crates.io-index)" = "c00f6e5bc3fb2b5f87e75e8d0fd4ae6720d55f3ee23d389b7c6cae30f8db8db1"
 "checksum libz-sys 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)" = "3fdd64ef8ee652185674455c1d450b83cbc8ad895625d543b5324d923f82e4d8"
 "checksum locale 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5fdbe492a9c0238da900a1165c42fc5067161ce292678a6fe80921f30fe307fd"
 "checksum log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "880f77541efa6e5cc74e76910c9884d9859683118839d6a1dc3b11e63512565b"

+ 0 - 287
src/options/colours.rs

@@ -1,287 +0,0 @@
-use style::Colours;
-
-use options::{flags, Vars, Misfire};
-use options::parser::MatchedFlags;
-
-
-
-/// Under what circumstances we should display coloured, rather than plain,
-/// output to the terminal.
-///
-/// By default, we want to display the colours when stdout can display them.
-/// Turning them on when output is going to, say, a pipe, would make programs
-/// such as `grep` or `more` not work properly. So the `Automatic` mode does
-/// this check and only displays colours when they can be truly appreciated.
-#[derive(PartialEq, Debug)]
-enum TerminalColours {
-
-    /// Display them even when output isn’t going to a terminal.
-    Always,
-
-    /// Display them when output is going to a terminal, but not otherwise.
-    Automatic,
-
-    /// Never display them, even when output is going to a terminal.
-    Never,
-}
-
-impl Default for TerminalColours {
-    fn default() -> TerminalColours {
-        TerminalColours::Automatic
-    }
-}
-
-const COLOURS: &[&str] = &["always", "auto", "never"];
-
-impl TerminalColours {
-
-    /// Determine which terminal colour conditions to use.
-    fn deduce(matches: &MatchedFlags) -> Result<TerminalColours, Misfire> {
-
-        let word = match matches.get_where(|f| f.matches(&flags::COLOR) || f.matches(&flags::COLOUR))? {
-            Some(w) => w,
-            None    => return Ok(TerminalColours::default()),
-        };
-
-        if word == "always" {
-            Ok(TerminalColours::Always)
-        }
-        else if word == "auto" || word == "automatic" {
-            Ok(TerminalColours::Automatic)
-        }
-        else if word == "never" {
-            Ok(TerminalColours::Never)
-        }
-        else {
-            Err(Misfire::bad_argument(&flags::COLOR, word, COLOURS))
-        }
-    }
-}
-
-
-impl Colours {
-    pub fn deduce<V, TW>(matches: &MatchedFlags, vars: &V, widther: TW) -> Result<Colours, Misfire>
-    where TW: Fn() -> Option<usize>, V: Vars {
-        use self::TerminalColours::*;
-        use style::LSColors;
-        use options::vars;
-
-        let tc = TerminalColours::deduce(matches)?;
-        if tc == Never || (tc == Automatic && widther().is_none()) {
-            return Ok(Colours::plain());
-        }
-
-        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));
-        }
-
-        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));
-        }
-
-        Ok(colours)
-    }
-}
-
-
-#[cfg(test)]
-mod terminal_test {
-    use super::*;
-    use std::ffi::OsString;
-    use options::flags;
-    use options::parser::{Flag, Arg};
-
-    use options::test::parse_for_test;
-    use options::test::Strictnesses::*;
-
-    pub fn os(input: &'static str) -> OsString {
-        let mut os = OsString::new();
-        os.push(input);
-        os
-    }
-
-    static TEST_ARGS: &[&Arg] = &[ &flags::COLOR, &flags::COLOUR ];
-
-    macro_rules! test {
-        ($name:ident:  $inputs:expr;  $stricts:expr => $result:expr) => {
-            #[test]
-            fn $name() {
-                for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| TerminalColours::deduce(mf)) {
-                    assert_eq!(result, $result);
-                }
-            }
-        };
-
-        ($name:ident:  $inputs:expr;  $stricts:expr => err $result:expr) => {
-            #[test]
-            fn $name() {
-                for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| TerminalColours::deduce(mf)) {
-                    assert_eq!(result.unwrap_err(), $result);
-                }
-            }
-        };
-    }
-
-
-    // Default
-    test!(empty:         [];                     Both => Ok(TerminalColours::default()));
-
-    // --colour
-    test!(u_always:      ["--colour=always"];    Both => Ok(TerminalColours::Always));
-    test!(u_auto:        ["--colour", "auto"];   Both => Ok(TerminalColours::Automatic));
-    test!(u_never:       ["--colour=never"];     Both => Ok(TerminalColours::Never));
-
-    // --color
-    test!(no_u_always:   ["--color", "always"];  Both => Ok(TerminalColours::Always));
-    test!(no_u_auto:     ["--color=auto"];       Both => Ok(TerminalColours::Automatic));
-    test!(no_u_never:    ["--color", "never"];   Both => Ok(TerminalColours::Never));
-
-    // Errors
-    test!(no_u_error:    ["--color=upstream"];   Both => err Misfire::bad_argument(&flags::COLOR, &os("upstream"), super::COLOURS));  // the error is for --color
-    test!(u_error:       ["--colour=lovers"];    Both => err Misfire::bad_argument(&flags::COLOR, &os("lovers"),   super::COLOURS));  // and so is this one!
-
-    // Overriding
-    test!(overridden_1:  ["--colour=auto", "--colour=never"];  Last => Ok(TerminalColours::Never));
-    test!(overridden_2:  ["--color=auto",  "--colour=never"];  Last => Ok(TerminalColours::Never));
-    test!(overridden_3:  ["--colour=auto", "--color=never"];   Last => Ok(TerminalColours::Never));
-    test!(overridden_4:  ["--color=auto",  "--color=never"];   Last => Ok(TerminalColours::Never));
-
-    test!(overridden_5:  ["--colour=auto", "--colour=never"];  Complain => err Misfire::Duplicate(Flag::Long("colour"), Flag::Long("colour")));
-    test!(overridden_6:  ["--color=auto",  "--colour=never"];  Complain => err Misfire::Duplicate(Flag::Long("color"),  Flag::Long("colour")));
-    test!(overridden_7:  ["--colour=auto", "--color=never"];   Complain => err Misfire::Duplicate(Flag::Long("colour"), Flag::Long("color")));
-    test!(overridden_8:  ["--color=auto",  "--color=never"];   Complain => err Misfire::Duplicate(Flag::Long("color"),  Flag::Long("color")));
-}
-
-
-#[cfg(test)]
-mod colour_test {
-    use super::*;
-    use options::flags;
-    use options::parser::{Flag, Arg};
-
-    use options::test::parse_for_test;
-    use options::test::Strictnesses::*;
-
-    static TEST_ARGS: &[&Arg] = &[ &flags::COLOR,       &flags::COLOUR,
-                                   &flags::COLOR_SCALE, &flags::COLOUR_SCALE ];
-
-    macro_rules! 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| Colours::deduce(mf, &None, &$widther)) {
-                    assert_eq!(result, $result);
-                }
-            }
-        };
-
-        ($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| Colours::deduce(mf, &None, &$widther)) {
-                    assert_eq!(result.unwrap_err(), $result);
-                }
-            }
-        };
-
-        ($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| Colours::deduce(mf, &None, &$widther)) {
-                    println!("Testing {:?}", result);
-                    match result {
-                        $pat => assert!(true),
-                        _    => assert!(false),
-                    }
-                }
-            }
-        };
-    }
-
-    test!(width_1:  ["--colour", "always"],    || Some(80);  Both => Ok(Colours::colourful(false)));
-    test!(width_2:  ["--colour", "always"],    || None;      Both => Ok(Colours::colourful(false)));
-    test!(width_3:  ["--colour", "never"],     || Some(80);  Both => Ok(Colours::plain()));
-    test!(width_4:  ["--colour", "never"],     || None;      Both => Ok(Colours::plain()));
-    test!(width_5:  ["--colour", "automatic"], || Some(80);  Both => Ok(Colours::colourful(false)));
-    test!(width_6:  ["--colour", "automatic"], || None;      Both => Ok(Colours::plain()));
-    test!(width_7:  [],                        || Some(80);  Both => Ok(Colours::colourful(false)));
-    test!(width_8:  [],                        || None;      Both => Ok(Colours::plain()));
-
-    test!(scale_1:  ["--color=always", "--color-scale", "--colour-scale"], || None;   Last => like Ok(Colours { scale: true,  .. }));
-    test!(scale_2:  ["--color=always", "--color-scale",                 ], || None;   Last => like Ok(Colours { scale: true,  .. }));
-    test!(scale_3:  ["--color=always",                  "--colour-scale"], || None;   Last => like Ok(Colours { scale: true,  .. }));
-    test!(scale_4:  ["--color=always",                                  ], || None;   Last => like Ok(Colours { scale: false, .. }));
-
-    test!(scale_5:  ["--color=always", "--color-scale", "--colour-scale"], || None;   Complain => err Misfire::Duplicate(Flag::Long("color-scale"),  Flag::Long("colour-scale")));
-    test!(scale_6:  ["--color=always", "--color-scale",                 ], || None;   Complain => like Ok(Colours { scale: true,  .. }));
-    test!(scale_7:  ["--color=always",                  "--colour-scale"], || None;   Complain => like Ok(Colours { scale: true,  .. }));
-    test!(scale_8:  ["--color=always",                                  ], || None;   Complain => like Ok(Colours { scale: false, .. }));
-}
-
-
-
-#[cfg(test)]
-mod customs_test {
-    use std::ffi::OsString;
-
-    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) => {
-            #[test]
-            fn $name() {
-                let mut c = Colours::colourful(false);
-                $resulter(&mut c);
-
-                let vars = MockVars { ls: $ls, exa: $exa };
-
-                for result in parse_for_test(&[], &[], Both, |mf| Colours::deduce(mf, &vars, || Some(80))) {
-                    assert_eq!(result.as_ref(), Ok(&c));
-                }
-            }
-        };
-    }
-
-    struct MockVars {
-        ls: &'static str,
-        exa: &'static str,
-    }
-
-    // Test impl that just returns the value it has.
-    impl Vars for MockVars {
-        fn get(&self, name: &'static str) -> Option<OsString> {
-            use options::vars;
-
-            if name == vars::LS_COLORS && !self.ls.is_empty() {
-                OsString::from(self.ls.clone()).into()
-            }
-            else if name == vars::EXA_COLORS && !self.exa.is_empty() {
-                OsString::from(self.exa.clone()).into()
-            }
-            else {
-                None
-            }
-        }
-    }
-
-    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
-}

+ 2 - 2
src/options/mod.rs

@@ -64,7 +64,7 @@
 //! if no aliases are being used!
 //!
 //! Finally, this isn’t just useful when options could override each other.
-//! Creating an alias `exal=”exa --long --inode --header”` then invoking `exal
+//! Creating an alias `exal="exa --long --inode --header"` then invoking `exal
 //! --grid --long` shouldn’t complain about `--long` being given twice when
 //! it’s clear what the user wants.
 
@@ -75,7 +75,7 @@ use fs::dir_action::DirAction;
 use fs::filter::FileFilter;
 use output::{View, Mode, details, grid_details};
 
-mod colours;
+mod style;
 mod dir_action;
 mod filter;
 mod view;

+ 528 - 0
src/options/style.rs

@@ -0,0 +1,528 @@
+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,
+/// output to the terminal.
+///
+/// By default, we want to display the colours when stdout can display them.
+/// Turning them on when output is going to, say, a pipe, would make programs
+/// such as `grep` or `more` not work properly. So the `Automatic` mode does
+/// this check and only displays colours when they can be truly appreciated.
+#[derive(PartialEq, Debug)]
+enum TerminalColours {
+
+    /// Display them even when output isn’t going to a terminal.
+    Always,
+
+    /// Display them when output is going to a terminal, but not otherwise.
+    Automatic,
+
+    /// Never display them, even when output is going to a terminal.
+    Never,
+}
+
+impl Default for TerminalColours {
+    fn default() -> TerminalColours {
+        TerminalColours::Automatic
+    }
+}
+
+const COLOURS: &[&str] = &["always", "auto", "never"];
+
+impl TerminalColours {
+
+    /// Determine which terminal colour conditions to use.
+    fn deduce(matches: &MatchedFlags) -> Result<TerminalColours, Misfire> {
+
+        let word = match matches.get_where(|f| f.matches(&flags::COLOR) || f.matches(&flags::COLOUR))? {
+            Some(w) => w,
+            None    => return Ok(TerminalColours::default()),
+        };
+
+        if word == "always" {
+            Ok(TerminalColours::Always)
+        }
+        else if word == "auto" || word == "automatic" {
+            Ok(TerminalColours::Automatic)
+        }
+        else if word == "never" {
+            Ok(TerminalColours::Never)
+        }
+        else {
+            Err(Misfire::bad_argument(&flags::COLOR, word, COLOURS))
+        }
+    }
+}
+
+
+/// **Styles**, which is already an overloaded term, is a pair of view option
+/// sets that happen to both be affected by `LS_COLORS` and `EXA_COLORS`.
+/// Because it’s better to only iterate through that once, the two are deduced
+/// together.
+pub struct Styles {
+
+    /// The colours to paint user interface elements, like the date column,
+    /// and file kinds, such as directories.
+    pub colours: Colours,
+
+    /// The colours to paint the names of files that match glob patterns
+    /// (and the classify option).
+    pub style: FileStyle,
+}
+
+impl Styles {
+
+    #[allow(trivial_casts)]   // the "as Box<_>" stuff below warns about this for some reason
+    pub fn deduce<V, TW>(matches: &MatchedFlags, vars: &V, widther: TW) -> Result<Self, Misfire>
+    where TW: Fn() -> Option<usize>, V: Vars {
+        use self::TerminalColours::*;
+        use info::filetype::FileExtensions;
+        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 {
+                colours: Colours::plain(),
+                style: FileStyle { classify, exts: Box::new(NoFileColours) },
+            });
+        }
+
+        // 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());
+
+        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<_>,
+        };
+
+        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;
+        }
+
+        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)?;
+
+        Ok(if flagged { Classify::AddFileIndicators }
+                 else { Classify::JustFilenames })
+    }
+}
+
+
+
+#[cfg(test)]
+mod terminal_test {
+    use super::*;
+    use std::ffi::OsString;
+    use options::flags;
+    use options::parser::{Flag, Arg};
+
+    use options::test::parse_for_test;
+    use options::test::Strictnesses::*;
+
+    pub fn os(input: &'static str) -> OsString {
+        let mut os = OsString::new();
+        os.push(input);
+        os
+    }
+
+    static TEST_ARGS: &[&Arg] = &[ &flags::COLOR, &flags::COLOUR ];
+
+    macro_rules! test {
+        ($name:ident:  $inputs:expr;  $stricts:expr => $result:expr) => {
+            #[test]
+            fn $name() {
+                for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| TerminalColours::deduce(mf)) {
+                    assert_eq!(result, $result);
+                }
+            }
+        };
+
+        ($name:ident:  $inputs:expr;  $stricts:expr => err $result:expr) => {
+            #[test]
+            fn $name() {
+                for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| TerminalColours::deduce(mf)) {
+                    assert_eq!(result.unwrap_err(), $result);
+                }
+            }
+        };
+    }
+
+
+    // Default
+    test!(empty:         [];                     Both => Ok(TerminalColours::default()));
+
+    // --colour
+    test!(u_always:      ["--colour=always"];    Both => Ok(TerminalColours::Always));
+    test!(u_auto:        ["--colour", "auto"];   Both => Ok(TerminalColours::Automatic));
+    test!(u_never:       ["--colour=never"];     Both => Ok(TerminalColours::Never));
+
+    // --color
+    test!(no_u_always:   ["--color", "always"];  Both => Ok(TerminalColours::Always));
+    test!(no_u_auto:     ["--color=auto"];       Both => Ok(TerminalColours::Automatic));
+    test!(no_u_never:    ["--color", "never"];   Both => Ok(TerminalColours::Never));
+
+    // Errors
+    test!(no_u_error:    ["--color=upstream"];   Both => err Misfire::bad_argument(&flags::COLOR, &os("upstream"), super::COLOURS));  // the error is for --color
+    test!(u_error:       ["--colour=lovers"];    Both => err Misfire::bad_argument(&flags::COLOR, &os("lovers"),   super::COLOURS));  // and so is this one!
+
+    // Overriding
+    test!(overridden_1:  ["--colour=auto", "--colour=never"];  Last => Ok(TerminalColours::Never));
+    test!(overridden_2:  ["--color=auto",  "--colour=never"];  Last => Ok(TerminalColours::Never));
+    test!(overridden_3:  ["--colour=auto", "--color=never"];   Last => Ok(TerminalColours::Never));
+    test!(overridden_4:  ["--color=auto",  "--color=never"];   Last => Ok(TerminalColours::Never));
+
+    test!(overridden_5:  ["--colour=auto", "--colour=never"];  Complain => err Misfire::Duplicate(Flag::Long("colour"), Flag::Long("colour")));
+    test!(overridden_6:  ["--color=auto",  "--colour=never"];  Complain => err Misfire::Duplicate(Flag::Long("color"),  Flag::Long("colour")));
+    test!(overridden_7:  ["--colour=auto", "--color=never"];   Complain => err Misfire::Duplicate(Flag::Long("colour"), Flag::Long("color")));
+    test!(overridden_8:  ["--color=auto",  "--color=never"];   Complain => err Misfire::Duplicate(Flag::Long("color"),  Flag::Long("color")));
+}
+
+
+#[cfg(test)]
+mod colour_test {
+    use super::*;
+    use options::flags;
+    use options::parser::{Flag, Arg};
+
+    use options::test::parse_for_test;
+    use options::test::Strictnesses::*;
+
+    static TEST_ARGS: &[&Arg] = &[ &flags::COLOR,       &flags::COLOUR,
+                                   &flags::COLOR_SCALE, &flags::COLOUR_SCALE ];
+
+    macro_rules! 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| Styles::deduce(mf, &None, &$widther).map(|s| s.colours)) {
+                    assert_eq!(result, $result);
+                }
+            }
+        };
+
+        ($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| Styles::deduce(mf, &None, &$widther).map(|s| s.colours)) {
+                    assert_eq!(result.unwrap_err(), $result);
+                }
+            }
+        };
+
+        ($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| Styles::deduce(mf, &None, &$widther).map(|s| s.colours)) {
+                    println!("Testing {:?}", result);
+                    match result {
+                        $pat => assert!(true),
+                        _    => assert!(false),
+                    }
+                }
+            }
+        };
+    }
+
+    test!(width_1:  ["--colour", "always"],    || Some(80);  Both => Ok(Colours::colourful(false)));
+    test!(width_2:  ["--colour", "always"],    || None;      Both => Ok(Colours::colourful(false)));
+    test!(width_3:  ["--colour", "never"],     || Some(80);  Both => Ok(Colours::plain()));
+    test!(width_4:  ["--colour", "never"],     || None;      Both => Ok(Colours::plain()));
+    test!(width_5:  ["--colour", "automatic"], || Some(80);  Both => Ok(Colours::colourful(false)));
+    test!(width_6:  ["--colour", "automatic"], || None;      Both => Ok(Colours::plain()));
+    test!(width_7:  [],                        || Some(80);  Both => Ok(Colours::colourful(false)));
+    test!(width_8:  [],                        || None;      Both => Ok(Colours::plain()));
+
+    test!(scale_1:  ["--color=always", "--color-scale", "--colour-scale"], || None;   Last => like Ok(Colours { scale: true,  .. }));
+    test!(scale_2:  ["--color=always", "--color-scale",                 ], || None;   Last => like Ok(Colours { scale: true,  .. }));
+    test!(scale_3:  ["--color=always",                  "--colour-scale"], || None;   Last => like Ok(Colours { scale: true,  .. }));
+    test!(scale_4:  ["--color=always",                                  ], || None;   Last => like Ok(Colours { scale: false, .. }));
+
+    test!(scale_5:  ["--color=always", "--color-scale", "--colour-scale"], || None;   Complain => err Misfire::Duplicate(Flag::Long("color-scale"),  Flag::Long("colour-scale")));
+    test!(scale_6:  ["--color=always", "--color-scale",                 ], || None;   Complain => like Ok(Colours { scale: true,  .. }));
+    test!(scale_7:  ["--color=always",                  "--colour-scale"], || None;   Complain => like Ok(Colours { scale: true,  .. }));
+    test!(scale_8:  ["--color=always",                                  ], || None;   Complain => like Ok(Colours { scale: false, .. }));
+}
+
+
+
+#[cfg(test)]
+mod customs_test {
+    use std::ffi::OsString;
+
+    use super::*;
+    use options::Vars;
+
+    use ansi_term::Colour::*;
+
+    macro_rules! test {
+        ($name:ident:  ls $ls:expr, exa $exa:expr  =>  colours $expected:ident -> $process_expected:expr) => {
+            #[test]
+            #[allow(unused_mut)]
+            fn $name() {
+                let mut $expected = Colours::colourful(false);
+                $process_expected();
+
+                let vars = MockVars { ls: $ls, exa: $exa };
+
+                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);
+            }
+        };
+    }
+
+    struct MockVars {
+        ls: &'static str,
+        exa: &'static str,
+    }
+
+    // Test impl that just returns the value it has.
+    impl Vars for MockVars {
+        fn get(&self, name: &'static str) -> Option<OsString> {
+            use options::vars;
+
+            if name == vars::LS_COLORS && !self.ls.is_empty() {
+                OsString::from(self.ls.clone()).into()
+            }
+            else if name == vars::EXA_COLORS && !self.exa.is_empty() {
+                OsString::from(self.exa.clone()).into()
+            }
+            else {
+                None
+            }
+        }
+    }
+
+    // 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(); });
+}

+ 3 - 29
src/options/view.rs

@@ -1,9 +1,6 @@
-use style::Colours;
-
 use output::{View, Mode, grid, details};
 use output::grid_details::{self, RowThreshold};
 use output::table::{TimeTypes, Environment, SizeFormat, Columns, Options as TableOptions};
-use output::file_name::{Classify, FileStyle, NoFileColours};
 use output::time::TimeFormat;
 
 use options::{flags, Misfire, Vars};
@@ -16,9 +13,10 @@ impl View {
 
     /// Determine which view to use and all of that view’s arguments.
     pub fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<View, Misfire> {
+        use options::style::Styles;
+
         let mode = Mode::deduce(matches, vars)?;
-        let colours = Colours::deduce(matches, vars, || *TERM_WIDTH)?;
-        let style = FileStyle::deduce(matches, &colours)?;
+        let Styles { colours, style } = Styles::deduce(matches, vars, || *TERM_WIDTH)?;
         Ok(View { mode, colours, style })
     }
 }
@@ -335,30 +333,6 @@ impl TimeTypes {
 }
 
 
-impl FileStyle {
-
-    #[allow(trivial_casts)]
-    fn deduce(matches: &MatchedFlags, colours: &Colours) -> Result<FileStyle, Misfire> {
-        use info::filetype::FileExtensions;
-
-        let classify = Classify::deduce(matches)?;
-        let exts = if colours.colourful { Box::new(FileExtensions) as Box<_> }
-                                   else { Box::new(NoFileColours)  as Box<_> };
-
-        Ok(FileStyle { classify, exts })
-    }
-}
-
-impl Classify {
-    fn deduce(matches: &MatchedFlags) -> Result<Classify, Misfire> {
-        let flagged = matches.has(&flags::CLASSIFY)?;
-
-        Ok(if flagged { Classify::AddFileIndicators }
-                 else { Classify::JustFilenames })
-    }
-}
-
-
 // Gets, then caches, the width of the terminal that exa is running in.
 // This gets used multiple times above, with no real guarantee of order,
 // so it’s easier to just cache it the first time it runs.

+ 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))
+    }
+}

+ 6 - 15
src/style/colours.rs

@@ -195,7 +195,7 @@ impl Colours {
 
 
 impl Colours {
-    pub fn set_ls(&mut self, pair: &Pair) {
+    pub fn set_ls(&mut self, pair: &Pair) -> bool {
         match pair.key {
             "di" => self.filekinds.directory    = pair.to_style(),
             "ex" => self.filekinds.executable   = pair.to_style(),
@@ -207,23 +207,13 @@ impl Colours {
             "ln" => self.filekinds.symlink      = pair.to_style(),
             "or" => self.broken_arrow           = pair.to_style(),
             "mi" => self.broken_filename        = pair.to_style(),
-             _   => {/* don’t change anything */},
+             _   => return false,
         }
+        true
     }
 
-    pub fn set_exa(&mut self, pair: &Pair) {
+    pub fn set_exa(&mut self, pair: &Pair) -> bool {
         match pair.key {
-            "di" => self.filekinds.directory      = pair.to_style(),
-            "ex" => self.filekinds.executable     = pair.to_style(),
-            "fi" => self.filekinds.normal         = pair.to_style(),
-            "pi" => self.filekinds.pipe           = pair.to_style(),
-            "so" => self.filekinds.socket         = pair.to_style(),
-            "bd" => self.filekinds.block_device   = pair.to_style(),
-            "cd" => self.filekinds.char_device    = pair.to_style(),
-            "ln" => self.filekinds.symlink        = pair.to_style(),
-            "or" => self.broken_arrow             = pair.to_style(),
-            "mi" => self.broken_filename          = pair.to_style(),
-
             "ur" => self.perms.user_read          = pair.to_style(),
             "uw" => self.perms.user_write         = pair.to_style(),
             "ux" => self.perms.user_execute_file  = pair.to_style(),
@@ -265,8 +255,9 @@ impl Colours {
             "lp" => self.symlink_path             = pair.to_style(),
             "cc" => self.control_char             = pair.to_style(),
 
-             _   => {/* still don’t change anything */},
+             _   => return false,
         }
+        true
     }
 }
 

+ 34 - 2
src/style/lsc.rs

@@ -1,6 +1,6 @@
 use std::ops::FnMut;
 
-use ansi_term::Style;
+use ansi_term::{Colour, Style};
 use ansi_term::Colour::*;
 
 
@@ -25,11 +25,30 @@ pub struct Pair<'var> {
     pub value: &'var str,
 }
 
+use std::iter::Peekable;
+fn parse_into_high_colour<'a, I>(iter: &mut Peekable<I>) -> Option<Colour>
+where I: Iterator<Item=&'a str> {
+    match iter.peek() {
+        Some(&"5") => {
+            let _5 = iter.next();
+            if let Some(byte) = iter.next() {
+                if let Ok(num) = byte.parse() {
+                    return Some(Fixed(num));
+                }
+            }
+        }
+        _ => {},
+    }
+
+    None
+}
+
 impl<'var> Pair<'var> {
     pub fn to_style(&self) -> Style {
         let mut style = Style::default();
+        let mut iter = self.value.split(";").peekable();
 
-        for num in self.value.split(";") {
+        while let Some(num) = iter.next() {
             match num {
 
                 // Bold and italic
@@ -45,6 +64,7 @@ impl<'var> Pair<'var> {
                 "35" => style = style.fg(Purple),
                 "36" => style = style.fg(Cyan),
                 "37" => style = style.fg(White),
+                "38" => if let Some(c) = parse_into_high_colour(&mut iter) { style = style.fg(c) },
 
                 // Background colours
                 "40" => style = style.on(Black),
@@ -55,6 +75,8 @@ impl<'var> Pair<'var> {
                 "45" => style = style.on(Purple),
                 "46" => style = style.on(Cyan),
                 "47" => style = style.on(White),
+                "48" => if let Some(c) = parse_into_high_colour(&mut iter) { style = style.on(c) },
+
                  _    => {/* ignore the error and do nothing */},
             }
         }
@@ -93,6 +115,16 @@ mod ansi_test {
     test!(semis: ";;;;;;"    => Style::default());
     test!(nines: "99999999"  => Style::default());
     test!(word:  "GREEN"     => Style::default());
+
+    // Higher colours
+    test!(hifg:  "38;5;149"  => Fixed(149).normal());
+    test!(hibg:  "48;5;1"    => Style::default().on(Fixed(1)));
+    test!(hibo:  "48;5;1;1"  => Style::default().on(Fixed(1)).bold());
+    test!(hiund: "4;48;5;1"  => Style::default().on(Fixed(1)).underline());
+
+    test!(fgbg:  "38;5;121;48;5;212"  => Fixed(121).on(Fixed(212)));
+    test!(bgfg:  "48;5;121;38;5;212"  => Fixed(212).on(Fixed(121)));
+    test!(toohi: "48;5;999"           => Style::default());
 }
 
 

+ 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