Ver Fonte

Merge branch 'exa-colors'

This branch added support for the EXA_COLORS environment variable, and defines a bunch of two-letter configuration settings that allows theming exa.

The next step is to allow custom highlighting based on file names.
Benjamin Sago há 8 anos atrás
pai
commit
075fe802b4

+ 23 - 14
Vagrantfile

@@ -135,9 +135,9 @@ Vagrant.configure(2) do |config|
         echo -e "\033[32;1mt\033[0m or \033[32;1mtest-exa\033[0m to run \033[1mcargo test\033[0m"   >> /etc/motd
         echo -e "\033[32;1mx\033[0m or \033[32;1mrun-xtests\033[0m to run \033[1m/vagrant/xtests/run.sh\033[0m"  >> /etc/motd
         echo -e "\033[32;1mc\033[0m or \033[32;1mcompile-exa\033[0m to run all three"  >> /etc/motd
-        echo -e "\033[32;1mdebug on\033[0;32m|\033[1moff\033[0m to toggle printing logs"  >> /etc/motd
-        echo -e "\033[32;1mstrict on\033[0;32m|\033[1moff\033[0m to toggle strict mode"  >> /etc/motd
-        echo -e "\033[32;1mls-colors on\033[0;32m|\033[1moff\033[0m to toggle LS_COLORS\n"  >> /etc/motd
+        echo -e "\033[32;1mdebug\033[0m to toggle printing logs"  >> /etc/motd
+        echo -e "\033[32;1mstrict\033[0m to toggle strict mode"  >> /etc/motd
+        echo -e "\033[32;1mcolors\033[0m to toggle custom colours\n"  >> /etc/motd
 
         # help banner
         echo 'echo -e "\\033[4mVersions\\033[0m"' > /home/ubuntu/.bash_profile
@@ -147,17 +147,18 @@ Vagrant.configure(2) do |config|
 
         # cool prompt
         echo 'function nonzero_return() { RETVAL=$?; [ $RETVAL -ne 0 ] && echo "$RETVAL "; }' >> /home/ubuntu/.bash_profile
-        echo 'function debug_mode() { [ -n "$EXA_DEBUG" ] && echo "debug "; }' >> /home/ubuntu/.bash_profile
+        echo 'function debug_mode()  { [ -n "$EXA_DEBUG" ]  && echo "debug "; }'  >> /home/ubuntu/.bash_profile
         echo 'function strict_mode() { [ -n "$EXA_STRICT" ] && echo "strict "; }' >> /home/ubuntu/.bash_profile
-        echo 'function lsc_mode() { [ -n "$LS_COLORS" ] && echo "lsc "; }' >> /home/ubuntu/.bash_profile
-        echo 'export PS1="\\[\\e[1;36m\\]\\h \\[\\e[32m\\]\\w \\[\\e[31m\\]\\`nonzero_return\\`\\[\\e[35m\\]\\`debug_mode\\`\\[\\e[32m\\]\\`lsc_mode\\`\\[\\e[33m\\]\\`strict_mode\\`\\[\\e[36m\\]\\\\$\\[\\e[0m\\] "' >> /home/ubuntu/.bash_profile
+        echo 'function lsc_mode()    { [ -n "$LS_COLORS" ]  && echo "lsc "; }'    >> /home/ubuntu/.bash_profile
+        echo 'function exac_mode()   { [ -n "$EXA_COLORS" ] && echo "exac "; }'   >> /home/ubuntu/.bash_profile
+        echo 'export PS1="\\[\\e[1;36m\\]\\h \\[\\e[32m\\]\\w \\[\\e[31m\\]\\`nonzero_return\\`\\[\\e[35m\\]\\`debug_mode\\`\\[\\e[32m\\]\\`lsc_mode\\`\\[\\e[1;32m\\]\\`exac_mode\\`\\[\\e[33m\\]\\`strict_mode\\`\\[\\e[36m\\]\\\\$\\[\\e[0m\\] "' >> /home/ubuntu/.bash_profile
 
         # environment setting
         echo 'function debug () {' >> /home/ubuntu/.bash_profile
         echo '  case "$1" in "on") export EXA_DEBUG=1 ;;' >> /home/ubuntu/.bash_profile
         echo '    "off") export EXA_DEBUG= ;;' >> /home/ubuntu/.bash_profile
-        echo '    "") [ -n "$EXA_DEBUG" ] && echo "debug on" || echo "debug off" ;;' >> /home/ubuntu/.bash_profile
-        echo '    *) echo "Usage: debug on|off"; return 1 ;; esac; }' >> /home/ubuntu/.bash_profile
+        echo '    "")    [ -n "$EXA_DEBUG" ] && echo "debug on" || echo "debug off" ;;' >> /home/ubuntu/.bash_profile
+        echo '    *)     echo "Usage: debug on|off"; return 1 ;; esac; }' >> /home/ubuntu/.bash_profile
 
         echo 'function strict () {' >> /home/ubuntu/.bash_profile
         echo '  case "$1" in "on") export EXA_STRICT=1 ;;' >> /home/ubuntu/.bash_profile
@@ -165,13 +166,21 @@ Vagrant.configure(2) do |config|
         echo '    "") [ -n "$EXA_STRICT" ] && echo "strict on" || echo "strict off" ;;' >> /home/ubuntu/.bash_profile
         echo '    *) echo "Usage: strict on|off"; return 1 ;; esac; }' >> /home/ubuntu/.bash_profile
 
-        echo 'function ls-colors () {' >> /home/ubuntu/.bash_profile
+        echo 'function colors () {' >> /home/ubuntu/.bash_profile
         echo '  case "$1" in ' >> /home/ubuntu/.bash_profile
-        echo '    "on")      export LS_COLORS="di=34:ln=35:so=32:pi=33:ex=31:bd=34;46:cd=34;43:su=30;41:sg=30;46:tw=30;42:ow=30;43" ;;' >> /home/ubuntu/.bash_profile
-        echo '    "hacker")  export LS_COLORS="di=32:ex=32:fi=32:pi=32:so=32:bd=32:cd=32:ln=32:or=32:mi=32" ;;' >> /home/ubuntu/.bash_profile
-        echo '    "off")     export LS_COLORS= ;;' >> /home/ubuntu/.bash_profile
-        echo '    "")       [ -n "$LS_COLORS" ] && echo "LS_COLORS=$LS_COLORS" || echo "ls-colors off" ;;' >> /home/ubuntu/.bash_profile
-        echo '    *)        echo "Usage: ls-colors on|off"; return 1 ;; esac; }' >> /home/ubuntu/.bash_profile
+        echo '    "ls")' >> /home/ubuntu/.bash_profile
+        echo '      export LS_COLORS="di=34:ln=35:so=32:pi=33:ex=31:bd=34;46:cd=34;43:su=30;41:sg=30;46:tw=30;42:ow=30;43"'  >> /home/ubuntu/.bash_profile
+        echo '      export EXA_COLORS="" ;;' >> /home/ubuntu/.bash_profile
+        echo '    "hacker")' >> /home/ubuntu/.bash_profile
+        echo '      export LS_COLORS="di=32:ex=32:fi=32:pi=32:so=32:bd=32:cd=32:ln=32:or=32:mi=32"' >> /home/ubuntu/.bash_profile
+        echo '      export EXA_COLORS="ur=32:uw=32:ux=32:ue=32:gr=32:gw=32:gx=32:tr=32:tw=32:tx=32:su=32:sf=32:xa=32:sn=32:sb=32:df=32:ds=32:uu=32:un=32:gu=32:gn=32:lc=32:lm=32:ga=32:gm=32:gd=32:gv=32:gt=32:xx=32:da=32:in=32:bl=32:hd=32:lp=32:cc=32:" ;;' >> /home/ubuntu/.bash_profile
+        echo '    "off")' >> /home/ubuntu/.bash_profile
+        echo '      export LS_COLORS=' >> /home/ubuntu/.bash_profile
+        echo '      export EXA_COLORS= ;;' >> /home/ubuntu/.bash_profile
+        echo '    "")' >> /home/ubuntu/.bash_profile
+        echo '      [ -n "$LS_COLORS" ]  && echo "LS_COLORS=$LS_COLORS"   || echo "ls-colors off"'     >> /home/ubuntu/.bash_profile
+        echo '      [ -n "$EXA_COLORS" ] && echo "EXA_COLORS=$EXA_COLORS" || echo "exa-colors off" ;;' >> /home/ubuntu/.bash_profile
+        echo '    *) echo "Usage: ls-colors ls|hacker|off"; return 1 ;; esac; }' >> /home/ubuntu/.bash_profile
 
         # Disable last login date in sshd
         sed -i '/PrintLastLog yes/c\PrintLastLog no' /etc/ssh/sshd_config

+ 1 - 0
src/exa.rs

@@ -38,6 +38,7 @@ mod fs;
 mod info;
 mod options;
 mod output;
+mod style;
 
 
 /// The main program wrapper.

+ 34 - 11
src/info/filetype.rs

@@ -4,10 +4,13 @@
 //! those are the only metadata that we have access to without reading the
 //! file’s contents.
 
+use ansi_term::Style;
+
 use fs::File;
+use output::file_name::FileColours;
 
 
-#[derive(Debug)]
+#[derive(Debug, Default, PartialEq)]
 pub struct FileExtensions;
 
 impl FileExtensions {
@@ -15,7 +18,7 @@ impl FileExtensions {
     /// An “immediate” file is something that can be run or activated somehow
     /// in order to kick off the build of a project. It’s usually only present
     /// in directories full of source code.
-    pub fn is_immediate(&self, file: &File) -> bool {
+    fn is_immediate(&self, file: &File) -> bool {
         file.name.starts_with("README") || file.name_is_one_of( &[
             "Makefile", "Cargo.toml", "SConstruct", "CMakeLists.txt",
             "build.gradle", "Rakefile", "Gruntfile.js",
@@ -23,7 +26,7 @@ impl FileExtensions {
         ])
     }
 
-    pub fn is_image(&self, file: &File) -> bool {
+    fn is_image(&self, file: &File) -> bool {
         file.extension_is_one_of( &[
             "png", "jpeg", "jpg", "gif", "bmp", "tiff", "tif",
             "ppm", "pgm", "pbm", "pnm", "webp", "raw", "arw",
@@ -32,7 +35,7 @@ impl FileExtensions {
         ])
     }
 
-    pub fn is_video(&self, file: &File) -> bool {
+    fn is_video(&self, file: &File) -> bool {
         file.extension_is_one_of( &[
             "avi", "flv", "m2v", "mkv", "mov", "mp4", "mpeg",
             "mpg", "ogm", "ogv", "vob", "wmv", "webm", "m2ts",
@@ -40,26 +43,26 @@ impl FileExtensions {
         ])
     }
 
-    pub fn is_music(&self, file: &File) -> bool {
+    fn is_music(&self, file: &File) -> bool {
         file.extension_is_one_of( &[
             "aac", "m4a", "mp3", "ogg", "wma", "mka", "opus",
         ])
     }
 
     // Lossless music, rather than any other kind of data...
-    pub fn is_lossless(&self, file: &File) -> bool {
+    fn is_lossless(&self, file: &File) -> bool {
         file.extension_is_one_of( &[
             "alac", "ape", "flac", "wav",
         ])
     }
 
-    pub fn is_crypto(&self, file: &File) -> bool {
+    fn is_crypto(&self, file: &File) -> bool {
         file.extension_is_one_of( &[
             "asc", "enc", "gpg", "pgp", "sig", "signature", "pfx", "p12",
         ])
     }
 
-    pub fn is_document(&self, file: &File) -> bool {
+    fn is_document(&self, file: &File) -> bool {
         file.extension_is_one_of( &[
             "djvu", "doc", "docx", "dvi", "eml", "eps", "fotd",
             "odp", "odt", "pdf", "ppt", "pptx", "rtf",
@@ -67,7 +70,7 @@ impl FileExtensions {
         ])
     }
 
-    pub fn is_compressed(&self, file: &File) -> bool {
+    fn is_compressed(&self, file: &File) -> bool {
         file.extension_is_one_of( &[
             "zip", "tar", "Z", "z", "gz", "bz2", "a", "ar", "7z",
             "iso", "dmg", "tc", "rar", "par", "tgz", "xz", "txz",
@@ -75,13 +78,13 @@ impl FileExtensions {
         ])
     }
 
-    pub fn is_temp(&self, file: &File) -> bool {
+    fn is_temp(&self, file: &File) -> bool {
         file.name.ends_with('~')
             || (file.name.starts_with('#') && file.name.ends_with('#'))
             || file.extension_is_one_of( &[ "tmp", "swp", "swo", "swn", "bak" ])
     }
 
-    pub fn is_compiled(&self, file: &File) -> bool {
+    fn is_compiled(&self, file: &File) -> bool {
         if file.extension_is_one_of( &[ "class", "elc", "hi", "o", "pyc" ]) {
             true
         }
@@ -93,3 +96,23 @@ impl FileExtensions {
         }
     }
 }
+
+impl FileColours for FileExtensions {
+    fn colour_file(&self, file: &File) -> Option<Style> {
+        use ansi_term::Colour::*;
+
+        Some(match file {
+            f if self.is_immediate(f)   => Yellow.bold().underline(),
+            f if self.is_image(f)       => Fixed(133).normal(),
+            f if self.is_video(f)       => Fixed(135).normal(),
+            f if self.is_music(f)       => Fixed(92).normal(),
+            f if self.is_lossless(f)    => Fixed(93).normal(),
+            f if self.is_crypto(f)      => Fixed(109).normal(),
+            f if self.is_document(f)    => Fixed(105).normal(),
+            f if self.is_compressed(f)  => Red.normal(),
+            f if self.is_temp(f)        => Fixed(244).normal(),
+            f if self.is_compiled(f)    => Fixed(137).normal(),
+            _                           => return None,
+        })
+    }
+}

+ 15 - 18
src/options/colours.rs

@@ -1,4 +1,4 @@
-use output::Colours;
+use style::Colours;
 
 use options::{flags, Vars, Misfire};
 use options::parser::MatchedFlags;
@@ -63,7 +63,8 @@ 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 output::lsc::LSColors;
+        use style::LSColors;
+        use options::vars;
 
         let tc = TerminalColours::deduce(matches)?;
         if tc == Never || (tc == Automatic && widther().is_none()) {
@@ -73,20 +74,14 @@ impl Colours {
         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("LS_COLORS") {
+        if let Some(lsc) = vars.get(vars::LS_COLORS) {
             let lsc = lsc.to_string_lossy();
-            let lsc = LSColors::parse(lsc.as_ref());
-
-            if let Some(c) = lsc.get("di") { colours.filekinds.directory    = c; }
-            if let Some(c) = lsc.get("ex") { colours.filekinds.executable   = c; }
-            if let Some(c) = lsc.get("fi") { colours.filekinds.normal       = c; }
-            if let Some(c) = lsc.get("pi") { colours.filekinds.pipe         = c; }
-            if let Some(c) = lsc.get("so") { colours.filekinds.socket       = c; }
-            if let Some(c) = lsc.get("bd") { colours.filekinds.block_device = c; }
-            if let Some(c) = lsc.get("cd") { colours.filekinds.char_device  = c; }
-            if let Some(c) = lsc.get("ln") { colours.filekinds.symlink      = c; }
-            if let Some(c) = lsc.get("or") { colours.broken_arrow           = c; }
-            if let Some(c) = lsc.get("mi") { colours.broken_filename        = c; }
+            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)
@@ -251,7 +246,7 @@ mod customs_test {
                 let vars = MockVars { ls: $ls, exa: $exa };
 
                 for result in parse_for_test(&[], &[], Both, |mf| Colours::deduce(mf, &vars, || Some(80))) {
-                    assert_eq!(result, Ok(c));
+                    assert_eq!(result.as_ref(), Ok(&c));
                 }
             }
         };
@@ -265,10 +260,12 @@ mod customs_test {
     // Test impl that just returns the value it has.
     impl Vars for MockVars {
         fn get(&self, name: &'static str) -> Option<OsString> {
-            if name == "LS_COLORS" && !self.ls.is_empty() {
+            use options::vars;
+
+            if name == vars::LS_COLORS && !self.ls.is_empty() {
                 OsString::from(self.ls.clone()).into()
             }
-            else if name == "EXA_COLORS" && !self.exa.is_empty() {
+            else if name == vars::EXA_COLORS && !self.exa.is_empty() {
                 OsString::from(self.exa.clone()).into()
             }
             else {

+ 6 - 17
src/options/mod.rs

@@ -89,6 +89,9 @@ use self::version::VersionString;
 mod misfire;
 pub use self::misfire::Misfire;
 
+pub mod vars;
+pub use self::vars::Vars;
+
 mod parser;
 mod flags;
 use self::parser::MatchedFlags;
@@ -120,8 +123,9 @@ impl Options {
     where I: IntoIterator<Item=&'args OsString>,
           V: Vars {
         use options::parser::{Matches, Strictness};
+        use options::vars;
 
-        let strictness = match vars.get("EXA_STRICT") {
+        let strictness = match vars.get(vars::EXA_STRICT) {
             None                         => Strictness::UseLastArguments,
             Some(ref t) if t.is_empty()  => Strictness::UseLastArguments,
             _                            => Strictness::ComplainAboutRedundantArguments,
@@ -162,28 +166,13 @@ impl Options {
 }
 
 
-/// Mockable wrapper for `std::env::var_os`.
-pub trait Vars {
-    fn get(&self, name: &'static str) -> Option<OsString>;
-}
-
-
-
 
 #[cfg(test)]
 pub mod test {
-    use super::{Options, Misfire, Vars, flags};
+    use super::{Options, Misfire, flags};
     use options::parser::{Arg, MatchedFlags};
     use std::ffi::OsString;
 
-    // Test impl that just returns the value it has.
-    impl Vars for Option<OsString> {
-        fn get(&self, _name: &'static str) -> Option<OsString> {
-            self.clone()
-        }
-    }
-
-
     #[derive(PartialEq, Debug)]
     pub enum Strictnesses {
         Last,

+ 48 - 0
src/options/vars.rs

@@ -0,0 +1,48 @@
+use std::ffi::OsString;
+
+
+// General variables
+
+/// Environment variable used to colour files, both by their filesystem type
+/// (symlink, socket, directory) and their file name or extension (image,
+/// video, archive);
+pub static LS_COLORS: &str = "LS_COLORS";
+
+/// Environment variable used to override the width of the terminal, in
+/// characters.
+pub static COLUMNS: &str = "COLUMNS";
+
+
+// exa-specific variables
+
+/// Environment variable used to colour exa’s interface when colours are
+/// enabled. This includes all the colours that LS_COLORS would recognise,
+/// overriding them if necessary. It can also contain exa-specific codes.
+pub static EXA_COLORS: &str = "EXA_COLORS";
+
+/// Environment variable used to switch on strict argument checking, such as
+/// complaining if an argument was specified twice, or if two conflict.
+/// This is meant to be so you don’t accidentally introduce the wrong
+/// behaviour in a script, rather than for general command-line use.
+pub static EXA_STRICT: &str = "EXA_STRICT";
+
+/// Environment variable used to limit the grid-details view
+/// (`--grid --long`) so it’s only activated if there’s at least the given
+/// number of rows of output.
+pub static EXA_GRID_ROWS: &str = "EXA_GRID_ROWS";
+
+
+
+/// Mockable wrapper for `std::env::var_os`.
+pub trait Vars {
+    fn get(&self, name: &'static str) -> Option<OsString>;
+}
+
+
+// Test impl that just returns the value it has.
+#[cfg(test)]
+impl Vars for Option<OsString> {
+    fn get(&self, _name: &'static str) -> Option<OsString> {
+        self.clone()
+    }
+}

+ 18 - 8
src/options/view.rs

@@ -1,15 +1,15 @@
-use output::Colours;
+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};
+use output::file_name::{Classify, FileStyle, NoFileColours};
 use output::time::TimeFormat;
 
 use options::{flags, Misfire, Vars};
 use options::parser::MatchedFlags;
 
 use fs::feature::xattr;
-use info::filetype::FileExtensions;
 
 
 impl View {
@@ -18,7 +18,7 @@ impl View {
     pub fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<View, Misfire> {
         let mode = Mode::deduce(matches, vars)?;
         let colours = Colours::deduce(matches, vars, || *TERM_WIDTH)?;
-        let style = FileStyle::deduce(matches)?;
+        let style = FileStyle::deduce(matches, &colours)?;
         Ok(View { mode, colours, style })
     }
 }
@@ -156,7 +156,9 @@ impl TerminalWidth {
     ///
     /// Returns an error if a requested width doesn’t parse to an integer.
     fn deduce<V: Vars>(vars: &V) -> Result<TerminalWidth, Misfire> {
-        if let Some(columns) = vars.get("COLUMNS").and_then(|s| s.into_string().ok()) {
+        use options::vars;
+
+        if let Some(columns) = vars.get(vars::COLUMNS).and_then(|s| s.into_string().ok()) {
             match columns.parse() {
                 Ok(width)  => Ok(TerminalWidth::Set(width)),
                 Err(e)     => Err(Misfire::FailedParse(e)),
@@ -185,7 +187,9 @@ impl RowThreshold {
     /// Determine whether to use a row threshold based on the given
     /// environment variables.
     fn deduce<V: Vars>(vars: &V) -> Result<RowThreshold, Misfire> {
-        if let Some(columns) = vars.get("EXA_GRID_ROWS").and_then(|s| s.into_string().ok()) {
+        use options::vars;
+
+        if let Some(columns) = vars.get(vars::EXA_GRID_ROWS).and_then(|s| s.into_string().ok()) {
             match columns.parse() {
                 Ok(rows)  => Ok(RowThreshold::MinimumRows(rows)),
                 Err(e)    => Err(Misfire::FailedParse(e)),
@@ -332,9 +336,15 @@ impl TimeTypes {
 
 
 impl FileStyle {
-    fn deduce(matches: &MatchedFlags) -> Result<FileStyle, Misfire> {
+
+    #[allow(trivial_casts)]
+    fn deduce(matches: &MatchedFlags, colours: &Colours) -> Result<FileStyle, Misfire> {
+        use info::filetype::FileExtensions;
+
         let classify = Classify::deduce(matches)?;
-        let exts = FileExtensions;
+        let exts = if colours.colourful { Box::new(FileExtensions) as Box<_> }
+                                   else { Box::new(NoFileColours)  as Box<_> };
+
         Ok(FileStyle { classify, exts })
     }
 }

+ 1 - 1
src/output/details.rs

@@ -70,7 +70,7 @@ use fs::{Dir, File};
 use fs::dir_action::RecurseOptions;
 use fs::filter::FileFilter;
 use fs::feature::xattr::{Attribute, FileAttributes};
-use output::colours::Colours;
+use style::Colours;
 use output::cell::TextCell;
 use output::tree::{TreeTrunk, TreeParams, TreeDepth};
 use output::file_name::FileStyle;

+ 59 - 45
src/output/file_name.rs

@@ -3,10 +3,9 @@ use std::path::Path;
 use ansi_term::{ANSIString, Style};
 
 use fs::{File, FileTarget};
-use info::filetype::FileExtensions;
-use output::Colours;
 use output::escape;
 use output::cell::TextCellContents;
+use output::render::FiletypeColours;
 
 
 /// Basically a file name factory.
@@ -17,19 +16,19 @@ pub struct FileStyle {
     pub classify: Classify,
 
     /// Mapping of file extensions to colours, to highlight regular files.
-    pub exts: FileExtensions,
+    pub exts: Box<FileColours>,
 }
 
 impl FileStyle {
 
     /// Create a new `FileName` that prints the given file’s name, painting it
     /// with the remaining arguments.
-    pub fn for_file<'a, 'dir>(&'a self, file: &'a File<'dir>, colours: &'a Colours) -> FileName<'a, 'dir> {
+    pub fn for_file<'a, 'dir, C: Colours>(&'a self, file: &'a File<'dir>, colours: &'a C) -> FileName<'a, 'dir, C> {
         FileName {
             file, colours,
             link_style: LinkStyle::JustFilenames,
-            exts:       &self.exts,
             classify:   self.classify,
+            exts:       &*self.exts,
             target:     if file.is_link() { Some(file.link_target()) }
                                      else { None }
         }
@@ -75,15 +74,15 @@ impl Default for Classify {
 
 /// A **file name** holds all the information necessary to display the name
 /// of the given file. This is used in all of the views.
-pub struct FileName<'a, 'dir: 'a> {
+pub struct FileName<'a,  'dir: 'a,  C: Colours+'a> {
 
-    /// A reference to the file that we're getting the name of.
+    /// A reference to the file that were getting the name of.
     file: &'a File<'dir>,
 
     /// The colours used to paint the file name and its surrounding text.
-    colours: &'a Colours,
+    colours: &'a C,
 
-    /// The file that this file points to if it's a link.
+    /// The file that this file points to if its a link.
     target: Option<FileTarget<'dir>>,
 
     /// How to handle displaying links.
@@ -93,11 +92,11 @@ pub struct FileName<'a, 'dir: 'a> {
     classify: Classify,
 
     /// Mapping of file extensions to colours, to highlight regular files.
-    exts: &'a FileExtensions,
+    exts: &'a FileColours,
 }
 
 
-impl<'a, 'dir> FileName<'a, 'dir> {
+impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
 
     /// Sets the flag on this file name to display link targets with an
     /// arrow followed by their path.
@@ -131,7 +130,7 @@ impl<'a, 'dir> FileName<'a, 'dir> {
             match *target {
                 FileTarget::Ok(ref target) => {
                     bits.push(Style::default().paint(" "));
-                    bits.push(self.colours.punctuation.paint("->"));
+                    bits.push(self.colours.normal_arrow().paint("->"));
                     bits.push(Style::default().paint(" "));
 
                     if let Some(parent) = target.path.parent() {
@@ -156,9 +155,9 @@ impl<'a, 'dir> FileName<'a, 'dir> {
 
                 FileTarget::Broken(ref broken_path) => {
                     bits.push(Style::default().paint(" "));
-                    bits.push(self.colours.broken_arrow.paint("->"));
+                    bits.push(self.colours.broken_arrow().paint("->"));
                     bits.push(Style::default().paint(" "));
-                    escape(broken_path.display().to_string(), &mut bits, self.colours.broken_filename, self.colours.control_char.underline());
+                    escape(broken_path.display().to_string(), &mut bits, self.colours.broken_filename(), self.colours.control_char().underline());
                 },
 
                 FileTarget::Err(_) => {
@@ -182,11 +181,11 @@ impl<'a, 'dir> FileName<'a, 'dir> {
         let coconut = parent.components().count();
 
         if coconut == 1 && parent.has_root() {
-            bits.push(self.colours.symlink_path.paint("/"));
+            bits.push(self.colours.symlink_path().paint("/"));
         }
         else if coconut >= 1 {
-            escape(parent.to_string_lossy().to_string(), bits, self.colours.symlink_path, self.colours.control_char);
-            bits.push(self.colours.symlink_path.paint("/"));
+            escape(parent.to_string_lossy().to_string(), bits, self.colours.symlink_path(), self.colours.control_char());
+            bits.push(self.colours.symlink_path().paint("/"));
         }
     }
 
@@ -223,7 +222,7 @@ impl<'a, 'dir> FileName<'a, 'dir> {
     fn coloured_file_name<'unused>(&self) -> Vec<ANSIString<'unused>> {
         let file_style = self.style();
         let mut bits = Vec::new();
-        escape(self.file.name.clone(), &mut bits, file_style, self.colours.control_char);
+        escape(self.file.name.clone(), &mut bits, file_style, self.colours.control_char());
         bits
     }
 
@@ -232,42 +231,57 @@ impl<'a, 'dir> FileName<'a, 'dir> {
     /// depending on which “type” of file it appears to be -- either from the
     /// class on the filesystem or from its name.
     pub fn style(&self) -> Style {
-
         // Override the style with the “broken link” style when this file is
         // a link that we can’t follow for whatever reason. This is used when
         // there’s no other place to show that the link doesn’t work.
         if let LinkStyle::JustFilenames = self.link_style {
             if let Some(ref target) = self.target {
                 if target.is_broken() {
-                    return self.colours.broken_arrow;
+                    return self.colours.broken_arrow();
                 }
             }
         }
 
-        // Otherwise, just apply a bunch of rules in order. For example,
-        // executable image files should be executable rather than images.
-        match self.file {
-            f if f.is_directory()        => self.colours.filekinds.directory,
-            f if f.is_executable_file()  => self.colours.filekinds.executable,
-            f if f.is_link()             => self.colours.filekinds.symlink,
-            f if f.is_pipe()             => self.colours.filekinds.pipe,
-            f if f.is_block_device()     => self.colours.filekinds.block_device,
-            f if f.is_char_device()      => self.colours.filekinds.char_device,
-            f if f.is_socket()           => self.colours.filekinds.socket,
-            f if !f.is_file()            => self.colours.filekinds.special,
-
-            f if self.exts.is_immediate(f)   => self.colours.filetypes.immediate,
-            f if self.exts.is_image(f)       => self.colours.filetypes.image,
-            f if self.exts.is_video(f)       => self.colours.filetypes.video,
-            f if self.exts.is_music(f)       => self.colours.filetypes.music,
-            f if self.exts.is_lossless(f)    => self.colours.filetypes.lossless,
-            f if self.exts.is_crypto(f)      => self.colours.filetypes.crypto,
-            f if self.exts.is_document(f)    => self.colours.filetypes.document,
-            f if self.exts.is_compressed(f)  => self.colours.filetypes.compressed,
-            f if self.exts.is_temp(f)        => self.colours.filetypes.temp,
-            f if self.exts.is_compiled(f)    => self.colours.filetypes.compiled,
-
-            _                                => self.colours.filekinds.normal,
-        }
+        self.kind_style()
+            .or_else(|| self.exts.colour_file(self.file))
+            .unwrap_or_else(|| self.colours.normal())
+    }
+
+    fn kind_style(&self) -> Option<Style> {
+        Some(match self.file {
+            f if f.is_directory()        => self.colours.directory(),
+            f if f.is_executable_file()  => self.colours.executable_file(),
+            f if f.is_link()             => self.colours.symlink(),
+            f if f.is_pipe()             => self.colours.pipe(),
+            f if f.is_block_device()     => self.colours.block_device(),
+            f if f.is_char_device()      => self.colours.char_device(),
+            f if f.is_socket()           => self.colours.socket(),
+            f if !f.is_file()            => self.colours.special(),
+            _                            => return None,
+        })
     }
 }
+
+pub trait Colours: FiletypeColours {
+    fn broken_arrow(&self) -> Style;
+    fn broken_filename(&self) -> Style;
+    fn normal_arrow(&self) -> Style;
+    fn control_char(&self) -> Style;
+    fn symlink_path(&self) -> Style;
+    fn executable_file(&self) -> Style;
+}
+
+// needs Debug because FileStyle derives it
+use std::fmt::Debug;
+use std::marker::Sync;
+pub trait FileColours: Debug+Sync {
+    fn colour_file(&self, file: &File) -> Option<Style>;
+}
+
+
+#[derive(PartialEq, Debug)]
+pub struct NoFileColours;
+impl FileColours for NoFileColours {
+    fn colour_file(&self, _file: &File) -> Option<Style> { None }
+}
+

+ 1 - 1
src/output/grid.rs

@@ -3,7 +3,7 @@ use std::io::{Write, Result as IOResult};
 use term_grid as tg;
 
 use fs::File;
-use output::colours::Colours;
+use style::Colours;
 use output::file_name::FileStyle;
 
 

+ 1 - 1
src/output/grid_details.rs

@@ -9,8 +9,8 @@ use fs::{Dir, File};
 use fs::feature::xattr::FileAttributes;
 use fs::filter::FileFilter;
 
+use style::Colours;
 use output::cell::TextCell;
-use output::colours::Colours;
 use output::details::{Options as DetailsOptions, Row as DetailsRow, Render as DetailsRender};
 use output::grid::Options as GridOptions;
 use output::file_name::FileStyle;

+ 2 - 3
src/output/lines.rs

@@ -3,9 +3,8 @@ use std::io::{Write, Result as IOResult};
 use ansi_term::ANSIStrings;
 
 use fs::File;
-
 use output::file_name::{FileName, FileStyle};
-use super::colours::Colours;
+use style::Colours;
 
 
 /// The lines view literally just displays each file, line-by-line.
@@ -25,7 +24,7 @@ impl<'a> Render<'a> {
         Ok(())
     }
 
-    fn render_file<'f>(&self, file: &'f File<'a>) -> FileName<'f, 'a> {
+    fn render_file<'f>(&self, file: &'f File<'a>) -> FileName<'f, 'a, Colours> {
         self.style.for_file(file, self.colours).with_link_paths()
     }
 }

+ 0 - 148
src/output/lsc.rs

@@ -1,148 +0,0 @@
-#![allow(dead_code)]
-
-use std::collections::HashMap;
-
-use ansi_term::Style;
-use ansi_term::Colour::*;
-
-
-pub struct LSColors<'var> {
-    contents: HashMap<&'var str, &'var str>
-}
-
-impl<'var> LSColors<'var> {
-    pub fn parse(input: &'var str) -> LSColors<'var> {
-        let contents = input.split(":")
-                            .flat_map(|mapping| {
-
-            let bits = mapping.split("=")
-                              .take(3)
-                              .collect::<Vec<_>>();
-
-            if bits.len() != 2 || bits[0].is_empty() || bits[1].is_empty() { None }
-            else { Some( (bits[0], bits[1]) ) }
-        }).collect();
-        LSColors { contents }
-    }
-
-    pub fn get(&self, facet_name: &str) -> Option<Style> {
-        self.contents.get(facet_name).map(ansi_to_style)
-    }
-}
-
-fn ansi_to_style(ansi: &&str) -> Style {
-    let mut style = Style::default();
-
-    for num in ansi.split(";") {
-        match num {
-
-            // Bold and italic
-            "1"  => style = style.bold(),
-            "4"  => style = style.underline(),
-
-            // Foreground colours
-            "30" => style = style.fg(Black),
-            "31" => style = style.fg(Red),
-            "32" => style = style.fg(Green),
-            "33" => style = style.fg(Yellow),
-            "34" => style = style.fg(Blue),
-            "35" => style = style.fg(Purple),
-            "36" => style = style.fg(Cyan),
-            "37" => style = style.fg(White),
-
-            // Background colours
-            "40" => style = style.on(Black),
-            "41" => style = style.on(Red),
-            "42" => style = style.on(Green),
-            "43" => style = style.on(Yellow),
-            "44" => style = style.on(Blue),
-            "45" => style = style.on(Purple),
-            "46" => style = style.on(Cyan),
-            "47" => style = style.on(White),
-             _    => {/* ignore the error and do nothing */},
-        }
-    }
-
-    style
-}
-
-
-#[cfg(test)]
-mod ansi_test {
-    use super::*;
-    use ansi_term::Style;
-
-    macro_rules! test {
-        ($name:ident: $input:expr => $result:expr) => {
-            #[test]
-            fn $name() {
-                assert_eq!(ansi_to_style(&$input), $result);
-            }
-        };
-    }
-
-    // Styles
-    test!(bold:  "1"         => Style::default().bold());
-    test!(under: "4"         => Style::default().underline());
-    test!(both:  "1;4"       => Style::default().bold().underline());
-    test!(fg:    "31"        => Red.normal());
-    test!(bg:    "43"        => Style::default().on(Yellow));
-    test!(bfg:   "31;43"     => Red.on(Yellow));
-    test!(all:   "43;31;1;4" => Red.on(Yellow).bold().underline());
-    test!(again: "1;1;1;1;1" => Style::default().bold());
-
-    // Failure cases
-    test!(empty: ""          => Style::default());
-    test!(semis: ";;;;;;"    => Style::default());
-    test!(nines: "99999999"  => Style::default());
-    test!(word:  "GREEN"     => Style::default());
-}
-
-
-
-#[cfg(test)]
-mod test {
-    use super::*;
-
-    macro_rules! test {
-        ($name:ident: $input:expr, $facet:expr => $result:expr) => {
-            #[test]
-            fn $name() {
-                let lsc = LSColors::parse($input);
-                assert_eq!(lsc.get($facet), $result.into());
-                assert_eq!(lsc.get(""), None);
-            }
-        };
-    }
-
-    // Bad parses
-    test!(empty:    "",       "di" => None);
-    test!(jibber:   "blah",   "di" => None);
-
-    test!(equals:     "=",    "di" => None);
-    test!(starts:     "=di",  "di" => None);
-    test!(ends:     "id=",    "id" => None);
-
-    // Foreground colours
-    test!(red:     "di=31",   "di" => Red.normal());
-    test!(green:   "cb=32",   "cb" => Green.normal());
-    test!(blue:    "la=34",   "la" => Blue.normal());
-
-    // Background colours
-    test!(yellow:  "do=43",   "do" => Style::default().on(Yellow));
-    test!(purple:  "re=45",   "re" => Style::default().on(Purple));
-    test!(cyan:    "mi=46",   "mi" => Style::default().on(Cyan));
-
-    // Bold and underline
-    test!(bold:    "fa=1",    "fa" => Style::default().bold());
-    test!(under:   "so=4",    "so" => Style::default().underline());
-    test!(both:    "la=1;4",  "la" => Style::default().bold().underline());
-
-    // More and many
-    test!(more_1:  "me=43;21;55;34:yu=1;4;1", "me" => Blue.on(Yellow));
-    test!(more_2:  "me=43;21;55;34:yu=1;4;1", "yu" => Style::default().bold().underline());
-
-    test!(many_1:  "red=31:green=32:blue=34", "red"   => Red.normal());
-    test!(many_2:  "red=31:green=32:blue=34", "green" => Green.normal());
-    test!(many_3:  "red=31:green=32:blue=34", "blue"  => Blue.normal());
-}

+ 2 - 5
src/output/mod.rs

@@ -1,23 +1,20 @@
 use output::file_name::FileStyle;
+use style::Colours;
 
 pub use self::cell::{TextCell, TextCellContents, DisplayWidth};
-pub use self::colours::Colours;
 pub use self::escape::escape;
-pub use self::lsc::LSColors;
 
 pub mod details;
 pub mod file_name;
 pub mod grid_details;
 pub mod grid;
 pub mod lines;
-pub mod lsc;
+pub mod render;
 pub mod table;
 pub mod time;
 
 mod cell;
-mod colours;
 mod escape;
-mod render;
 mod tree;
 
 

+ 1 - 3
src/output/table.rs

@@ -10,14 +10,12 @@ use locale;
 
 use users::UsersCache;
 
+use style::Colours;
 use output::cell::TextCell;
-use output::colours::Colours;
 use output::time::TimeFormat;
-
 use fs::{File, Dir, fields as f};
 
 
-
 /// Options for displaying a table.
 pub struct Options {
     pub env: Environment,

+ 92 - 31
src/output/colours.rs → src/style/colours.rs

@@ -2,14 +2,17 @@ use ansi_term::Style;
 use ansi_term::Colour::{Red, Green, Yellow, Blue, Cyan, Purple, Fixed};
 
 use output::render;
+use output::file_name::Colours as FileNameColours;
 
+use style::lsc::Pair;
 
-#[derive(Clone, Copy, Debug, Default, PartialEq)]
+
+#[derive(Debug, Default, PartialEq)]
 pub struct Colours {
+    pub colourful: bool,
     pub scale: bool,
 
     pub filekinds:  FileKinds,
-    pub filetypes:  FileTypes,
     pub perms:      Permissions,
     pub size:       Size,
     pub users:      Users,
@@ -28,7 +31,6 @@ pub struct Colours {
     pub control_char:     Style,
 }
 
-// Colours for files depending on their filesystem type.
 #[derive(Clone, Copy, Debug, Default, PartialEq)]
 pub struct FileKinds {
     pub normal: Style,
@@ -42,21 +44,6 @@ pub struct FileKinds {
     pub executable: Style,
 }
 
-// Colours for files depending on their name or extension.
-#[derive(Clone, Copy, Debug, Default, PartialEq)]
-pub struct FileTypes {
-    pub image: Style,
-    pub video: Style,
-    pub music: Style,
-    pub lossless: Style,
-    pub crypto: Style,
-    pub document: Style,
-    pub compressed: Style,
-    pub temp: Style,
-    pub immediate: Style,
-    pub compiled: Style,
-}
-
 #[derive(Clone, Copy, Debug, Default, PartialEq)]
 pub struct Permissions {
     pub user_read:          Style,
@@ -123,6 +110,7 @@ impl Colours {
 
     pub fn colourful(scale: bool) -> Colours {
         Colours {
+            colourful: true,
             scale: scale,
 
             filekinds: FileKinds {
@@ -137,19 +125,6 @@ impl Colours {
                 executable:   Green.bold(),
             },
 
-            filetypes: FileTypes {
-                image:       Fixed(133).normal(),
-                video:       Fixed(135).normal(),
-                music:       Fixed(92).normal(),
-                lossless:    Fixed(93).normal(),
-                crypto:      Fixed(109).normal(),
-                document:    Fixed(105).normal(),
-                compressed:  Red.normal(),
-                temp:        Fixed(244).normal(),
-                immediate:   Yellow.bold().underline(),
-                compiled:    Fixed(137).normal(),
-            },
-
             perms: Permissions {
                 user_read:           Yellow.bold(),
                 user_write:          Red.bold(),
@@ -219,6 +194,83 @@ impl Colours {
 }
 
 
+impl Colours {
+    pub fn set_ls(&mut self, pair: &Pair) {
+        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(),
+             _   => {/* don’t change anything */},
+        }
+    }
+
+    pub fn set_exa(&mut self, pair: &Pair) {
+        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(),
+            "ue" => self.perms.user_execute_other = pair.to_style(),
+            "gr" => self.perms.group_read         = pair.to_style(),
+            "gw" => self.perms.group_write        = pair.to_style(),
+            "gx" => self.perms.group_execute      = pair.to_style(),
+            "tr" => self.perms.other_read         = pair.to_style(),
+            "tw" => self.perms.other_write        = pair.to_style(),
+            "tx" => self.perms.other_execute      = pair.to_style(),
+            "su" => self.perms.special_user_file  = pair.to_style(),
+            "sf" => self.perms.special_other      = pair.to_style(),
+            "xa" => self.perms.attribute          = pair.to_style(),
+
+            "sn" => self.size.numbers             = pair.to_style(),
+            "sb" => self.size.unit                = pair.to_style(),
+            "df" => self.size.major               = pair.to_style(),
+            "ds" => self.size.minor               = pair.to_style(),
+
+            "uu" => self.users.user_you           = pair.to_style(),
+            "un" => self.users.user_someone_else  = pair.to_style(),
+            "gu" => self.users.group_yours        = pair.to_style(),
+            "gn" => self.users.group_not_yours    = pair.to_style(),
+
+            "lc" => self.links.normal             = pair.to_style(),
+            "lm" => self.links.multi_link_file    = pair.to_style(),
+
+            "ga" => self.git.new                  = pair.to_style(),
+            "gm" => self.git.modified             = pair.to_style(),
+            "gd" => self.git.deleted              = pair.to_style(),
+            "gv" => self.git.renamed              = pair.to_style(),
+            "gt" => self.git.typechange           = pair.to_style(),
+
+            "xx" => self.punctuation              = pair.to_style(),
+            "da" => self.date                     = pair.to_style(),
+            "in" => self.inode                    = pair.to_style(),
+            "bl" => self.blocks                   = pair.to_style(),
+            "hd" => self.header                   = pair.to_style(),
+            "lp" => self.symlink_path             = pair.to_style(),
+            "cc" => self.control_char             = pair.to_style(),
+
+             _   => {/* still don’t change anything */},
+        }
+    }
+}
+
+
 impl render::BlocksColours for Colours {
     fn block_count(&self)  -> Style { self.blocks }
     fn no_blocks(&self)    -> Style { self.punctuation }
@@ -307,3 +359,12 @@ impl render::UserColours for Colours {
     fn someone_else(&self)  -> Style { self.users.user_someone_else }
 }
 
+impl FileNameColours for Colours {
+    fn broken_arrow(&self)    -> Style { self.broken_arrow }
+    fn broken_filename(&self) -> Style { self.broken_filename }
+    fn normal_arrow(&self)    -> Style { self.punctuation }
+    fn control_char(&self)    -> Style { self.control_char }
+    fn symlink_path(&self)    -> Style { self.symlink_path }
+    fn executable_file(&self) -> Style { self.filekinds.executable }
+}
+

+ 141 - 0
src/style/lsc.rs

@@ -0,0 +1,141 @@
+use std::ops::FnMut;
+
+use ansi_term::Style;
+use ansi_term::Colour::*;
+
+
+pub struct LSColors<'var>(pub &'var str);
+
+impl<'var> LSColors<'var> {
+    pub fn each_pair<C>(&mut self, mut callback: C) where C: FnMut(Pair<'var>) -> () {
+        for next in self.0.split(":") {
+            let bits = next.split("=")
+                           .take(3)
+                           .collect::<Vec<_>>();
+
+            if bits.len() == 2 && !bits[0].is_empty() && !bits[1].is_empty() {
+                callback(Pair { key: bits[0], value: bits[1] });
+            }
+        }
+    }
+}
+
+pub struct Pair<'var> {
+    pub key: &'var str,
+    pub value: &'var str,
+}
+
+impl<'var> Pair<'var> {
+    pub fn to_style(&self) -> Style {
+        let mut style = Style::default();
+
+        for num in self.value.split(";") {
+            match num {
+
+                // Bold and italic
+                "1"  => style = style.bold(),
+                "4"  => style = style.underline(),
+
+                // Foreground colours
+                "30" => style = style.fg(Black),
+                "31" => style = style.fg(Red),
+                "32" => style = style.fg(Green),
+                "33" => style = style.fg(Yellow),
+                "34" => style = style.fg(Blue),
+                "35" => style = style.fg(Purple),
+                "36" => style = style.fg(Cyan),
+                "37" => style = style.fg(White),
+
+                // Background colours
+                "40" => style = style.on(Black),
+                "41" => style = style.on(Red),
+                "42" => style = style.on(Green),
+                "43" => style = style.on(Yellow),
+                "44" => style = style.on(Blue),
+                "45" => style = style.on(Purple),
+                "46" => style = style.on(Cyan),
+                "47" => style = style.on(White),
+                 _    => {/* ignore the error and do nothing */},
+            }
+        }
+
+        style
+    }
+}
+
+
+#[cfg(test)]
+mod ansi_test {
+    use super::*;
+    use ansi_term::Style;
+
+    macro_rules! test {
+        ($name:ident: $input:expr => $result:expr) => {
+            #[test]
+            fn $name() {
+                assert_eq!(Pair { key: "", value: $input }.to_style(), $result);
+            }
+        };
+    }
+
+    // Styles
+    test!(bold:  "1"         => Style::default().bold());
+    test!(under: "4"         => Style::default().underline());
+    test!(both:  "1;4"       => Style::default().bold().underline());
+    test!(fg:    "31"        => Red.normal());
+    test!(bg:    "43"        => Style::default().on(Yellow));
+    test!(bfg:   "31;43"     => Red.on(Yellow));
+    test!(all:   "43;31;1;4" => Red.on(Yellow).bold().underline());
+    test!(again: "1;1;1;1;1" => Style::default().bold());
+
+    // Failure cases
+    test!(empty: ""          => Style::default());
+    test!(semis: ";;;;;;"    => Style::default());
+    test!(nines: "99999999"  => Style::default());
+    test!(word:  "GREEN"     => Style::default());
+}
+
+
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    macro_rules! test {
+        ($name:ident: $input:expr => $result:expr) => {
+            #[test]
+            fn $name() {
+                let mut lscs = Vec::new();
+                LSColors($input).each_pair(|p| lscs.push( (p.key.clone(), p.to_style()) ));
+                assert_eq!(lscs, $result.to_vec());
+            }
+        };
+    }
+
+    // Bad parses
+    test!(empty:    ""       => []);
+    test!(jibber:   "blah"   => []);
+
+    test!(equals:     "="    => []);
+    test!(starts:     "=di"  => []);
+    test!(ends:     "id="    => []);
+
+    // Foreground colours
+    test!(green:   "cb=32"   => [ ("cb", Green.normal()) ]);
+    test!(red:     "di=31"   => [ ("di", Red.normal()) ]);
+    test!(blue:    "la=34"   => [ ("la", Blue.normal()) ]);
+
+    // Background colours
+    test!(yellow:  "do=43"   => [ ("do", Style::default().on(Yellow)) ]);
+    test!(purple:  "re=45"   => [ ("re", Style::default().on(Purple)) ]);
+    test!(cyan:    "mi=46"   => [ ("mi", Style::default().on(Cyan)) ]);
+
+    // Bold and underline
+    test!(bold:    "fa=1"    => [ ("fa", Style::default().bold()) ]);
+    test!(under:   "so=4"    => [ ("so", Style::default().underline()) ]);
+    test!(both:    "la=1;4"  => [ ("la", Style::default().bold().underline()) ]);
+
+    // More and many
+    test!(more:  "me=43;21;55;34:yu=1;4;1"  => [ ("me", Blue.on(Yellow)), ("yu", Style::default().bold().underline()) ]);
+    test!(many:  "red=31:green=32:blue=34"  => [ ("red", Red.normal()), ("green", Green.normal()), ("blue", Blue.normal()) ]);
+}

+ 5 - 0
src/style/mod.rs

@@ -0,0 +1,5 @@
+mod colours;
+pub use self::colours::Colours;
+
+mod lsc;
+pub use self::lsc::LSColors;

+ 6 - 0
xtests/file-names-exts-bw

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

+ 6 - 0
xtests/file_names_bw

@@ -0,0 +1,6 @@
+ansi: [\u{1b}[34mblue\u{1b}[0m]  form-feed: [\u{c}]      new-line-dir: [\n]
+ascii: hello                     invalid-utf8-1: [�]     new-line: [\n]
+backspace: [\u{8}]               invalid-utf8-2: [�(]    return: [\r]
+bell: [\u{7}]                    invalid-utf8-3: [�(]    tab: [\t]
+emoji: [🆒]                      invalid-utf8-4: [�(�(]  utf-8: pâté
+escape: [\u{1b}]                 links                   vertical-tab: [\u{b}]

+ 8 - 0
xtests/run.sh

@@ -32,6 +32,10 @@ export EXA_STRICT="1"
 # We also don’t want to see reams and reams of debug output.
 export EXA_DEBUG=""
 
+# And default colours by default
+export LS_COLORS=""
+export EXA_COLORS=""
+
 
 # Check that no files were created more than a year ago.
 # Files not from the current year use a different date format, meaning
@@ -178,6 +182,10 @@ COLUMNS=80 $exa_binary --colour=always    $testcases/files -l | diff -q - $resul
 COLUMNS=80 $exa_binary --colour=never     $testcases/files -l | diff -q - $results/files_l_bw  || exit 1
 COLUMNS=80 $exa_binary --colour=automatic $testcases/files -l | diff -q - $results/files_l_bw  || exit 1
 
+# Switching colour off
+COLUMNS=80 $exa_binary --colour=never     $testcases/file-names      | diff -q - $results/file_names_bw       || exit 1
+COLUMNS=80 $exa_binary --colour=never     $testcases/file-names-exts | diff -q - $results/file-names-exts-bw  || exit 1
+
 
 # Git
 $exa $testcases/git/additions -l --git 2>&1 | diff -q - $results/git_additions  || exit 1