Ver Fonte

Merge branch 'view-options'

Benjamin Sago há 5 anos atrás
pai
commit
b05f18cae0

+ 1 - 1
src/info/filetype.rs

@@ -7,8 +7,8 @@
 use ansi_term::Style;
 
 use crate::fs::File;
-use crate::output::file_name::FileColours;
 use crate::output::icons::FileIcon;
+use crate::theme::FileColours;
 
 
 #[derive(Debug, Default, PartialEq)]

+ 40 - 15
src/main.rs

@@ -35,13 +35,14 @@ use crate::fs::feature::git::GitCache;
 use crate::fs::filter::GitIgnore;
 use crate::options::{Options, Vars, vars, OptionsResult};
 use crate::output::{escape, lines, grid, grid_details, details, View, Mode};
+use crate::theme::Theme;
 
 mod fs;
 mod info;
 mod logger;
 mod options;
 mod output;
-mod style;
+mod theme;
 
 
 fn main() {
@@ -61,7 +62,10 @@ fn main() {
 
             let git = git_options(&options, &input_paths);
             let writer = io::stdout();
-            let exa = Exa { options, writer, input_paths, git };
+
+            let console_width = options.view.width.actual_terminal_width();
+            let theme = options.theme.to_theme(console_width.is_some());
+            let exa = Exa { options, writer, input_paths, theme, console_width, git };
 
             match exa.run() {
                 Ok(exit_status) => {
@@ -114,6 +118,15 @@ pub struct Exa<'args> {
     /// names (anything that isn’t an option).
     pub input_paths: Vec<&'args OsStr>,
 
+    /// The theme that has been configured from the command-line options and
+    /// environment variables. If colours are disabled, this is a theme with
+    /// every style set to the default.
+    pub theme: Theme,
+
+    /// The detected width of the console. This is used to determine which
+    /// view to use.
+    pub console_width: Option<usize>,
+
     /// A global Git cache, if the option was passed in.
     /// This has to last the lifetime of the program, because the user might
     /// want to list several directories in the same repository.
@@ -241,45 +254,57 @@ impl<'args> Exa<'args> {
     }
 
     /// Prints the list of files using whichever view is selected.
-    /// For various annoying logistical reasons, each one handles
-    /// printing differently...
     fn print_files(&mut self, dir: Option<&Dir>, files: Vec<File<'_>>) -> io::Result<()> {
         if files.is_empty() {
             return Ok(());
         }
 
-        let View { ref mode, ref colours, ref style } = self.options.view;
+        let theme = &self.theme;
+        let View { ref mode, ref file_style, .. } = self.options.view;
 
-        match mode {
-            Mode::Lines(ref opts) => {
-                let r = lines::Render { files, colours, style, opts };
+        match (mode, self.console_width) {
+            (Mode::Grid(ref opts), Some(console_width)) => {
+                let r = grid::Render { files, theme, file_style, opts, console_width };
                 r.render(&mut self.writer)
             }
 
-            Mode::Grid(ref opts) => {
-                let r = grid::Render { files, colours, style, opts };
+            (Mode::Grid(_), None) |
+            (Mode::Lines,   _)    => {
+                let r = lines::Render { files, theme, file_style };
                 r.render(&mut self.writer)
             }
 
-            Mode::Details(ref opts) => {
+            (Mode::Details(ref opts), _) => {
                 let filter = &self.options.filter;
                 let recurse = self.options.dir_action.recurse_options();
 
                 let git_ignoring = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore;
                 let git = self.git.as_ref();
-                let r = details::Render { dir, files, colours, style, opts, filter, recurse, git_ignoring, git };
+                let r = details::Render { dir, files, theme, file_style, opts, filter, recurse, git_ignoring, git };
                 r.render(&mut self.writer)
             }
 
-            Mode::GridDetails(ref opts) => {
+            (Mode::GridDetails(ref opts), Some(console_width)) => {
                 let grid = &opts.grid;
-                let filter = &self.options.filter;
                 let details = &opts.details;
                 let row_threshold = opts.row_threshold;
 
+                let filter = &self.options.filter;
+                let git_ignoring = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore;
+                let git = self.git.as_ref();
+
+                let r = grid_details::Render { dir, files, theme, file_style, grid, details, filter, row_threshold, git_ignoring, git, console_width };
+                r.render(&mut self.writer)
+            }
+
+            (Mode::GridDetails(ref opts), None) => {
+                let opts = &opts.to_details_options();
+                let filter = &self.options.filter;
+                let recurse = self.options.dir_action.recurse_options();
                 let git_ignoring = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore;
+
                 let git = self.git.as_ref();
-                let r = grid_details::Render { dir, files, colours, style, grid, details, filter, row_threshold, git_ignoring, git };
+                let r = details::Render { dir, files, theme, file_style, opts, filter, recurse, git_ignoring, git };
                 r.render(&mut self.writer)
             }
         }

+ 5 - 3
src/options/dir_action.rs

@@ -12,7 +12,7 @@ impl DirAction {
     /// There are three possible actions, and they overlap somewhat: the
     /// `--tree` flag is another form of recursion, so those two are allowed
     /// to both be present, but the `--list-dirs` flag is used separately.
-    pub fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
+    pub fn deduce(matches: &MatchedFlags<'_>, can_tree: bool) -> Result<Self, OptionsError> {
         let recurse = matches.has(&flags::RECURSE)?;
         let as_file = matches.has(&flags::LIST_DIRS)?;
         let tree    = matches.has(&flags::TREE)?;
@@ -30,7 +30,9 @@ impl DirAction {
             }
         }
 
-        if tree {
+        if tree && can_tree {
+            // Tree is only appropriate in details mode, so this has to
+            // examine the View, which should have already been deduced by now
             Ok(Self::Recurse(RecurseOptions::deduce(matches, true)?))
         }
         else if recurse {
@@ -83,7 +85,7 @@ mod test {
                 use crate::options::test::Strictnesses::*;
 
                 static TEST_ARGS: &[&Arg] = &[&flags::RECURSE, &flags::LIST_DIRS, &flags::TREE, &flags::LEVEL ];
-                for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) {
+                for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf, true)) {
                     assert_eq!(result, $result);
                 }
             }

+ 23 - 0
src/options/file_name.rs

@@ -0,0 +1,23 @@
+use crate::options::{flags, OptionsError};
+use crate::options::parser::MatchedFlags;
+
+use crate::output::file_name::{Options, Classify};
+
+
+impl Options {
+    pub fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
+        let classify = Classify::deduce(matches)?;
+        let icons = matches.has(&flags::ICONS)?;
+
+        Ok(Self { classify, icons })
+    }
+}
+
+impl Classify {
+    fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
+        let flagged = matches.has(&flags::CLASSIFY)?;
+
+        if flagged { Ok(Self::AddFileIndicators) }
+              else { Ok(Self::JustFilenames) }
+    }
+}

+ 14 - 5
src/options/mod.rs

@@ -74,11 +74,13 @@ use std::ffi::OsStr;
 use crate::fs::dir_action::DirAction;
 use crate::fs::filter::{FileFilter, GitIgnore};
 use crate::output::{View, Mode, details, grid_details};
+use crate::theme::Options as ThemeOptions;
 
 mod dir_action;
+mod file_name;
 mod filter;
 mod flags;
-mod style;
+mod theme;
 mod view;
 
 mod error;
@@ -109,8 +111,14 @@ pub struct Options {
     /// How to sort and filter files before outputting them.
     pub filter: FileFilter,
 
-    /// The type of output to use (lines, grid, or details).
+    /// The user’s preference of view to use (lines, grid, details, or
+    /// grid-details) along with the options on how to render file names.
+    /// If the view requires the terminal to have a width, and there is no
+    /// width, then the view will be downgraded.
     pub view: View,
+
+    /// The options to make up the styles of the UI and file names.
+    pub theme: ThemeOptions,
 }
 
 impl Options {
@@ -168,11 +176,12 @@ impl Options {
     /// Determines the complete set of options based on the given command-line
     /// arguments, after they’ve been parsed.
     fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
-        let dir_action = DirAction::deduce(matches)?;
-        let filter = FileFilter::deduce(matches)?;
         let view = View::deduce(matches, vars)?;
+        let dir_action = DirAction::deduce(matches, matches!(view.mode, Mode::Details(_)))?;
+        let filter = FileFilter::deduce(matches)?;
+        let theme = ThemeOptions::deduce(matches, vars)?;
 
-        Ok(Self { dir_action, view, filter })
+        Ok(Self { dir_action, filter, view, theme })
     }
 }
 

+ 12 - 4
src/options/parser.rs

@@ -394,13 +394,21 @@ impl<'a> MatchedFlags<'a> {
                         else { Err(OptionsError::Duplicate(all[0].0, all[1].0)) }
         }
         else {
-            let any = self.flags.iter().rev()
-                          .find(|tuple| tuple.1.is_none() && predicate(&tuple.0))
-                          .map(|tuple| &tuple.0);
-            Ok(any)
+            Ok(self.has_where_any(predicate))
         }
     }
 
+    /// Returns the first found argument that satisfies the predicate, or
+    /// nothing if none is found, with strict mode having no effect.
+    ///
+    /// You’ll have to test the resulting flag to see which argument it was.
+    pub fn has_where_any<P>(&self, predicate: P) -> Option<&Flag>
+    where P: Fn(&Flag) -> bool {
+        self.flags.iter().rev()
+            .find(|tuple| tuple.1.is_none() && predicate(&tuple.0))
+            .map(|tuple| &tuple.0)
+    }
+
     // This code could probably be better.
     // Both ‘has’ and ‘get’ immediately begin with a conditional, which makes
     // me think the functionality could be moved to inside Strictness.

+ 159 - 0
src/options/theme.rs

@@ -0,0 +1,159 @@
+use crate::options::{flags, vars, Vars, OptionsError};
+use crate::options::parser::MatchedFlags;
+use crate::theme::{Options, UseColours, ColourScale, Definitions};
+
+
+impl Options {
+    pub fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
+        let use_colours = UseColours::deduce(matches)?;
+        let colour_scale = ColourScale::deduce(matches)?;
+
+        let definitions = if use_colours == UseColours::Never {
+                Definitions::default()
+            }
+            else {
+                Definitions::deduce(vars)
+            };
+
+        Ok(Self { use_colours, colour_scale, definitions })
+    }
+}
+
+
+impl UseColours {
+    fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
+        let word = match matches.get_where(|f| f.matches(&flags::COLOR) || f.matches(&flags::COLOUR))? {
+            Some(w)  => w,
+            None     => return Ok(Self::Automatic),
+        };
+
+        if word == "always" {
+            Ok(Self::Always)
+        }
+        else if word == "auto" || word == "automatic" {
+            Ok(Self::Automatic)
+        }
+        else if word == "never" {
+            Ok(Self::Never)
+        }
+        else {
+            Err(OptionsError::BadArgument(&flags::COLOR, word.into()))
+        }
+    }
+}
+
+
+impl ColourScale {
+    fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
+        if matches.has_where(|f| f.matches(&flags::COLOR_SCALE) || f.matches(&flags::COLOUR_SCALE))?.is_some() {
+            Ok(Self::Gradient)
+        }
+        else {
+            Ok(Self::Fixed)
+        }
+    }
+}
+
+
+impl Definitions {
+    fn deduce<V: Vars>(vars: &V) -> Self {
+        let ls =  vars.get(vars::LS_COLORS) .map(|e| e.to_string_lossy().to_string());
+        let exa = vars.get(vars::EXA_COLORS).map(|e| e.to_string_lossy().to_string());
+        Self { ls, exa }
+    }
+}
+
+
+#[cfg(test)]
+mod terminal_test {
+    use super::*;
+    use std::ffi::OsString;
+    use crate::options::flags;
+    use crate::options::parser::{Flag, Arg};
+
+    use crate::options::test::parse_for_test;
+    use crate::options::test::Strictnesses::*;
+
+    static TEST_ARGS: &[&Arg] = &[ &flags::COLOR,       &flags::COLOUR,
+                                   &flags::COLOR_SCALE, &flags::COLOUR_SCALE, ];
+
+    macro_rules! test {
+        ($name:ident:  $type:ident <- $inputs:expr;  $stricts:expr => $result:expr) => {
+            #[test]
+            fn $name() {
+                for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) {
+                    assert_eq!(result, $result);
+                }
+            }
+        };
+
+        ($name:ident:  $type:ident <- $inputs:expr;  $stricts:expr => err $result:expr) => {
+            #[test]
+            fn $name() {
+                for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) {
+                    assert_eq!(result.unwrap_err(), $result);
+                }
+            }
+        };
+    }
+
+    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> {
+            if name == vars::LS_COLORS && ! self.ls.is_empty() {
+                Some(OsString::from(self.ls.clone()))
+            }
+            else if name == vars::EXA_COLORS && ! self.exa.is_empty() {
+                Some(OsString::from(self.exa.clone()))
+            }
+            else {
+                None
+            }
+        }
+    }
+
+
+
+    // Default
+    test!(empty:         UseColours <- [];                     Both => Ok(UseColours::Automatic));
+
+    // --colour
+    test!(u_always:      UseColours <- ["--colour=always"];    Both => Ok(UseColours::Always));
+    test!(u_auto:        UseColours <- ["--colour", "auto"];   Both => Ok(UseColours::Automatic));
+    test!(u_never:       UseColours <- ["--colour=never"];     Both => Ok(UseColours::Never));
+
+    // --color
+    test!(no_u_always:   UseColours <- ["--color", "always"];  Both => Ok(UseColours::Always));
+    test!(no_u_auto:     UseColours <- ["--color=auto"];       Both => Ok(UseColours::Automatic));
+    test!(no_u_never:    UseColours <- ["--color", "never"];   Both => Ok(UseColours::Never));
+
+    // Errors
+    test!(no_u_error:    UseColours <- ["--color=upstream"];   Both => err OptionsError::BadArgument(&flags::COLOR, OsString::from("upstream")));  // the error is for --color
+    test!(u_error:       UseColours <- ["--colour=lovers"];    Both => err OptionsError::BadArgument(&flags::COLOR, OsString::from("lovers")));    // and so is this one!
+
+    // Overriding
+    test!(overridden_1:  UseColours <- ["--colour=auto", "--colour=never"];  Last => Ok(UseColours::Never));
+    test!(overridden_2:  UseColours <- ["--color=auto",  "--colour=never"];  Last => Ok(UseColours::Never));
+    test!(overridden_3:  UseColours <- ["--colour=auto", "--color=never"];   Last => Ok(UseColours::Never));
+    test!(overridden_4:  UseColours <- ["--color=auto",  "--color=never"];   Last => Ok(UseColours::Never));
+
+    test!(overridden_5:  UseColours <- ["--colour=auto", "--colour=never"];  Complain => err OptionsError::Duplicate(Flag::Long("colour"), Flag::Long("colour")));
+    test!(overridden_6:  UseColours <- ["--color=auto",  "--colour=never"];  Complain => err OptionsError::Duplicate(Flag::Long("color"),  Flag::Long("colour")));
+    test!(overridden_7:  UseColours <- ["--colour=auto", "--color=never"];   Complain => err OptionsError::Duplicate(Flag::Long("colour"), Flag::Long("color")));
+    test!(overridden_8:  UseColours <- ["--color=auto",  "--color=never"];   Complain => err OptionsError::Duplicate(Flag::Long("color"),  Flag::Long("color")));
+
+    test!(scale_1:  ColourScale <- ["--color-scale", "--colour-scale"];   Last => Ok(ColourScale::Gradient));
+    test!(scale_2:  ColourScale <- ["--color-scale",                 ];   Last => Ok(ColourScale::Gradient));
+    test!(scale_3:  ColourScale <- [                 "--colour-scale"];   Last => Ok(ColourScale::Gradient));
+    test!(scale_4:  ColourScale <- [                                 ];   Last => Ok(ColourScale::Fixed));
+
+    test!(scale_5:  ColourScale <- ["--color-scale", "--colour-scale"];   Complain => err OptionsError::Duplicate(Flag::Long("color-scale"),  Flag::Long("colour-scale")));
+    test!(scale_6:  ColourScale <- ["--color-scale",                 ];   Complain => Ok(ColourScale::Gradient));
+    test!(scale_7:  ColourScale <- [                 "--colour-scale"];   Complain => Ok(ColourScale::Gradient));
+    test!(scale_8:  ColourScale <- [                                 ];   Complain => Ok(ColourScale::Fixed));
+}

+ 107 - 143
src/options/view.rs

@@ -1,120 +1,86 @@
-use lazy_static::lazy_static;
-
 use crate::fs::feature::xattr;
 use crate::options::{flags, OptionsError, Vars};
 use crate::options::parser::MatchedFlags;
-use crate::output::{View, Mode, grid, details, lines};
+use crate::output::{View, Mode, TerminalWidth, grid, details};
 use crate::output::grid_details::{self, RowThreshold};
+use crate::output::file_name::Options as FileStyle;
 use crate::output::table::{TimeTypes, SizeFormat, Columns, Options as TableOptions};
 use crate::output::time::TimeFormat;
 
 
 impl View {
-
-    /// Determine which view to use and all of that view’s arguments.
     pub fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
-        use crate::options::style::Styles;
-
         let mode = Mode::deduce(matches, vars)?;
-        let Styles { colours, style } = Styles::deduce(matches, vars, || *TERM_WIDTH)?;
-        Ok(Self { mode, colours, style })
+        let width = TerminalWidth::deduce(vars)?;
+        let file_style = FileStyle::deduce(matches)?;
+        Ok(Self { mode, width, file_style })
     }
 }
 
 
 impl Mode {
 
-    /// Determine the mode from the command-line arguments.
+    /// Determine which viewing mode to use based on the user’s options.
+    ///
+    /// As with the other options, arguments are scanned right-to-left and the
+    /// first flag found is matched, so `exa --oneline --long` will pick a
+    /// details view, and `exa --long --oneline` will pick the lines view.
+    ///
+    /// This is complicated a little by the fact that `--grid` and `--tree`
+    /// can also combine with `--long`, so care has to be taken to use the
     pub fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
-        let long = || {
-            if matches.is_strict() && matches.has(&flags::ACROSS)? && ! matches.has(&flags::GRID)? {
-                Err(OptionsError::Useless(&flags::ACROSS, true, &flags::LONG))
-            }
-            else if matches.is_strict() && matches.has(&flags::ONE_LINE)? {
-                Err(OptionsError::Useless(&flags::ONE_LINE, true, &flags::LONG))
-            }
-            else {
-                Ok(details::Options {
-                    table: Some(TableOptions::deduce(matches, vars)?),
-                    header: matches.has(&flags::HEADER)?,
-                    xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?,
-                    icons: matches.has(&flags::ICONS)?,
-                })
-            }
-        };
-
-        let other_options_scan = || {
-            if let Some(width) = TerminalWidth::deduce(vars)?.width() {
-                if matches.has(&flags::ONE_LINE)? {
-                    if matches.is_strict() && matches.has(&flags::ACROSS)? {
-                        Err(OptionsError::Useless(&flags::ACROSS, true, &flags::ONE_LINE))
-                    }
-                    else {
-                        let lines = lines::Options { icons: matches.has(&flags::ICONS)? };
-                        Ok(Self::Lines(lines))
-                    }
-                }
-                else if matches.has(&flags::TREE)? {
-                    let details = details::Options {
-                        table: None,
-                        header: false,
-                        xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?,
-                        icons: matches.has(&flags::ICONS)?,
-                    };
-
-                    Ok(Self::Details(details))
-                }
-                else {
-                    let grid = grid::Options {
-                        across: matches.has(&flags::ACROSS)?,
-                        console_width: width,
-                        icons: matches.has(&flags::ICONS)?,
-                    };
-
-                    Ok(Self::Grid(grid))
-                }
-            }
-
-            // If the terminal width couldn’t be matched for some reason, such
-            // as the program’s stdout being connected to a file, then
-            // fallback to the lines or details view.
-            else if matches.has(&flags::TREE)? {
-                let details = details::Options {
-                    table: None,
-                    header: false,
-                    xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?,
-                    icons: matches.has(&flags::ICONS)?,
-                };
-
-                Ok(Self::Details(details))
-            }
-            else if matches.has(&flags::LONG)? {
-                let details = long()?;
-                Ok(Self::Details(details))
-            } else {
-                let lines = lines::Options { icons: matches.has(&flags::ICONS)?, };
-                Ok(Self::Lines(lines))
+        let flag = matches.has_where_any(|f| f.matches(&flags::LONG) || f.matches(&flags::ONE_LINE)
+                                          || f.matches(&flags::GRID) || f.matches(&flags::TREE));
+
+        let flag = match flag {
+            Some(f) => f,
+            None => {
+                Self::strict_check_long_flags(matches)?;
+                let grid = grid::Options::deduce(matches)?;
+                return Ok(Self::Grid(grid));
             }
         };
 
-        if matches.has(&flags::LONG)? {
-            let details = long()?;
-            if matches.has(&flags::GRID)? {
-                let other_options_mode = other_options_scan()?;
-                if let Self::Grid(grid) = other_options_mode {
-                    let row_threshold = RowThreshold::deduce(vars)?;
-                    let opts = grid_details::Options { grid, details, row_threshold };
-                    return Ok(Self::GridDetails(opts));
-                }
-                else {
-                    return Ok(other_options_mode);
-                }
+        if flag.matches(&flags::LONG)
+        || (flag.matches(&flags::TREE) && matches.has(&flags::LONG)?)
+        || (flag.matches(&flags::GRID) && matches.has(&flags::LONG)?)
+        {
+            let _ = matches.has(&flags::LONG)?;
+            let details = details::Options::deduce_long(matches, vars)?;
+
+            let flag = matches.has_where_any(|f| f.matches(&flags::GRID) || f.matches(&flags::TREE));
+
+            if flag.is_some() && flag.unwrap().matches(&flags::GRID) {
+                let _ = matches.has(&flags::GRID)?;
+                let grid = grid::Options::deduce(matches)?;
+                let row_threshold = RowThreshold::deduce(vars)?;
+                let grid_details = grid_details::Options { grid, details, row_threshold };
+                return Ok(Self::GridDetails(grid_details));
             }
             else {
+                // the --tree case is handled by the DirAction parser later
                 return Ok(Self::Details(details));
             }
         }
 
+        Self::strict_check_long_flags(matches)?;
+
+        if flag.matches(&flags::TREE) {
+            let _ = matches.has(&flags::TREE)?;
+            let details = details::Options::deduce_tree(matches)?;
+            return Ok(Self::Details(details));
+        }
+
+        if flag.matches(&flags::ONE_LINE) {
+            let _ = matches.has(&flags::ONE_LINE)?;
+            return Ok(Self::Lines);
+        }
+
+        let grid = grid::Options::deduce(matches)?;
+        Ok(Self::Grid(grid))
+    }
+
+    fn strict_check_long_flags(matches: &MatchedFlags<'_>) -> Result<(), OptionsError> {
         // If --long hasn’t been passed, then check if we need to warn the
         // user about flags that won’t have any effect.
         if matches.is_strict() {
@@ -129,36 +95,57 @@ impl Mode {
                 return Err(OptionsError::Useless(&flags::GIT, false, &flags::LONG));
             }
             else if matches.has(&flags::LEVEL)? && ! matches.has(&flags::RECURSE)? && ! matches.has(&flags::TREE)? {
-                // TODO: I’m not sure if the code even gets this far.
-                // There is an identical check in dir_action
                 return Err(OptionsError::Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE));
             }
         }
 
-        other_options_scan()
+        Ok(())
     }
 }
 
 
-/// The width of the terminal requested by the user.
-#[derive(PartialEq, Debug, Copy, Clone)]
-enum TerminalWidth {
+impl grid::Options {
+    fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
+        let grid = grid::Options {
+            across: matches.has(&flags::ACROSS)?,
+        };
+
+        Ok(grid)
+    }
+}
+
+
+impl details::Options {
+    fn deduce_tree(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
+        let details = details::Options {
+            table: None,
+            header: false,
+            xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?,
+        };
 
-    /// The user requested this specific number of columns.
-    Set(usize),
+        Ok(details)
+    }
 
-    /// The terminal was found to have this number of columns.
-    Terminal(usize),
+    fn deduce_long<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
+        if matches.is_strict() {
+            if matches.has(&flags::ACROSS)? && ! matches.has(&flags::GRID)? {
+                return Err(OptionsError::Useless(&flags::ACROSS, true, &flags::LONG));
+            }
+            else if matches.has(&flags::ONE_LINE)? {
+                return Err(OptionsError::Useless(&flags::ONE_LINE, true, &flags::LONG));
+            }
+        }
 
-    /// The user didn’t request any particular terminal width.
-    Unset,
+        Ok(details::Options {
+            table: Some(TableOptions::deduce(matches, vars)?),
+            header: matches.has(&flags::HEADER)?,
+            xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?,
+        })
+    }
 }
 
-impl TerminalWidth {
 
-    /// Determine a requested terminal width from the command-line arguments.
-    ///
-    /// Returns an error if a requested width doesn’t parse to an integer.
+impl TerminalWidth {
     fn deduce<V: Vars>(vars: &V) -> Result<Self, OptionsError> {
         use crate::options::vars;
 
@@ -168,28 +155,14 @@ impl TerminalWidth {
                 Err(e)     => Err(OptionsError::FailedParse(e)),
             }
         }
-        else if let Some(width) = *TERM_WIDTH {
-            Ok(Self::Terminal(width))
-        }
         else {
-            Ok(Self::Unset)
-        }
-    }
-
-    fn width(self) -> Option<usize> {
-        match self {
-            Self::Set(width)       |
-            Self::Terminal(width)  => Some(width),
-            Self::Unset            => None,
+            Ok(Self::Automatic)
         }
     }
 }
 
 
 impl RowThreshold {
-
-    /// Determine whether to use a row threshold based on the given
-    /// environment variables.
     fn deduce<V: Vars>(vars: &V) -> Result<Self, OptionsError> {
         use crate::options::vars;
 
@@ -357,20 +330,6 @@ impl TimeTypes {
 }
 
 
-// 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.
-lazy_static! {
-    static ref TERM_WIDTH: Option<usize> = {
-        // All of stdin, stdout, and stderr could not be connected to a
-        // terminal, but we’re only interested in stdout because it’s
-        // where the output goes.
-        use term_size::dimensions_stdout;
-        dimensions_stdout().map(|t| t.0)
-    };
-}
-
-
 #[cfg(test)]
 mod test {
     use super::*;
@@ -383,10 +342,10 @@ mod test {
 
     static TEST_ARGS: &[&Arg] = &[ &flags::BINARY, &flags::BYTES,    &flags::TIME_STYLE,
                                    &flags::TIME,   &flags::MODIFIED, &flags::CHANGED,
-                                   &flags::CREATED, &flags::ACCESSED, &flags::ICONS,
+                                   &flags::CREATED, &flags::ACCESSED,
                                    &flags::HEADER, &flags::GROUP,  &flags::INODE, &flags::GIT,
                                    &flags::LINKS,  &flags::BLOCKS, &flags::LONG,  &flags::LEVEL,
-                                   &flags::GRID,   &flags::ACROSS, &flags::ONE_LINE ];
+                                   &flags::GRID,   &flags::ACROSS, &flags::ONE_LINE, &flags::TREE ];
 
     macro_rules! test {
 
@@ -561,7 +520,6 @@ mod test {
         use super::*;
 
         use crate::output::grid::Options as GridOptions;
-        use crate::output::lines::Options as LineOptions;
 
 
         // Default
@@ -572,12 +530,10 @@ mod test {
         test!(grid:          Mode <- ["--grid"], None;    Both => like Ok(Mode::Grid(GridOptions { across: false, .. })));
         test!(across:        Mode <- ["--across"], None;  Both => like Ok(Mode::Grid(GridOptions { across: true,  .. })));
         test!(gracross:      Mode <- ["-xG"], None;       Both => like Ok(Mode::Grid(GridOptions { across: true,  .. })));
-        test!(icons:         Mode <- ["--icons"], None;   Both => like Ok(Mode::Grid(GridOptions { icons: true,   .. })));
 
         // Lines views
-        test!(lines:         Mode <- ["--oneline"], None;     Both => like Ok(Mode::Lines(LineOptions { .. })));
-        test!(prima:         Mode <- ["-1"], None;            Both => like Ok(Mode::Lines(LineOptions { .. })));
-        test!(line_icon:     Mode <- ["-1", "--icons"], None; Both => like Ok(Mode::Lines(LineOptions { icons: true })));
+        test!(lines:         Mode <- ["--oneline"], None;     Both => like Ok(Mode::Lines));
+        test!(prima:         Mode <- ["-1"], None;            Both => like Ok(Mode::Lines));
 
         // Details views
         test!(long:          Mode <- ["--long"], None;    Both => like Ok(Mode::Details(_)));
@@ -589,7 +545,6 @@ mod test {
 
         // Options that do nothing with --long
         test!(long_across:   Mode <- ["--long", "--across"],   None;  Last => like Ok(Mode::Details(_)));
-        test!(long_oneline:  Mode <- ["--long", "--oneline"],  None;  Last => like Ok(Mode::Details(_)));
 
         // Options that do nothing without --long
         test!(just_header:   Mode <- ["--header"], None;  Last => like Ok(Mode::Grid(_)));
@@ -613,5 +568,14 @@ mod test {
 
         #[cfg(feature = "git")]
         test!(just_git_2:    Mode <- ["--git"],    None;  Complain => err OptionsError::Useless(&flags::GIT,    false, &flags::LONG));
+
+        // Contradictions and combinations
+        test!(lgo:           Mode <- ["--long", "--grid", "--oneline"], None;  Both => like Ok(Mode::Lines));
+        test!(lgt:           Mode <- ["--long", "--grid", "--tree"],    None;  Both => like Ok(Mode::Details(_)));
+        test!(tgl:           Mode <- ["--tree", "--grid", "--long"],    None;  Both => like Ok(Mode::GridDetails(_)));
+        test!(tlg:           Mode <- ["--tree", "--long", "--grid"],    None;  Both => like Ok(Mode::GridDetails(_)));
+        test!(ot:            Mode <- ["--oneline", "--tree"],           None;  Both => like Ok(Mode::Details(_)));
+        test!(og:            Mode <- ["--oneline", "--grid"],           None;  Both => like Ok(Mode::Grid(_)));
+        test!(tg:            Mode <- ["--tree", "--grid"],              None;  Both => like Ok(Mode::Grid(_)));
     }
 }

+ 19 - 19
src/output/details.rs

@@ -73,12 +73,12 @@ use crate::fs::dir_action::RecurseOptions;
 use crate::fs::feature::git::GitCache;
 use crate::fs::feature::xattr::{Attribute, FileAttributes};
 use crate::fs::filter::FileFilter;
-use crate::style::Colours;
 use crate::output::cell::TextCell;
 use crate::output::icons::painted_icon;
-use crate::output::file_name::FileStyle;
+use crate::output::file_name::Options as FileStyle;
 use crate::output::table::{Table, Options as TableOptions, Row as TableRow};
 use crate::output::tree::{TreeTrunk, TreeParams, TreeDepth};
+use crate::theme::Theme;
 
 
 /// With the **Details** view, the output gets formatted into columns, with
@@ -106,17 +106,14 @@ pub struct Options {
 
     /// Whether to show each file’s extended attributes.
     pub xattr: bool,
-
-    /// Whether icons mode is enabled.
-    pub icons: bool,
 }
 
 
 pub struct Render<'a> {
     pub dir: Option<&'a Dir>,
     pub files: Vec<File<'a>>,
-    pub colours: &'a Colours,
-    pub style: &'a FileStyle,
+    pub theme: &'a Theme,
+    pub file_style: &'a FileStyle,
     pub opts: &'a Options,
 
     /// Whether to recurse through directories with a tree view, and if so,
@@ -162,7 +159,7 @@ impl<'a> Render<'a> {
                 (None,    _)        => {/* Keep Git how it is */},
             }
 
-            let mut table = Table::new(table, self.git, self.colours);
+            let mut table = Table::new(table, self.git, &self.theme);
 
             if self.opts.header {
                 let header = table.header_row();
@@ -256,18 +253,21 @@ impl<'a> Render<'a> {
                     }
 
                     let mut dir = None;
-
                     if let Some(r) = self.recurse {
                         if file.is_directory() && r.tree && ! r.is_too_deep(depth.0) {
                             match file.to_dir() {
-                                Ok(d)   => { dir = Some(d); },
-                                Err(e)  => { errors.push((e, None)) },
+                                Ok(d) => {
+                                    dir = Some(d);
+                                }
+                                Err(e) => {
+                                    errors.push((e, None));
+                                }
                             }
                         }
                     };
 
-                    let icon = if self.opts.icons { Some(painted_icon(file, self.style)) }
-                                             else { None };
+                    let icon = if self.file_style.icons { Some(painted_icon(file, self.theme)) }
+                                                   else { None };
 
                     let egg = Egg { table_row, xattrs, errors, dir, file, icon };
                     unsafe { std::ptr::write(file_eggs.lock().unwrap()[idx].as_mut_ptr(), egg) }
@@ -292,7 +292,7 @@ impl<'a> Render<'a> {
                 name_cell.push(ANSIGenericString::from(icon), 2)
             }
 
-            let style = self.style.for_file(egg.file, self.colours)
+            let style = self.file_style.for_file(egg.file, self.theme)
                             .with_link_paths()
                             .paint()
                             .promote();
@@ -355,7 +355,7 @@ impl<'a> Render<'a> {
         Row {
             tree:     TreeParams::new(TreeDepth::root(), false),
             cells:    Some(header),
-            name:     TextCell::paint_str(self.colours.header, "Name"),
+            name:     TextCell::paint_str(self.theme.ui.header, "Name"),
         }
     }
 
@@ -369,12 +369,12 @@ impl<'a> Render<'a> {
 
         // TODO: broken_symlink() doesn’t quite seem like the right name for
         // the style that’s being used here. Maybe split it in two?
-        let name = TextCell::paint(self.colours.broken_symlink(), error_message);
+        let name = TextCell::paint(self.theme.broken_symlink(), error_message);
         Row { cells: None, name, tree }
     }
 
     fn render_xattr(&self, xattr: &Attribute, tree: TreeParams) -> Row {
-        let name = TextCell::paint(self.colours.perms.attribute, format!("{} (len {})", xattr.name, xattr.size));
+        let name = TextCell::paint(self.theme.ui.perms.attribute, format!("{} (len {})", xattr.name, xattr.size));
         Row { cells: None, name, tree }
     }
 
@@ -388,7 +388,7 @@ impl<'a> Render<'a> {
             total_width: table.widths().total(),
             table,
             inner: rows.into_iter(),
-            tree_style: self.colours.punctuation,
+            tree_style: self.theme.ui.punctuation,
         }
     }
 
@@ -396,7 +396,7 @@ impl<'a> Render<'a> {
         Iter {
             tree_trunk: TreeTrunk::default(),
             inner: rows.into_iter(),
-            tree_style: self.colours.punctuation,
+            tree_style: self.theme.ui.punctuation,
         }
     }
 }

+ 25 - 63
src/output/file_name.rs

@@ -1,5 +1,4 @@
 use std::fmt::Debug;
-use std::marker::Sync;
 use std::path::Path;
 
 use ansi_term::{ANSIString, Style};
@@ -11,34 +10,32 @@ use crate::output::render::FiletypeColours;
 
 
 /// Basically a file name factory.
-#[derive(Debug)]
-pub struct FileStyle {
+#[derive(Debug, Copy, Clone)]
+pub struct Options {
 
     /// Whether to append file class characters to file names.
     pub classify: Classify,
 
-    /// Mapping of file extensions to colours, to highlight regular files.
-    pub exts: Box<dyn FileColours>,
+    /// Whether to prepend icon characters before file names.
+    pub icons: bool,
 }
 
-impl FileStyle {
+impl Options {
 
     /// Create a new `FileName` that prints the given file’s name, painting it
     /// with the remaining arguments.
-    pub fn for_file<'a, 'dir, C: Colours>(&'a self, file: &'a File<'dir>, colours: &'a C) -> FileName<'a, 'dir, C> {
+    pub fn for_file<'a, 'dir, C>(self, file: &'a File<'dir>, colours: &'a C) -> FileName<'a, 'dir, C> {
         FileName {
             file,
             colours,
             link_style: LinkStyle::JustFilenames,
-            classify:   self.classify,
-            exts:       &*self.exts,
+            options:    self,
             target:     if file.is_link() { Some(file.link_target()) }
                                      else { None }
         }
     }
 }
 
-
 /// When displaying a file name, there needs to be some way to handle broken
 /// links, depending on how long the resulting Cell can be.
 #[derive(PartialEq, Debug, Copy, Clone)]
@@ -76,7 +73,7 @@ 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,  C: Colours> {
+pub struct FileName<'a, 'dir, C> {
 
     /// A reference to the file that we’re getting the name of.
     file: &'a File<'dir>,
@@ -85,20 +82,15 @@ pub struct FileName<'a, 'dir,  C: Colours> {
     colours: &'a C,
 
     /// The file that this file points to if it’s a link.
-    target: Option<FileTarget<'dir>>,
+    target: Option<FileTarget<'dir>>,  // todo: remove?
 
     /// How to handle displaying links.
     link_style: LinkStyle,
 
-    /// Whether to append file class characters to file names.
-    classify: Classify,
-
-    /// Mapping of file extensions to colours, to highlight regular files.
-    exts: &'a dyn FileColours,
+    options: Options,
 }
 
-
-impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
+impl<'a, 'dir, C> FileName<'a, 'dir, C> {
 
     /// Sets the flag on this file name to display link targets with an
     /// arrow followed by their path.
@@ -106,6 +98,9 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
         self.link_style = LinkStyle::FullLinkPaths;
         self
     }
+}
+
+impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
 
     /// Paints the name of the file using the colours, resulting in a vector
     /// of coloured cells that can be printed to the terminal.
@@ -146,13 +141,17 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
                     }
 
                     if ! target.name.is_empty() {
+                        let target_options = Options {
+                            classify: Classify::JustFilenames,
+                            icons: false,
+                        };
+
                         let target = FileName {
                             file: target,
                             colours: self.colours,
                             target: None,
                             link_style: LinkStyle::FullLinkPaths,
-                            classify: Classify::JustFilenames,
-                            exts: self.exts,
+                            options: target_options,
                         };
 
                         for bit in target.coloured_file_name() {
@@ -179,7 +178,7 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
                 }
             }
         }
-        else if let Classify::AddFileIndicators = self.classify {
+        else if let Classify::AddFileIndicators = self.options.classify {
             if let Some(class) = self.classify_char() {
                 bits.push(Style::default().paint(class));
             }
@@ -188,7 +187,6 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
         bits.into()
     }
 
-
     /// Adds the bits of the parent path to the given bits vector.
     /// The path gets its characters escaped based on the colours.
     fn add_parent_bits(&self, bits: &mut Vec<ANSIString<'_>>, parent: &Path) {
@@ -208,7 +206,6 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
         }
     }
 
-
     /// The character to be displayed after a file when classifying is on, if
     /// the file’s type has one associated with it.
     fn classify_char(&self) -> Option<&'static str> {
@@ -232,7 +229,6 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
         }
     }
 
-
     /// Returns at least one ANSI-highlighted string representing this file’s
     /// name using the given set of colours.
     ///
@@ -257,7 +253,6 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
         bits
     }
 
-
     /// Figures out which colour to paint the filename part of the output,
     /// depending on which “type” of file it appears to be — either from the
     /// class on the filesystem or from its name. (Or the broken link colour,
@@ -271,13 +266,7 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
             }
         }
 
-        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 {
+        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(),
@@ -286,8 +275,8 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
             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,
-        })
+            _                            => self.colours.colour_file(self.file),
+        }
     }
 }
 
@@ -319,33 +308,6 @@ pub trait Colours: FiletypeColours {
 
     /// The style to paint a file that has its executable bit set.
     fn executable_file(&self) -> Style;
-}
-
 
-// needs Debug because FileStyle derives it
-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
-    }
-}
-
-// 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))
-    }
+    fn colour_file(&self, file: &File<'_>) -> Style;
 }

+ 15 - 16
src/output/grid.rs

@@ -4,20 +4,18 @@ use term_grid as tg;
 
 use crate::fs::File;
 use crate::output::cell::DisplayWidth;
-use crate::output::file_name::FileStyle;
+use crate::output::file_name::Options as FileStyle;
 use crate::output::icons::painted_icon;
-use crate::style::Colours;
+use crate::theme::Theme;
 
 
 #[derive(PartialEq, Debug, Copy, Clone)]
 pub struct Options {
     pub across: bool,
-    pub console_width: usize,
-    pub icons: bool,
 }
 
 impl Options {
-    pub fn direction(&self) -> tg::Direction {
+    pub fn direction(self) -> tg::Direction {
         if self.across { tg::Direction::LeftToRight }
                   else { tg::Direction::TopToBottom }
     }
@@ -26,9 +24,10 @@ impl Options {
 
 pub struct Render<'a> {
     pub files: Vec<File<'a>>,
-    pub colours: &'a Colours,
-    pub style: &'a FileStyle,
+    pub theme: &'a Theme,
+    pub file_style: &'a FileStyle,
     pub opts: &'a Options,
+    pub console_width: usize,
 }
 
 impl<'a> Render<'a> {
@@ -41,13 +40,13 @@ impl<'a> Render<'a> {
         grid.reserve(self.files.len());
 
         for file in &self.files {
-            let icon = if self.opts.icons { Some(painted_icon(file, self.style)) }
-                                     else { None };
+            let icon = if self.file_style.icons { Some(painted_icon(file, self.theme)) }
+                                           else { None };
 
-            let filename = self.style.for_file(file, self.colours).paint();
+            let filename = self.file_style.for_file(file, self.theme).paint();
 
-            let width = if self.opts.icons { DisplayWidth::from(2) + filename.width() }
-                                      else { filename.width() };
+            let width = if self.file_style.icons { DisplayWidth::from(2) + filename.width() }
+                                            else { filename.width() };
 
             grid.add(tg::Cell {
                 contents:  format!("{}{}", &icon.unwrap_or_default(), filename.strings()),
@@ -55,7 +54,7 @@ impl<'a> Render<'a> {
             });
         }
 
-        if let Some(display) = grid.fit_into_width(self.opts.console_width) {
+        if let Some(display) = grid.fit_into_width(self.console_width) {
             write!(w, "{}", display)
         }
         else {
@@ -63,11 +62,11 @@ impl<'a> Render<'a> {
             // This isn’t *quite* the same as the lines view, which also
             // displays full link paths.
             for file in &self.files {
-                if self.opts.icons {
-                    write!(w, "{}", painted_icon(file, self.style))?;
+                if self.file_style.icons {
+                    write!(w, "{}", painted_icon(file, self.theme))?;
                 }
 
-                let name_cell = self.style.for_file(file, self.colours).paint();
+                let name_cell = self.file_style.for_file(file, self.theme).paint();
                 writeln!(w, "{}", name_cell.strings())?;
             }
 

+ 28 - 17
src/output/grid_details.rs

@@ -11,12 +11,12 @@ use crate::fs::feature::xattr::FileAttributes;
 use crate::fs::filter::FileFilter;
 use crate::output::cell::TextCell;
 use crate::output::details::{Options as DetailsOptions, Row as DetailsRow, Render as DetailsRender};
-use crate::output::file_name::FileStyle;
+use crate::output::file_name::Options as FileStyle;
 use crate::output::grid::Options as GridOptions;
 use crate::output::icons::painted_icon;
 use crate::output::table::{Table, Row as TableRow, Options as TableOptions};
 use crate::output::tree::{TreeParams, TreeDepth};
-use crate::style::Colours;
+use crate::theme::Theme;
 
 
 #[derive(PartialEq, Debug)]
@@ -26,6 +26,13 @@ pub struct Options {
     pub row_threshold: RowThreshold,
 }
 
+impl Options {
+    pub fn to_details_options(&self) -> &DetailsOptions {
+        &self.details
+    }
+}
+
+
 /// The grid-details view can be configured to revert to just a details view
 /// (with one column) if it wouldn’t produce enough rows of output.
 ///
@@ -56,10 +63,10 @@ pub struct Render<'a> {
     pub files: Vec<File<'a>>,
 
     /// How to colour various pieces of text.
-    pub colours: &'a Colours,
+    pub theme: &'a Theme,
 
     /// How to format filenames.
-    pub style: &'a FileStyle,
+    pub file_style: &'a FileStyle,
 
     /// The grid part of the grid-details view.
     pub grid: &'a GridOptions,
@@ -80,6 +87,8 @@ pub struct Render<'a> {
     pub git_ignoring: bool,
 
     pub git: Option<&'a GitCache>,
+
+    pub console_width: usize,
 }
 
 impl<'a> Render<'a> {
@@ -90,12 +99,12 @@ impl<'a> Render<'a> {
     /// This includes an empty files vector because the files get added to
     /// the table in *this* file, not in details: we only want to insert every
     /// *n* files into each column’s table, not all of them.
-    pub fn details(&self) -> DetailsRender<'a> {
+    fn details_for_column(&self) -> DetailsRender<'a> {
         DetailsRender {
             dir:           self.dir,
             files:         Vec::new(),
-            colours:       self.colours,
-            style:         self.style,
+            theme:         self.theme,
+            file_style:    self.file_style,
             opts:          self.details,
             recurse:       None,
             filter:        self.filter,
@@ -105,13 +114,15 @@ impl<'a> Render<'a> {
     }
 
     /// Create a Details render for when this grid-details render doesn’t fit
-    /// in the terminal (or something has gone wrong) and we have given up.
+    /// in the terminal (or something has gone wrong) and we have given up, or
+    /// when the user asked for a grid-details view but the terminal width is
+    /// not available, so we downgrade.
     pub fn give_up(self) -> DetailsRender<'a> {
         DetailsRender {
             dir:           self.dir,
             files:         self.files,
-            colours:       self.colours,
-            style:         self.style,
+            theme:         self.theme,
+            file_style:    self.file_style,
             opts:          self.details,
             recurse:       None,
             filter:        self.filter,
@@ -135,7 +146,7 @@ impl<'a> Render<'a> {
     pub fn find_fitting_grid(&mut self) -> Option<(grid::Grid, grid::Width)> {
         let options = self.details.table.as_ref().expect("Details table options not given!");
 
-        let drender = self.details();
+        let drender = self.details_for_column();
 
         let (first_table, _) = self.make_table(options, &drender);
 
@@ -145,15 +156,15 @@ impl<'a> Render<'a> {
 
         let file_names = self.files.iter()
                              .map(|file| {
-                                 if self.details.icons {
+                                 if self.file_style.icons {
                                     let mut icon_cell = TextCell::default();
-                                    icon_cell.push(ANSIGenericString::from(painted_icon(file, self.style)), 2);
-                                    let file_cell = self.style.for_file(file, self.colours).paint().promote();
+                                    icon_cell.push(ANSIGenericString::from(painted_icon(file, self.theme)), 2);
+                                    let file_cell = self.file_style.for_file(file, self.theme).paint().promote();
                                     icon_cell.append(file_cell);
                                     icon_cell
                                  }
                                  else {
-                                     self.style.for_file(file, self.colours).paint().promote()
+                                     self.file_style.for_file(file, self.theme).paint().promote()
                                  }
                              })
                              .collect::<Vec<_>>();
@@ -167,7 +178,7 @@ impl<'a> Render<'a> {
 
             let the_grid_fits = {
                 let d = grid.fit_into_columns(column_count);
-                d.is_complete() && d.width() <= self.grid.console_width
+                d.is_complete() && d.width() <= self.console_width
             };
 
             if the_grid_fits {
@@ -197,7 +208,7 @@ impl<'a> Render<'a> {
             (None,    _)        => {/* Keep Git how it is */},
         }
 
-        let mut table = Table::new(options, self.git, self.colours);
+        let mut table = Table::new(options, self.git, &self.theme);
         let mut rows = Vec::new();
 
         if self.details.header {

+ 19 - 18
src/output/icons.rs

@@ -2,7 +2,7 @@ use ansi_term::Style;
 
 use crate::fs::File;
 use crate::info::filetype::FileExtensions;
-use crate::output::file_name::FileStyle;
+use crate::theme::Theme;
 
 
 pub trait FileIcon {
@@ -28,26 +28,27 @@ impl Icons {
 }
 
 
-pub fn painted_icon(file: &File<'_>, style: &FileStyle) -> String {
+pub fn painted_icon(file: &File<'_>, theme: &Theme) -> String {
+    use crate::output::file_name::Colours;
+
     let file_icon = icon(file).to_string();
-    let painted = style.exts
-        .colour_file(file)
-        .map_or(file_icon.to_string(), |c| {
-            // Remove underline from icon
-            if c.is_underline {
-                match c.foreground {
-                    Some(color) => {
-                        Style::from(color).paint(file_icon).to_string()
-                    }
-                    None => {
-                        Style::default().paint(file_icon).to_string()
-                    }
+    let c = theme.colour_file(file);
+
+    // Remove underline from icon
+    let painted =
+        if c.is_underline {
+            match c.foreground {
+                Some(color) => {
+                    Style::from(color).paint(file_icon).to_string()
+                }
+                None => {
+                    Style::default().paint(file_icon).to_string()
                 }
             }
-            else {
-                c.paint(file_icon).to_string()
-            }
-        });
+        }
+        else {
+            c.paint(file_icon).to_string()
+        };
 
     format!("{}  ", painted)
 }

+ 13 - 16
src/output/lines.rs

@@ -3,33 +3,27 @@ use std::io::{self, Write};
 use ansi_term::{ANSIStrings, ANSIGenericString};
 
 use crate::fs::File;
-use crate::output::cell::TextCell;
-use crate::output::file_name::{FileName, FileStyle};
+use crate::output::cell::{TextCell, TextCellContents};
+use crate::output::file_name::{Options as FileStyle};
 use crate::output::icons::painted_icon;
-use crate::style::Colours;
+use crate::theme::Theme;
 
 
-#[derive(PartialEq, Debug, Copy, Clone)]
-pub struct Options {
-    pub icons: bool,
-}
-
 /// The lines view literally just displays each file, line-by-line.
 pub struct Render<'a> {
     pub files: Vec<File<'a>>,
-    pub colours: &'a Colours,
-    pub style: &'a FileStyle,
-    pub opts: &'a Options,
+    pub theme: &'a Theme,
+    pub file_style: &'a FileStyle,
 }
 
 impl<'a> Render<'a> {
     pub fn render<W: Write>(&self, w: &mut W) -> io::Result<()> {
         for file in &self.files {
-            let name_cell = self.render_file(file).paint();
-            if self.opts.icons {
+            let name_cell = self.render_file(file);
+            if self.file_style.icons {
                 // Create a TextCell for the icon then append the text to it
                 let mut cell = TextCell::default();
-                let icon = painted_icon(file, self.style);
+                let icon = painted_icon(file, self.theme);
                 cell.push(ANSIGenericString::from(icon), 2);
                 cell.append(name_cell.promote());
                 writeln!(w, "{}", ANSIStrings(&cell))?;
@@ -42,7 +36,10 @@ impl<'a> Render<'a> {
         Ok(())
     }
 
-    fn render_file<'f>(&self, file: &'f File<'a>) -> FileName<'f, 'a, Colours> {
-        self.style.for_file(file, self.colours).with_link_paths()
+    fn render_file<'f>(&self, file: &'f File<'a>) -> TextCellContents {
+        self.file_style
+            .for_file(file, self.theme)
+            .with_link_paths()
+            .paint()
     }
 }

+ 28 - 6
src/output/mod.rs

@@ -1,6 +1,3 @@
-use crate::output::file_name::FileStyle;
-use crate::style::Colours;
-
 pub use self::cell::{TextCell, TextCellContents, DisplayWidth};
 pub use self::escape::escape;
 
@@ -23,8 +20,8 @@ mod tree;
 #[derive(Debug)]
 pub struct View {
     pub mode: Mode,
-    pub colours: Colours,
-    pub style: FileStyle,
+    pub width: TerminalWidth,
+    pub file_style: file_name::Options,
 }
 
 
@@ -35,5 +32,30 @@ pub enum Mode {
     Grid(grid::Options),
     Details(details::Options),
     GridDetails(grid_details::Options),
-    Lines(lines::Options),
+    Lines,
+}
+
+
+/// The width of the terminal requested by the user.
+#[derive(PartialEq, Debug, Copy, Clone)]
+pub enum TerminalWidth {
+
+    /// The user requested this specific number of columns.
+    Set(usize),
+
+    /// Look up the terminal size at runtime.
+    Automatic,
+}
+
+impl TerminalWidth {
+    pub fn actual_terminal_width(self) -> Option<usize> {
+        // All of stdin, stdout, and stderr could not be connected to a
+        // terminal, but we’re only interested in stdout because it’s
+        // where the output goes.
+
+        match self {
+            Self::Set(width)  => Some(width),
+            Self::Automatic   => term_size::dimensions_stdout().map(|t| t.0),
+        }
+    }
 }

+ 18 - 18
src/output/table.rs

@@ -15,7 +15,7 @@ use crate::fs::feature::git::GitCache;
 use crate::output::cell::TextCell;
 use crate::output::render::TimeRender;
 use crate::output::time::TimeFormat;
-use crate::style::Colours;
+use crate::theme::Theme;
 
 
 /// Options for displaying a table.
@@ -306,7 +306,7 @@ lazy_static! {
 
 pub struct Table<'a> {
     columns: Vec<Column>,
-    colours: &'a Colours,
+    theme: &'a Theme,
     env: &'a Environment,
     widths: TableWidths,
     time_format: TimeFormat,
@@ -320,13 +320,13 @@ pub struct Row {
 }
 
 impl<'a, 'f> Table<'a> {
-    pub fn new(options: &'a Options, git: Option<&'a GitCache>, colours: &'a Colours) -> Table<'a> {
+    pub fn new(options: &'a Options, git: Option<&'a GitCache>, theme: &'a Theme) -> Table<'a> {
         let columns = options.columns.collect(git.is_some());
         let widths = TableWidths::zero(columns.len());
         let env = &*ENVIRONMENT;
 
         Table {
-            colours,
+            theme,
             widths,
             columns,
             git,
@@ -342,7 +342,7 @@ impl<'a, 'f> Table<'a> {
 
     pub fn header_row(&self) -> Row {
         let cells = self.columns.iter()
-                        .map(|c| TextCell::paint_str(self.colours.header, c.header()))
+                        .map(|c| TextCell::paint_str(self.theme.ui.header, c.header()))
                         .collect();
 
         Row { cells }
@@ -377,44 +377,44 @@ impl<'a, 'f> Table<'a> {
     fn display(&self, file: &File<'_>, column: Column, xattrs: bool) -> TextCell {
         match column {
             Column::Permissions => {
-                self.permissions_plus(file, xattrs).render(self.colours)
+                self.permissions_plus(file, xattrs).render(self.theme)
             }
             Column::FileSize => {
-                file.size().render(self.colours, self.size_format, &self.env.numeric)
+                file.size().render(self.theme, self.size_format, &self.env.numeric)
             }
             Column::HardLinks => {
-                file.links().render(self.colours, &self.env.numeric)
+                file.links().render(self.theme, &self.env.numeric)
             }
             Column::Inode => {
-                file.inode().render(self.colours.inode)
+                file.inode().render(self.theme.ui.inode)
             }
             Column::Blocks => {
-                file.blocks().render(self.colours)
+                file.blocks().render(self.theme)
             }
             Column::User => {
-                file.user().render(self.colours, &*self.env.lock_users())
+                file.user().render(self.theme, &*self.env.lock_users())
             }
             Column::Group => {
-                file.group().render(self.colours, &*self.env.lock_users())
+                file.group().render(self.theme, &*self.env.lock_users())
             }
             Column::GitStatus => {
-                self.git_status(file).render(self.colours)
+                self.git_status(file).render(self.theme)
             }
             Column::Octal => {
-                self.octal_permissions(file).render(self.colours.octal)
+                self.octal_permissions(file).render(self.theme.ui.octal)
             }
 
             Column::Timestamp(TimeType::Modified)  => {
-                file.modified_time().render(self.colours.date, &self.env.tz, self.time_format)
+                file.modified_time().render(self.theme.ui.date, &self.env.tz, self.time_format)
             }
             Column::Timestamp(TimeType::Changed)   => {
-                file.changed_time().render(self.colours.date, &self.env.tz, self.time_format)
+                file.changed_time().render(self.theme.ui.date, &self.env.tz, self.time_format)
             }
             Column::Timestamp(TimeType::Created)   => {
-                file.created_time().render(self.colours.date, &self.env.tz, self.time_format)
+                file.created_time().render(self.theme.ui.date, &self.env.tz, self.time_format)
             }
             Column::Timestamp(TimeType::Accessed)  => {
-                file.accessed_time().render(self.colours.date, &self.env.tz, self.time_format)
+                file.accessed_time().render(self.theme.ui.date, &self.env.tz, self.time_format)
             }
         }
     }

+ 0 - 471
src/style/colours.rs

@@ -1,471 +0,0 @@
-use ansi_term::Colour::{Red, Green, Yellow, Blue, Cyan, Purple, Fixed};
-use ansi_term::Style;
-
-use crate::output::file_name::Colours as FileNameColours;
-use crate::output::render;
-use crate::style::lsc::Pair;
-
-
-#[derive(Debug, Default, PartialEq)]
-pub struct Colours {
-    pub colourful: bool,
-
-    pub filekinds:  FileKinds,
-    pub perms:      Permissions,
-    pub size:       Size,
-    pub users:      Users,
-    pub links:      Links,
-    pub git:        Git,
-
-    pub punctuation:  Style,
-    pub date:         Style,
-    pub inode:        Style,
-    pub blocks:       Style,
-    pub header:       Style,
-    pub octal:        Style,
-
-    pub symlink_path:         Style,
-    pub control_char:         Style,
-    pub broken_symlink:       Style,
-    pub broken_path_overlay:  Style,
-}
-
-#[derive(Clone, Copy, Debug, Default, PartialEq)]
-pub struct FileKinds {
-    pub normal: Style,
-    pub directory: Style,
-    pub symlink: Style,
-    pub pipe: Style,
-    pub block_device: Style,
-    pub char_device: Style,
-    pub socket: Style,
-    pub special: Style,
-    pub executable: Style,
-}
-
-#[derive(Clone, Copy, Debug, Default, PartialEq)]
-pub struct Permissions {
-    pub user_read:          Style,
-    pub user_write:         Style,
-    pub user_execute_file:  Style,
-    pub user_execute_other: Style,
-
-    pub group_read:    Style,
-    pub group_write:   Style,
-    pub group_execute: Style,
-
-    pub other_read:    Style,
-    pub other_write:   Style,
-    pub other_execute: Style,
-
-    pub special_user_file: Style,
-    pub special_other:     Style,
-
-    pub attribute: Style,
-}
-
-#[derive(Clone, Copy, Debug, Default, PartialEq)]
-pub struct Size {
-    pub major: Style,
-    pub minor: Style,
-
-    pub number_byte: Style,
-    pub number_kilo: Style,
-    pub number_mega: Style,
-    pub number_giga: Style,
-    pub number_huge: Style,
-
-    pub unit_byte: Style,
-    pub unit_kilo: Style,
-    pub unit_mega: Style,
-    pub unit_giga: Style,
-    pub unit_huge: Style,
-}
-
-#[derive(Clone, Copy, Debug, Default, PartialEq)]
-pub struct Users {
-    pub user_you: Style,
-    pub user_someone_else: Style,
-    pub group_yours: Style,
-    pub group_not_yours: Style,
-}
-
-#[derive(Clone, Copy, Debug, Default, PartialEq)]
-pub struct Links {
-    pub normal: Style,
-    pub multi_link_file: Style,
-}
-
-#[derive(Clone, Copy, Debug, Default, PartialEq)]
-pub struct Git {
-    pub new: Style,
-    pub modified: Style,
-    pub deleted: Style,
-    pub renamed: Style,
-    pub typechange: Style,
-    pub ignored: Style,
-    pub conflicted: Style,
-}
-
-impl Colours {
-    pub fn plain() -> Self {
-        Self::default()
-    }
-
-    pub fn colourful(scale: bool) -> Self {
-        Self {
-            colourful: true,
-
-            filekinds: FileKinds {
-                normal:       Style::default(),
-                directory:    Blue.bold(),
-                symlink:      Cyan.normal(),
-                pipe:         Yellow.normal(),
-                block_device: Yellow.bold(),
-                char_device:  Yellow.bold(),
-                socket:       Red.bold(),
-                special:      Yellow.normal(),
-                executable:   Green.bold(),
-            },
-
-            perms: Permissions {
-                user_read:           Yellow.bold(),
-                user_write:          Red.bold(),
-                user_execute_file:   Green.bold().underline(),
-                user_execute_other:  Green.bold(),
-
-                group_read:          Yellow.normal(),
-                group_write:         Red.normal(),
-                group_execute:       Green.normal(),
-
-                other_read:          Yellow.normal(),
-                other_write:         Red.normal(),
-                other_execute:       Green.normal(),
-
-                special_user_file:   Purple.normal(),
-                special_other:       Purple.normal(),
-
-                attribute:           Style::default(),
-            },
-
-            size: Size::colourful(scale),
-
-            users: Users {
-                user_you:           Yellow.bold(),
-                user_someone_else:  Style::default(),
-                group_yours:        Yellow.bold(),
-                group_not_yours:    Style::default(),
-            },
-
-            links: Links {
-                normal:          Red.bold(),
-                multi_link_file: Red.on(Yellow),
-            },
-
-            git: Git {
-                new:         Green.normal(),
-                modified:    Blue.normal(),
-                deleted:     Red.normal(),
-                renamed:     Yellow.normal(),
-                typechange:  Purple.normal(),
-                ignored:     Style::default().dimmed(),
-                conflicted:  Red.normal(),
-            },
-
-            punctuation:  Fixed(244).normal(),
-            date:         Blue.normal(),
-            inode:        Purple.normal(),
-            blocks:       Cyan.normal(),
-            octal:        Purple.normal(),
-            header:       Style::default().underline(),
-
-            symlink_path:         Cyan.normal(),
-            control_char:         Red.normal(),
-            broken_symlink:       Red.normal(),
-            broken_path_overlay:  Style::default().underline(),
-        }
-    }
-}
-
-impl Size {
-    pub fn colourful(scale: bool) -> Self {
-        if scale { Self::colourful_scale() }
-            else { Self::colourful_plain() }
-    }
-
-    fn colourful_plain() -> Self {
-        Self {
-            major:  Green.bold(),
-            minor:  Green.normal(),
-
-            number_byte: Green.bold(),
-            number_kilo: Green.bold(),
-            number_mega: Green.bold(),
-            number_giga: Green.bold(),
-            number_huge: Green.bold(),
-
-            unit_byte: Green.normal(),
-            unit_kilo: Green.normal(),
-            unit_mega: Green.normal(),
-            unit_giga: Green.normal(),
-            unit_huge: Green.normal(),
-        }
-    }
-
-    fn colourful_scale() -> Self {
-        Self {
-            major:  Green.bold(),
-            minor:  Green.normal(),
-
-            number_byte: Fixed(118).normal(),
-            number_kilo: Fixed(190).normal(),
-            number_mega: Fixed(226).normal(),
-            number_giga: Fixed(220).normal(),
-            number_huge: Fixed(214).normal(),
-
-            unit_byte: Green.normal(),
-            unit_kilo: Green.normal(),
-            unit_mega: Green.normal(),
-            unit_giga: Green.normal(),
-            unit_huge: Green.normal(),
-        }
-    }
-}
-
-
-/// Some of the styles are **overlays**: although they have the same attribute
-/// set as regular styles (foreground and background colours, bold, underline,
-/// etc), they’re intended to be used to *amend* existing styles.
-///
-/// For example, the target path of a broken symlink is displayed in a red,
-/// underlined style by default. Paths can contain control characters, so
-/// these control characters need to be underlined too, otherwise it looks
-/// weird. So instead of having four separate configurable styles for “link
-/// path”, “broken link path”, “control character” and “broken control
-/// character”, there are styles for “link path”, “control character”, and
-/// “broken link overlay”, the latter of which is just set to override the
-/// underline attribute on the other two.
-fn apply_overlay(mut base: Style, overlay: Style) -> Style {
-    if let Some(fg) = overlay.foreground { base.foreground = Some(fg); }
-    if let Some(bg) = overlay.background { base.background = Some(bg); }
-
-    if overlay.is_bold          { base.is_bold          = true; }
-    if overlay.is_dimmed        { base.is_dimmed        = true; }
-    if overlay.is_italic        { base.is_italic        = true; }
-    if overlay.is_underline     { base.is_underline     = true; }
-    if overlay.is_blink         { base.is_blink         = true; }
-    if overlay.is_reverse       { base.is_reverse       = true; }
-    if overlay.is_hidden        { base.is_hidden        = true; }
-    if overlay.is_strikethrough { base.is_strikethrough = true; }
-
-    base
-}
-// TODO: move this function to the ansi_term crate
-
-
-impl Colours {
-
-    /// Sets a value on this set of colours using one of the keys understood
-    /// by the `LS_COLORS` environment variable. Invalid keys set nothing, but
-    /// return false.
-    pub fn set_ls(&mut self, pair: &Pair<'_>) -> bool {
-        match pair.key {
-            "di" => self.filekinds.directory    = pair.to_style(),  // DIR
-            "ex" => self.filekinds.executable   = pair.to_style(),  // EXEC
-            "fi" => self.filekinds.normal       = pair.to_style(),  // FILE
-            "pi" => self.filekinds.pipe         = pair.to_style(),  // FIFO
-            "so" => self.filekinds.socket       = pair.to_style(),  // SOCK
-            "bd" => self.filekinds.block_device = pair.to_style(),  // BLK
-            "cd" => self.filekinds.char_device  = pair.to_style(),  // CHR
-            "ln" => self.filekinds.symlink      = pair.to_style(),  // LINK
-            "or" => self.broken_symlink         = pair.to_style(),  // ORPHAN
-             _   => return false,
-             // Codes we don’t do anything with:
-             // MULTIHARDLINK, DOOR, SETUID, SETGID, CAPABILITY,
-             // STICKY_OTHER_WRITABLE, OTHER_WRITABLE, STICKY, MISSING
-        }
-        true
-    }
-
-    /// Sets a value on this set of colours using one of the keys understood
-    /// by the `EXA_COLORS` environment variable. Invalid keys set nothing,
-    /// but return false. This doesn’t take the `LS_COLORS` keys into account,
-    /// so `set_ls` should have been run first.
-    pub fn set_exa(&mut self, pair: &Pair<'_>) -> bool {
-        match pair.key {
-            "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.set_number_style(pair.to_style()),
-            "sb" => self.set_unit_style(pair.to_style()),
-            "nb" => self.size.number_byte         = pair.to_style(),
-            "nk" => self.size.number_kilo         = pair.to_style(),
-            "nm" => self.size.number_mega         = pair.to_style(),
-            "ng" => self.size.number_giga         = pair.to_style(),
-            "nh" => self.size.number_huge         = pair.to_style(),
-            "ub" => self.size.unit_byte           = pair.to_style(),
-            "uk" => self.size.unit_kilo           = pair.to_style(),
-            "um" => self.size.unit_mega           = pair.to_style(),
-            "ug" => self.size.unit_giga           = pair.to_style(),
-            "uh" => self.size.unit_huge           = 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(),
-            "bO" => self.broken_path_overlay      = pair.to_style(),
-
-             _   => return false,
-        }
-
-        true
-    }
-
-    pub fn set_number_style(&mut self, style: Style) {
-        self.size.number_byte = style;
-        self.size.number_kilo = style;
-        self.size.number_mega = style;
-        self.size.number_giga = style;
-        self.size.number_huge = style;
-    }
-
-    pub fn set_unit_style(&mut self, style: Style) {
-        self.size.unit_byte = style;
-        self.size.unit_kilo = style;
-        self.size.unit_mega = style;
-        self.size.unit_giga = style;
-        self.size.unit_huge = style;
-    }
-}
-
-
-impl render::BlocksColours for Colours {
-    fn block_count(&self)  -> Style { self.blocks }
-    fn no_blocks(&self)    -> Style { self.punctuation }
-}
-
-impl render::FiletypeColours for Colours {
-    fn normal(&self)       -> Style { self.filekinds.normal }
-    fn directory(&self)    -> Style { self.filekinds.directory }
-    fn pipe(&self)         -> Style { self.filekinds.pipe }
-    fn symlink(&self)      -> Style { self.filekinds.symlink }
-    fn block_device(&self) -> Style { self.filekinds.block_device }
-    fn char_device(&self)  -> Style { self.filekinds.char_device }
-    fn socket(&self)       -> Style { self.filekinds.socket }
-    fn special(&self)      -> Style { self.filekinds.special }
-}
-
-impl render::GitColours for Colours {
-    fn not_modified(&self)  -> Style { self.punctuation }
-    #[allow(clippy::new_ret_no_self)]
-    fn new(&self)           -> Style { self.git.new }
-    fn modified(&self)      -> Style { self.git.modified }
-    fn deleted(&self)       -> Style { self.git.deleted }
-    fn renamed(&self)       -> Style { self.git.renamed }
-    fn type_change(&self)   -> Style { self.git.typechange }
-    fn ignored(&self)       -> Style { self.git.ignored }
-    fn conflicted(&self)    -> Style { self.git.conflicted }
-}
-
-impl render::GroupColours for Colours {
-    fn yours(&self)      -> Style { self.users.group_yours }
-    fn not_yours(&self)  -> Style { self.users.group_not_yours }
-}
-
-impl render::LinksColours for Colours {
-    fn normal(&self)           -> Style { self.links.normal }
-    fn multi_link_file(&self)  -> Style { self.links.multi_link_file }
-}
-
-impl render::PermissionsColours for Colours {
-    fn dash(&self)               -> Style { self.punctuation }
-    fn user_read(&self)          -> Style { self.perms.user_read }
-    fn user_write(&self)         -> Style { self.perms.user_write }
-    fn user_execute_file(&self)  -> Style { self.perms.user_execute_file }
-    fn user_execute_other(&self) -> Style { self.perms.user_execute_other }
-    fn group_read(&self)         -> Style { self.perms.group_read }
-    fn group_write(&self)        -> Style { self.perms.group_write }
-    fn group_execute(&self)      -> Style { self.perms.group_execute }
-    fn other_read(&self)         -> Style { self.perms.other_read }
-    fn other_write(&self)        -> Style { self.perms.other_write }
-    fn other_execute(&self)      -> Style { self.perms.other_execute }
-    fn special_user_file(&self)  -> Style { self.perms.special_user_file }
-    fn special_other(&self)      -> Style { self.perms.special_other }
-    fn attribute(&self)          -> Style { self.perms.attribute }
-}
-
-impl render::SizeColours for Colours {
-    fn size(&self, prefix: Option<number_prefix::Prefix>) -> Style {
-        use number_prefix::Prefix::*;
-
-        match prefix {
-            None                    => self.size.number_byte,
-            Some(Kilo) | Some(Kibi) => self.size.number_kilo,
-            Some(Mega) | Some(Mebi) => self.size.number_mega,
-            Some(Giga) | Some(Gibi) => self.size.number_giga,
-            Some(_)                 => self.size.number_huge,
-        }
-    }
-
-    fn unit(&self, prefix: Option<number_prefix::Prefix>) -> Style {
-        use number_prefix::Prefix::*;
-
-        match prefix {
-            None                    => self.size.unit_byte,
-            Some(Kilo) | Some(Kibi) => self.size.unit_kilo,
-            Some(Mega) | Some(Mebi) => self.size.unit_mega,
-            Some(Giga) | Some(Gibi) => self.size.unit_giga,
-            Some(_)                 => self.size.unit_huge,
-        }
-    }
-
-    fn no_size(&self) -> Style { self.punctuation }
-    fn major(&self)   -> Style { self.size.major }
-    fn comma(&self)   -> Style { self.punctuation }
-    fn minor(&self)   -> Style { self.size.minor }
-}
-
-impl render::UserColours for Colours {
-    fn you(&self)           -> Style { self.users.user_you }
-    fn someone_else(&self)  -> Style { self.users.user_someone_else }
-}
-
-impl FileNameColours for Colours {
-    fn normal_arrow(&self)        -> Style { self.punctuation }
-    fn broken_symlink(&self)      -> Style { self.broken_symlink }
-    fn broken_filename(&self)     -> Style { apply_overlay(self.broken_symlink, self.broken_path_overlay) }
-    fn broken_control_char(&self) -> Style { apply_overlay(self.control_char,   self.broken_path_overlay) }
-    fn control_char(&self)        -> Style { self.control_char }
-    fn symlink_path(&self)        -> Style { self.symlink_path }
-    fn executable_file(&self)     -> Style { self.filekinds.executable }
-}

+ 0 - 6
src/style/mod.rs

@@ -1,6 +0,0 @@
-mod colours;
-pub use self::colours::Colours;
-pub use self::colours::Size as SizeColours;
-
-mod lsc;
-pub use self::lsc::LSColors;

+ 130 - 0
src/theme/default_theme.rs

@@ -0,0 +1,130 @@
+use ansi_term::Style;
+use ansi_term::Colour::*;
+
+use crate::theme::ColourScale;
+use crate::theme::ui_styles::*;
+
+
+impl UiStyles {
+    pub fn default_theme(scale: ColourScale) -> Self {
+        Self {
+            colourful: true,
+
+            filekinds: FileKinds {
+                normal:       Style::default(),
+                directory:    Blue.bold(),
+                symlink:      Cyan.normal(),
+                pipe:         Yellow.normal(),
+                block_device: Yellow.bold(),
+                char_device:  Yellow.bold(),
+                socket:       Red.bold(),
+                special:      Yellow.normal(),
+                executable:   Green.bold(),
+            },
+
+            perms: Permissions {
+                user_read:           Yellow.bold(),
+                user_write:          Red.bold(),
+                user_execute_file:   Green.bold().underline(),
+                user_execute_other:  Green.bold(),
+
+                group_read:          Yellow.normal(),
+                group_write:         Red.normal(),
+                group_execute:       Green.normal(),
+
+                other_read:          Yellow.normal(),
+                other_write:         Red.normal(),
+                other_execute:       Green.normal(),
+
+                special_user_file:   Purple.normal(),
+                special_other:       Purple.normal(),
+
+                attribute:           Style::default(),
+            },
+
+            size: Size::colourful(scale),
+
+            users: Users {
+                user_you:           Yellow.bold(),
+                user_someone_else:  Style::default(),
+                group_yours:        Yellow.bold(),
+                group_not_yours:    Style::default(),
+            },
+
+            links: Links {
+                normal:          Red.bold(),
+                multi_link_file: Red.on(Yellow),
+            },
+
+            git: Git {
+                new:         Green.normal(),
+                modified:    Blue.normal(),
+                deleted:     Red.normal(),
+                renamed:     Yellow.normal(),
+                typechange:  Purple.normal(),
+                ignored:     Style::default().dimmed(),
+                conflicted:  Red.normal(),
+            },
+
+            punctuation:  Fixed(244).normal(),
+            date:         Blue.normal(),
+            inode:        Purple.normal(),
+            blocks:       Cyan.normal(),
+            octal:        Purple.normal(),
+            header:       Style::default().underline(),
+
+            symlink_path:         Cyan.normal(),
+            control_char:         Red.normal(),
+            broken_symlink:       Red.normal(),
+            broken_path_overlay:  Style::default().underline(),
+        }
+    }
+}
+
+
+impl Size {
+    pub fn colourful(scale: ColourScale) -> Self {
+        match scale {
+            ColourScale::Gradient  => Self::colourful_gradient(),
+            ColourScale::Fixed     => Self::colourful_fixed(),
+        }
+    }
+
+    fn colourful_fixed() -> Self {
+        Self {
+            major:  Green.bold(),
+            minor:  Green.normal(),
+
+            number_byte: Green.bold(),
+            number_kilo: Green.bold(),
+            number_mega: Green.bold(),
+            number_giga: Green.bold(),
+            number_huge: Green.bold(),
+
+            unit_byte: Green.normal(),
+            unit_kilo: Green.normal(),
+            unit_mega: Green.normal(),
+            unit_giga: Green.normal(),
+            unit_huge: Green.normal(),
+        }
+    }
+
+    fn colourful_gradient() -> Self {
+        Self {
+            major:  Green.bold(),
+            minor:  Green.normal(),
+
+            number_byte: Fixed(118).normal(),
+            number_kilo: Fixed(190).normal(),
+            number_mega: Fixed(226).normal(),
+            number_giga: Fixed(220).normal(),
+            number_huge: Fixed(214).normal(),
+
+            unit_byte: Green.normal(),
+            unit_kilo: Green.normal(),
+            unit_mega: Green.normal(),
+            unit_giga: Green.normal(),
+            unit_huge: Green.normal(),
+        }
+    }
+}

+ 3 - 2
src/style/lsc.rs → src/theme/lsc.rs

@@ -70,8 +70,9 @@ where I: Iterator<Item = &'a str>
                 }*/
 
                 if let (Some(r), Some(g), Some(b)) = (hexes.parse().ok(),
-                                                           iter.next().and_then(|s| s.parse().ok()),
-                                                           iter.next().and_then(|s| s.parse().ok())) {
+                                                      iter.next().and_then(|s| s.parse().ok()),
+                                                      iter.next().and_then(|s| s.parse().ok()))
+                {
                     return Some(RGB(r, g, b));
                 }
             }

+ 249 - 272
src/options/style.rs → src/theme/mod.rs

@@ -1,11 +1,28 @@
 use ansi_term::Style;
 
 use crate::fs::File;
-use crate::options::{flags, Vars, OptionsError};
-use crate::options::parser::MatchedFlags;
-use crate::output::file_name::{Classify, FileStyle};
-use crate::style::Colours;
+use crate::output::file_name::Colours as FileNameColours;
+use crate::output::render;
 
+mod ui_styles;
+pub use self::ui_styles::UiStyles;
+pub use self::ui_styles::Size as SizeColours;
+
+mod lsc;
+pub use self::lsc::LSColors;
+
+mod default_theme;
+
+
+#[derive(PartialEq, Debug)]
+pub struct Options {
+
+    pub use_colours: UseColours,
+
+    pub colour_scale: ColourScale,
+
+    pub definitions: Definitions,
+}
 
 /// Under what circumstances we should display coloured, rather than plain,
 /// output to the terminal.
@@ -14,8 +31,8 @@ use crate::style::Colours;
 /// 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 {
+#[derive(PartialEq, Debug, Copy, Clone)]
+pub enum UseColours {
 
     /// Display them even when output isn’t going to a terminal.
     Always,
@@ -27,81 +44,39 @@ enum TerminalColours {
     Never,
 }
 
-impl Default for TerminalColours {
-    fn default() -> Self {
-        Self::Automatic
-    }
+#[derive(PartialEq, Debug, Copy, Clone)]
+pub enum ColourScale {
+    Fixed,
+    Gradient,
 }
 
-
-impl TerminalColours {
-
-    /// Determine which terminal colour conditions to use.
-    fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
-        let word = match matches.get_where(|f| f.matches(&flags::COLOR) || f.matches(&flags::COLOUR))? {
-            Some(w)  => w,
-            None     => return Ok(Self::default()),
-        };
-
-        if word == "always" {
-            Ok(Self::Always)
-        }
-        else if word == "auto" || word == "automatic" {
-            Ok(Self::Automatic)
-        }
-        else if word == "never" {
-            Ok(Self::Never)
-        }
-        else {
-            Err(OptionsError::BadArgument(&flags::COLOR, word.into()))
-        }
-    }
+#[derive(PartialEq, Debug, Default)]
+pub struct Definitions {
+    pub ls: Option<String>,
+    pub exa: Option<String>,
 }
 
 
-/// **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,
+pub struct Theme {
+    pub ui: UiStyles,
+    pub exts: Box<dyn FileColours>,
 }
 
-impl Styles {
+impl Options {
 
     #[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, OptionsError>
-    where TW: Fn() -> Option<usize>, V: Vars {
+    pub fn to_theme(&self, isatty: bool) -> Theme {
         use crate::info::filetype::FileExtensions;
-        use crate::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 == TerminalColours::Never || (tc == TerminalColours::Automatic && widther().is_none()) {
+        if self.use_colours == UseColours::Never || (self.use_colours == UseColours::Automatic && ! isatty) {
+            let ui = UiStyles::plain();
             let exts = Box::new(NoFileColours);
-
-            return Ok(Self {
-                colours: Colours::plain(),
-                style: FileStyle { classify, exts },
-            });
+            return Theme { ui, exts };
         }
 
         // 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);
+        let mut ui = UiStyles::default_theme(self.colour_scale);
+        let (exts, use_default_filetypes) = self.definitions.parse_color_vars(&mut ui);
 
         // Use between 0 and 2 file name highlighters
         let exts = match (exts.is_non_empty(), use_default_filetypes) {
@@ -111,67 +86,89 @@ impl Styles {
             ( true,  true)  => Box::new((exts, FileExtensions))  as Box<_>,
         };
 
-        let style = FileStyle { classify, exts };
-        Ok(Self { colours, style })
+        Theme { ui, exts }
     }
 }
 
-/// 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 log::*;
-
-    use crate::options::vars;
-    use crate::style::LSColors;
+impl Definitions {
+
+    /// 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(&self, colours: &mut UiStyles) -> (ExtensionMappings, bool) {
+        use log::*;
+
+        let mut exts = ExtensionMappings::default();
+
+        if let Some(lsc) = &self.ls {
+            LSColors(lsc).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 exts = ExtensionMappings::default();
+        let mut use_default_filetypes = true;
 
-    if let Some(lsc) = vars.get(vars::LS_COLORS) {
-        let lsc = lsc.to_string_lossy();
+        if let Some(exa) = &self.exa {
+            // Is this hacky? Yes.
+            if exa == "reset" || exa.starts_with("reset:") {
+                use_default_filetypes = false;
+            }
 
-        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);
+            LSColors(exa).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);
+                        }
                     }
-                }
-            }
-        });
-    }
+                };
+            });
+        }
 
-    let mut use_default_filetypes = true;
+        (exts, use_default_filetypes)
+    }
+}
 
-    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;
-        }
+pub trait FileColours: std::marker::Sync {
+    fn colour_file(&self, file: &File<'_>) -> Option<Style>;
+}
 
-        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);
-                    }
-                }
-            };
-        });
+#[derive(PartialEq, Debug)]
+struct NoFileColours;
+impl FileColours for NoFileColours {
+    fn colour_file(&self, _file: &File<'_>) -> Option<Style> {
+        None
     }
+}
 
-    (exts, use_default_filetypes)
+// 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))
+    }
 }
 
 
@@ -183,7 +180,6 @@ struct ExtensionMappings {
 // Loop through backwards so that colours specified later in the list override
 // colours specified earlier, like we do with options and strict mode
 
-use crate::output::file_name::FileColours;
 impl FileColours for ExtensionMappings {
     fn colour_file(&self, file: &File<'_>) -> Option<Style> {
         self.mappings.iter().rev()
@@ -203,166 +199,164 @@ impl ExtensionMappings {
 }
 
 
-impl Classify {
-    fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
-        let flagged = matches.has(&flags::CLASSIFY)?;
-
-        if flagged { Ok(Self::AddFileIndicators) }
-              else { Ok(Self::JustFilenames) }
-    }
-}
-
-
-#[cfg(test)]
-mod terminal_test {
-    use super::*;
-    use std::ffi::OsString;
-    use crate::options::flags;
-    use crate::options::parser::{Flag, Arg};
-
-    use crate::options::test::parse_for_test;
-    use crate::options::test::Strictnesses::*;
-
-    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()));
+impl render::BlocksColours for Theme {
+    fn block_count(&self)  -> Style { self.ui.blocks }
+    fn no_blocks(&self)    -> Style { self.ui.punctuation }
+}
 
-    // --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));
+impl render::FiletypeColours for Theme {
+    fn normal(&self)       -> Style { self.ui.filekinds.normal }
+    fn directory(&self)    -> Style { self.ui.filekinds.directory }
+    fn pipe(&self)         -> Style { self.ui.filekinds.pipe }
+    fn symlink(&self)      -> Style { self.ui.filekinds.symlink }
+    fn block_device(&self) -> Style { self.ui.filekinds.block_device }
+    fn char_device(&self)  -> Style { self.ui.filekinds.char_device }
+    fn socket(&self)       -> Style { self.ui.filekinds.socket }
+    fn special(&self)      -> Style { self.ui.filekinds.special }
+}
 
-    // --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));
+impl render::GitColours for Theme {
+    fn not_modified(&self)  -> Style { self.ui.punctuation }
+    #[allow(clippy::new_ret_no_self)]
+    fn new(&self)           -> Style { self.ui.git.new }
+    fn modified(&self)      -> Style { self.ui.git.modified }
+    fn deleted(&self)       -> Style { self.ui.git.deleted }
+    fn renamed(&self)       -> Style { self.ui.git.renamed }
+    fn type_change(&self)   -> Style { self.ui.git.typechange }
+    fn ignored(&self)       -> Style { self.ui.git.ignored }
+    fn conflicted(&self)    -> Style { self.ui.git.conflicted }
+}
 
-    // Errors
-    test!(no_u_error:    ["--color=upstream"];   Both => err OptionsError::BadArgument(&flags::COLOR, OsString::from("upstream")));  // the error is for --color
-    test!(u_error:       ["--colour=lovers"];    Both => err OptionsError::BadArgument(&flags::COLOR, OsString::from("lovers")));    // and so is this one!
+impl render::GroupColours for Theme {
+    fn yours(&self)      -> Style { self.ui.users.group_yours }
+    fn not_yours(&self)  -> Style { self.ui.users.group_not_yours }
+}
 
-    // 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));
+impl render::LinksColours for Theme {
+    fn normal(&self)           -> Style { self.ui.links.normal }
+    fn multi_link_file(&self)  -> Style { self.ui.links.multi_link_file }
+}
 
-    test!(overridden_5:  ["--colour=auto", "--colour=never"];  Complain => err OptionsError::Duplicate(Flag::Long("colour"), Flag::Long("colour")));
-    test!(overridden_6:  ["--color=auto",  "--colour=never"];  Complain => err OptionsError::Duplicate(Flag::Long("color"),  Flag::Long("colour")));
-    test!(overridden_7:  ["--colour=auto", "--color=never"];   Complain => err OptionsError::Duplicate(Flag::Long("colour"), Flag::Long("color")));
-    test!(overridden_8:  ["--color=auto",  "--color=never"];   Complain => err OptionsError::Duplicate(Flag::Long("color"),  Flag::Long("color")));
+impl render::PermissionsColours for Theme {
+    fn dash(&self)               -> Style { self.ui.punctuation }
+    fn user_read(&self)          -> Style { self.ui.perms.user_read }
+    fn user_write(&self)         -> Style { self.ui.perms.user_write }
+    fn user_execute_file(&self)  -> Style { self.ui.perms.user_execute_file }
+    fn user_execute_other(&self) -> Style { self.ui.perms.user_execute_other }
+    fn group_read(&self)         -> Style { self.ui.perms.group_read }
+    fn group_write(&self)        -> Style { self.ui.perms.group_write }
+    fn group_execute(&self)      -> Style { self.ui.perms.group_execute }
+    fn other_read(&self)         -> Style { self.ui.perms.other_read }
+    fn other_write(&self)        -> Style { self.ui.perms.other_write }
+    fn other_execute(&self)      -> Style { self.ui.perms.other_execute }
+    fn special_user_file(&self)  -> Style { self.ui.perms.special_user_file }
+    fn special_other(&self)      -> Style { self.ui.perms.special_other }
+    fn attribute(&self)          -> Style { self.ui.perms.attribute }
 }
 
+impl render::SizeColours for Theme {
+    fn size(&self, prefix: Option<number_prefix::Prefix>) -> Style {
+        use number_prefix::Prefix::*;
 
-#[cfg(test)]
-mod colour_test {
-    use super::*;
-    use crate::options::flags;
-    use crate::options::parser::{Flag, Arg};
+        match prefix {
+            None                    => self.ui.size.number_byte,
+            Some(Kilo) | Some(Kibi) => self.ui.size.number_kilo,
+            Some(Mega) | Some(Mebi) => self.ui.size.number_mega,
+            Some(Giga) | Some(Gibi) => self.ui.size.number_giga,
+            Some(_)                 => self.ui.size.number_huge,
+        }
+    }
 
-    use crate::options::test::parse_for_test;
-    use crate::options::test::Strictnesses::*;
+    fn unit(&self, prefix: Option<number_prefix::Prefix>) -> Style {
+        use number_prefix::Prefix::*;
 
-    static TEST_ARGS: &[&Arg] = &[ &flags::COLOR,       &flags::COLOUR,
-                                   &flags::COLOR_SCALE, &flags::COLOUR_SCALE ];
+        match prefix {
+            None                    => self.ui.size.unit_byte,
+            Some(Kilo) | Some(Kibi) => self.ui.size.unit_kilo,
+            Some(Mega) | Some(Mebi) => self.ui.size.unit_mega,
+            Some(Giga) | Some(Gibi) => self.ui.size.unit_giga,
+            Some(_)                 => self.ui.size.unit_huge,
+        }
+    }
 
-    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);
-                }
-            }
-        };
+    fn no_size(&self) -> Style { self.ui.punctuation }
+    fn major(&self)   -> Style { self.ui.size.major }
+    fn comma(&self)   -> Style { self.ui.punctuation }
+    fn minor(&self)   -> Style { self.ui.size.minor }
+}
 
-        ($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);
-                }
-            }
-        };
+impl render::UserColours for Theme {
+    fn you(&self)           -> Style { self.ui.users.user_you }
+    fn someone_else(&self)  -> Style { self.ui.users.user_someone_else }
+}
 
-        ($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),
-                    }
-                }
-            }
-        };
+impl FileNameColours for Theme {
+    fn normal_arrow(&self)        -> Style { self.ui.punctuation }
+    fn broken_symlink(&self)      -> Style { self.ui.broken_symlink }
+    fn broken_filename(&self)     -> Style { apply_overlay(self.ui.broken_symlink, self.ui.broken_path_overlay) }
+    fn broken_control_char(&self) -> Style { apply_overlay(self.ui.control_char,   self.ui.broken_path_overlay) }
+    fn control_char(&self)        -> Style { self.ui.control_char }
+    fn symlink_path(&self)        -> Style { self.ui.symlink_path }
+    fn executable_file(&self)     -> Style { self.ui.filekinds.executable }
+
+    fn colour_file(&self, file: &File<'_>) -> Style {
+        self.exts.colour_file(file).unwrap_or(self.ui.filekinds.normal)
     }
+}
 
-    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 => Ok(Colours::colourful(true)));
-    test!(scale_2:  ["--color=always", "--color-scale",                 ], || None;   Last => Ok(Colours::colourful(true)));
-    test!(scale_3:  ["--color=always",                  "--colour-scale"], || None;   Last => Ok(Colours::colourful(true)));
-    test!(scale_4:  ["--color=always",                                  ], || None;   Last => Ok(Colours::colourful(false)));
-
-    test!(scale_5:  ["--color=always", "--color-scale", "--colour-scale"], || None;   Complain => err OptionsError::Duplicate(Flag::Long("color-scale"),  Flag::Long("colour-scale")));
-    test!(scale_6:  ["--color=always", "--color-scale",                 ], || None;   Complain => Ok(Colours::colourful(true)));
-    test!(scale_7:  ["--color=always",                  "--colour-scale"], || None;   Complain => Ok(Colours::colourful(true)));
-    test!(scale_8:  ["--color=always",                                  ], || None;   Complain => Ok(Colours::colourful(false)));
+
+/// Some of the styles are **overlays**: although they have the same attribute
+/// set as regular styles (foreground and background colours, bold, underline,
+/// etc), they’re intended to be used to *amend* existing styles.
+///
+/// For example, the target path of a broken symlink is displayed in a red,
+/// underlined style by default. Paths can contain control characters, so
+/// these control characters need to be underlined too, otherwise it looks
+/// weird. So instead of having four separate configurable styles for “link
+/// path”, “broken link path”, “control character” and “broken control
+/// character”, there are styles for “link path”, “control character”, and
+/// “broken link overlay”, the latter of which is just set to override the
+/// underline attribute on the other two.
+fn apply_overlay(mut base: Style, overlay: Style) -> Style {
+    if let Some(fg) = overlay.foreground { base.foreground = Some(fg); }
+    if let Some(bg) = overlay.background { base.background = Some(bg); }
+
+    if overlay.is_bold          { base.is_bold          = true; }
+    if overlay.is_dimmed        { base.is_dimmed        = true; }
+    if overlay.is_italic        { base.is_italic        = true; }
+    if overlay.is_underline     { base.is_underline     = true; }
+    if overlay.is_blink         { base.is_blink         = true; }
+    if overlay.is_reverse       { base.is_reverse       = true; }
+    if overlay.is_hidden        { base.is_hidden        = true; }
+    if overlay.is_strikethrough { base.is_strikethrough = true; }
+
+    base
 }
+// TODO: move this function to the ansi_term crate
 
 
 #[cfg(test)]
 mod customs_test {
-    use std::ffi::OsString;
-
     use super::*;
-    use crate::options::Vars;
-
+    use crate::theme::ui_styles::UiStyles;
     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);
+                let mut $expected = UiStyles::default();
                 $process_expected();
 
-                let vars = MockVars { ls: $ls, exa: $exa };
+                let definitions = Definitions {
+                    ls:  Some($ls.into()),
+                    exa: Some($exa.into()),
+                };
 
-                let mut result = Colours::colourful(false);
-                let (_exts, _reset) = parse_color_vars(&vars, &mut result);
+                let mut result = UiStyles::default();
+                let (_exts, _reset) = definitions.parse_color_vars(&mut result);
                 assert_eq!($expected, result);
             }
         };
@@ -374,18 +368,19 @@ mod customs_test {
                                .map(|t| (glob::Pattern::new(t.0).unwrap(), t.1))
                                .collect();
 
-                let vars = MockVars { ls: $ls, exa: $exa };
+                let definitions = Definitions {
+                    ls:  Some($ls.into()),
+                    exa: Some($exa.into()),
+                };
 
-                let mut meh = Colours::colourful(false);
-                let (result, _reset) = parse_color_vars(&vars, &mut meh);
+                let (result, _reset) = definitions.parse_color_vars(&mut UiStyles::default());
                 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);
+                let mut $expected = UiStyles::colourful(false);
                 $process_expected();
 
                 let mappings: Vec<(glob::Pattern, Style)>
@@ -393,37 +388,19 @@ mod customs_test {
                                .map(|t| (glob::Pattern::new(t.0).unwrap(), t.1))
                                .collect();
 
-                let vars = MockVars { ls: $ls, exa: $exa };
+                let definitions = Definitions {
+                    ls:  Some($ls.into()),
+                    exa: Some($exa.into()),
+                };
 
-                let mut meh = Colours::colourful(false);
-                let (result, _reset) = parse_color_vars(&vars, &mut meh);
+                let mut meh = UiStyles::colourful(false);
+                let (result, _reset) = definitions.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 crate::options::vars;
-
-            if name == vars::LS_COLORS && ! self.ls.is_empty() {
-                Some(OsString::from(self.ls.clone()))
-            }
-            else if name == vars::EXA_COLORS && ! self.exa.is_empty() {
-                Some(OsString::from(self.exa.clone()))
-            }
-            else {
-                None
-            }
-        }
-    }
 
     // LS_COLORS can affect all of these colours:
     test!(ls_di:   ls "di=31", exa ""  =>  colours c -> { c.filekinds.directory    = Red.normal();    });

+ 217 - 0
src/theme/ui_styles.rs

@@ -0,0 +1,217 @@
+use ansi_term::Style;
+
+use crate::theme::lsc::Pair;
+
+
+#[derive(Debug, Default, PartialEq)]
+pub struct UiStyles {
+    pub colourful: bool,
+
+    pub filekinds:  FileKinds,
+    pub perms:      Permissions,
+    pub size:       Size,
+    pub users:      Users,
+    pub links:      Links,
+    pub git:        Git,
+
+    pub punctuation:  Style,
+    pub date:         Style,
+    pub inode:        Style,
+    pub blocks:       Style,
+    pub header:       Style,
+    pub octal:        Style,
+
+    pub symlink_path:         Style,
+    pub control_char:         Style,
+    pub broken_symlink:       Style,
+    pub broken_path_overlay:  Style,
+}
+
+#[derive(Clone, Copy, Debug, Default, PartialEq)]
+pub struct FileKinds {
+    pub normal: Style,
+    pub directory: Style,
+    pub symlink: Style,
+    pub pipe: Style,
+    pub block_device: Style,
+    pub char_device: Style,
+    pub socket: Style,
+    pub special: Style,
+    pub executable: Style,
+}
+
+#[derive(Clone, Copy, Debug, Default, PartialEq)]
+pub struct Permissions {
+    pub user_read:          Style,
+    pub user_write:         Style,
+    pub user_execute_file:  Style,
+    pub user_execute_other: Style,
+
+    pub group_read:    Style,
+    pub group_write:   Style,
+    pub group_execute: Style,
+
+    pub other_read:    Style,
+    pub other_write:   Style,
+    pub other_execute: Style,
+
+    pub special_user_file: Style,
+    pub special_other:     Style,
+
+    pub attribute: Style,
+}
+
+#[derive(Clone, Copy, Debug, Default, PartialEq)]
+pub struct Size {
+    pub major: Style,
+    pub minor: Style,
+
+    pub number_byte: Style,
+    pub number_kilo: Style,
+    pub number_mega: Style,
+    pub number_giga: Style,
+    pub number_huge: Style,
+
+    pub unit_byte: Style,
+    pub unit_kilo: Style,
+    pub unit_mega: Style,
+    pub unit_giga: Style,
+    pub unit_huge: Style,
+}
+
+#[derive(Clone, Copy, Debug, Default, PartialEq)]
+pub struct Users {
+    pub user_you: Style,
+    pub user_someone_else: Style,
+    pub group_yours: Style,
+    pub group_not_yours: Style,
+}
+
+#[derive(Clone, Copy, Debug, Default, PartialEq)]
+pub struct Links {
+    pub normal: Style,
+    pub multi_link_file: Style,
+}
+
+#[derive(Clone, Copy, Debug, Default, PartialEq)]
+pub struct Git {
+    pub new: Style,
+    pub modified: Style,
+    pub deleted: Style,
+    pub renamed: Style,
+    pub typechange: Style,
+    pub ignored: Style,
+    pub conflicted: Style,
+}
+
+impl UiStyles {
+    pub fn plain() -> Self {
+        Self::default()
+    }
+}
+
+
+impl UiStyles {
+
+    /// Sets a value on this set of colours using one of the keys understood
+    /// by the `LS_COLORS` environment variable. Invalid keys set nothing, but
+    /// return false.
+    pub fn set_ls(&mut self, pair: &Pair<'_>) -> bool {
+        match pair.key {
+            "di" => self.filekinds.directory    = pair.to_style(),  // DIR
+            "ex" => self.filekinds.executable   = pair.to_style(),  // EXEC
+            "fi" => self.filekinds.normal       = pair.to_style(),  // FILE
+            "pi" => self.filekinds.pipe         = pair.to_style(),  // FIFO
+            "so" => self.filekinds.socket       = pair.to_style(),  // SOCK
+            "bd" => self.filekinds.block_device = pair.to_style(),  // BLK
+            "cd" => self.filekinds.char_device  = pair.to_style(),  // CHR
+            "ln" => self.filekinds.symlink      = pair.to_style(),  // LINK
+            "or" => self.broken_symlink         = pair.to_style(),  // ORPHAN
+             _   => return false,
+             // Codes we don’t do anything with:
+             // MULTIHARDLINK, DOOR, SETUID, SETGID, CAPABILITY,
+             // STICKY_OTHER_WRITABLE, OTHER_WRITABLE, STICKY, MISSING
+        }
+        true
+    }
+
+    /// Sets a value on this set of colours using one of the keys understood
+    /// by the `EXA_COLORS` environment variable. Invalid keys set nothing,
+    /// but return false. This doesn’t take the `LS_COLORS` keys into account,
+    /// so `set_ls` should have been run first.
+    pub fn set_exa(&mut self, pair: &Pair<'_>) -> bool {
+        match pair.key {
+            "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.set_number_style(pair.to_style()),
+            "sb" => self.set_unit_style(pair.to_style()),
+            "nb" => self.size.number_byte         = pair.to_style(),
+            "nk" => self.size.number_kilo         = pair.to_style(),
+            "nm" => self.size.number_mega         = pair.to_style(),
+            "ng" => self.size.number_giga         = pair.to_style(),
+            "nh" => self.size.number_huge         = pair.to_style(),
+            "ub" => self.size.unit_byte           = pair.to_style(),
+            "uk" => self.size.unit_kilo           = pair.to_style(),
+            "um" => self.size.unit_mega           = pair.to_style(),
+            "ug" => self.size.unit_giga           = pair.to_style(),
+            "uh" => self.size.unit_huge           = 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(),
+            "bO" => self.broken_path_overlay      = pair.to_style(),
+
+             _   => return false,
+        }
+
+        true
+    }
+
+    pub fn set_number_style(&mut self, style: Style) {
+        self.size.number_byte = style;
+        self.size.number_kilo = style;
+        self.size.number_mega = style;
+        self.size.number_giga = style;
+        self.size.number_huge = style;
+    }
+
+    pub fn set_unit_style(&mut self, style: Style) {
+        self.size.unit_byte = style;
+        self.size.unit_kilo = style;
+        self.size.unit_mega = style;
+        self.size.unit_giga = style;
+        self.size.unit_huge = style;
+    }
+}

+ 79 - 0
xtests/icons.toml

@@ -0,0 +1,79 @@
+# view icons tests
+
+[[cmd]]
+name = "‘exa -1 --icons’ shows icons next to file names in lines mode"
+shell = "exa -1 --icons /testcases/files"
+stdout = { file = "outputs/files_oneline_icons.ansitxt" }
+stderr = { empty = true }
+status = 0
+tags = [ 'oneline', 'icons' ]
+
+[[cmd]]
+name = "‘exa --icons’ shows icons next to file names in grid mode"
+shell = "exa --icons /testcases/files"
+environment = { COLUMNS = "80" }
+stdout = { file = "outputs/files_grid_icons.ansitxt" }
+stderr = { empty = true }
+status = 0
+tags = [ 'env', 'grid', 'icons' ]
+
+[[cmd]]
+name = "‘exa -l --icons’ shows icons next to file names in long mode"
+shell = "exa -l --icons /testcases/files"
+stdout = { file = "outputs/files_long_icons.ansitxt" }
+stderr = { empty = true }
+status = 0
+tags = [ 'long', 'icons' ]
+
+[[cmd]]
+name = "‘exa -lG --icons’ shows icons next to file names in long-grid mode"
+shell = "exa -lG --icons /testcases/files"
+environment = { COLUMNS = "80" }
+stdout = { file = "outputs/files_long_grid_icons.ansitxt" }
+stderr = { empty = true }
+status = 0
+tags = [ 'env', 'long', 'grid', 'icons' ]
+
+[[cmd]]
+name = "‘exa -T --icons’ shows icons next to file names in tree mode"
+shell = "exa -T --icons /testcases/files"
+environment = { COLUMNS = "80" }
+stdout = { file = "outputs/files_tree_icons.ansitxt" }
+stderr = { empty = true }
+status = 0
+tags = [ 'tree', 'icons' ]
+
+[[cmd]]
+name = "‘exa -lT --icons’ shows icons next to file names in long-tree mode"
+shell = "exa -lT --icons /testcases/files"
+stdout = { file = "outputs/files_long_tree_icons.ansitxt" }
+stderr = { empty = true }
+status = 0
+tags = [ 'long', 'tree', 'icons' ]
+
+
+# file type icons tests
+
+[[cmd]]
+name = "‘exa -1 --icons’ produces icons based on file types"
+shell = "exa -1 --icons /testcases/file-names-exts"
+stdout = { file = "outputs/exts_oneline_icons.ansitxt" }
+stderr = { empty = true }
+status = 0
+tags = [ 'oneline', 'icons' ]
+
+[[cmd]]
+name = "‘exa -1 --icons’ produces icons based on permissions"
+shell = "exa -1 --icons /testcases/permissions"
+stdout = { file = "outputs/permissions_oneline_icons.ansitxt" }
+stderr = { empty = true }
+status = 0
+tags = [ 'oneline', 'icons' ]
+
+[[cmd]]
+name = "‘exa -1 --icons’ produces icons for links"
+shell = "exa -1 --icons /testcases/links"
+stdout = { file = "outputs/links_oneline_icons.ansitxt" }
+stderr = { empty = true }
+status = 0
+tags = [ 'oneline', 'icons' ]

+ 26 - 0
xtests/outputs/exts_oneline_icons.ansitxt

@@ -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

+ 6 - 0
xtests/outputs/files_grid_icons.ansitxt

@@ -0,0 +1,6 @@
+  1_bytes    3_bytes    5_bytes    7_bytes    9_bytes     11_bytes    13_bytes
+  1_KiB      3_KiB      5_KiB      7_KiB      9_KiB       11_KiB      13_KiB
+  1_MiB      3_MiB      5_MiB      7_MiB      9_MiB       11_MiB      13_MiB
+  2_bytes    4_bytes    6_bytes    8_bytes    10_bytes    12_bytes  
+  2_KiB      4_KiB      6_KiB      8_KiB      10_KiB      12_KiB    
+  2_MiB      4_MiB      6_MiB      8_MiB      10_MiB      12_MiB    

+ 39 - 0
xtests/outputs/files_long_grid_icons.ansitxt

@@ -0,0 +1,39 @@
+.rw-r--r--    1 cassowary  1 Jan 12:34   1_bytes
+.rw-r--r-- 1.0k cassowary  1 Jan 12:34   1_KiB
+.rw-r--r-- 1.0M cassowary  1 Jan 12:34   1_MiB
+.rw-r--r--    2 cassowary  1 Jan 12:34   2_bytes
+.rw-r--r-- 2.0k cassowary  1 Jan 12:34   2_KiB
+.rw-r--r-- 2.1M cassowary  1 Jan 12:34   2_MiB
+.rw-r--r--    3 cassowary  1 Jan 12:34   3_bytes
+.rw-r--r-- 3.1k cassowary  1 Jan 12:34   3_KiB
+.rw-r--r-- 3.1M cassowary  1 Jan 12:34   3_MiB
+.rw-r--r--    4 cassowary  1 Jan 12:34   4_bytes
+.rw-r--r-- 4.1k cassowary  1 Jan 12:34   4_KiB
+.rw-r--r-- 4.2M cassowary  1 Jan 12:34   4_MiB
+.rw-r--r--    5 cassowary  1 Jan 12:34   5_bytes
+.rw-r--r-- 5.1k cassowary  1 Jan 12:34   5_KiB
+.rw-r--r-- 5.2M cassowary  1 Jan 12:34   5_MiB
+.rw-r--r--    6 cassowary  1 Jan 12:34   6_bytes
+.rw-r--r-- 6.1k cassowary  1 Jan 12:34   6_KiB
+.rw-r--r-- 6.3M cassowary  1 Jan 12:34   6_MiB
+.rw-r--r--    7 cassowary  1 Jan 12:34   7_bytes
+.rw-r--r-- 7.2k cassowary  1 Jan 12:34   7_KiB
+.rw-r--r-- 7.3M cassowary  1 Jan 12:34   7_MiB
+.rw-r--r--    8 cassowary  1 Jan 12:34   8_bytes
+.rw-r--r-- 8.2k cassowary  1 Jan 12:34   8_KiB
+.rw-r--r-- 8.4M cassowary  1 Jan 12:34   8_MiB
+.rw-r--r--    9 cassowary  1 Jan 12:34   9_bytes
+.rw-r--r-- 9.2k cassowary  1 Jan 12:34   9_KiB
+.rw-r--r-- 9.4M cassowary  1 Jan 12:34   9_MiB
+.rw-r--r--   10 cassowary  1 Jan 12:34   10_bytes
+.rw-r--r--  10k cassowary  1 Jan 12:34   10_KiB
+.rw-r--r--  10M cassowary  1 Jan 12:34   10_MiB
+.rw-r--r--   11 cassowary  1 Jan 12:34   11_bytes
+.rw-r--r--  11k cassowary  1 Jan 12:34   11_KiB
+.rw-r--r--  11M cassowary  1 Jan 12:34   11_MiB
+.rw-r--r--   12 cassowary  1 Jan 12:34   12_bytes
+.rw-r--r--  12k cassowary  1 Jan 12:34   12_KiB
+.rw-r--r--  12M cassowary  1 Jan 12:34   12_MiB
+.rw-r--r--   13 cassowary  1 Jan 12:34   13_bytes
+.rw-r--r--  13k cassowary  1 Jan 12:34   13_KiB
+.rw-r--r--  13M cassowary  1 Jan 12:34   13_MiB

+ 39 - 0
xtests/outputs/files_long_icons.ansitxt

@@ -0,0 +1,39 @@
+.rw-r--r--    1 cassowary  1 Jan 12:34   1_bytes
+.rw-r--r-- 1.0k cassowary  1 Jan 12:34   1_KiB
+.rw-r--r-- 1.0M cassowary  1 Jan 12:34   1_MiB
+.rw-r--r--    2 cassowary  1 Jan 12:34   2_bytes
+.rw-r--r-- 2.0k cassowary  1 Jan 12:34   2_KiB
+.rw-r--r-- 2.1M cassowary  1 Jan 12:34   2_MiB
+.rw-r--r--    3 cassowary  1 Jan 12:34   3_bytes
+.rw-r--r-- 3.1k cassowary  1 Jan 12:34   3_KiB
+.rw-r--r-- 3.1M cassowary  1 Jan 12:34   3_MiB
+.rw-r--r--    4 cassowary  1 Jan 12:34   4_bytes
+.rw-r--r-- 4.1k cassowary  1 Jan 12:34   4_KiB
+.rw-r--r-- 4.2M cassowary  1 Jan 12:34   4_MiB
+.rw-r--r--    5 cassowary  1 Jan 12:34   5_bytes
+.rw-r--r-- 5.1k cassowary  1 Jan 12:34   5_KiB
+.rw-r--r-- 5.2M cassowary  1 Jan 12:34   5_MiB
+.rw-r--r--    6 cassowary  1 Jan 12:34   6_bytes
+.rw-r--r-- 6.1k cassowary  1 Jan 12:34   6_KiB
+.rw-r--r-- 6.3M cassowary  1 Jan 12:34   6_MiB
+.rw-r--r--    7 cassowary  1 Jan 12:34   7_bytes
+.rw-r--r-- 7.2k cassowary  1 Jan 12:34   7_KiB
+.rw-r--r-- 7.3M cassowary  1 Jan 12:34   7_MiB
+.rw-r--r--    8 cassowary  1 Jan 12:34   8_bytes
+.rw-r--r-- 8.2k cassowary  1 Jan 12:34   8_KiB
+.rw-r--r-- 8.4M cassowary  1 Jan 12:34   8_MiB
+.rw-r--r--    9 cassowary  1 Jan 12:34   9_bytes
+.rw-r--r-- 9.2k cassowary  1 Jan 12:34   9_KiB
+.rw-r--r-- 9.4M cassowary  1 Jan 12:34   9_MiB
+.rw-r--r--   10 cassowary  1 Jan 12:34   10_bytes
+.rw-r--r--  10k cassowary  1 Jan 12:34   10_KiB
+.rw-r--r--  10M cassowary  1 Jan 12:34   10_MiB
+.rw-r--r--   11 cassowary  1 Jan 12:34   11_bytes
+.rw-r--r--  11k cassowary  1 Jan 12:34   11_KiB
+.rw-r--r--  11M cassowary  1 Jan 12:34   11_MiB
+.rw-r--r--   12 cassowary  1 Jan 12:34   12_bytes
+.rw-r--r--  12k cassowary  1 Jan 12:34   12_KiB
+.rw-r--r--  12M cassowary  1 Jan 12:34   12_MiB
+.rw-r--r--   13 cassowary  1 Jan 12:34   13_bytes
+.rw-r--r--  13k cassowary  1 Jan 12:34   13_KiB
+.rw-r--r--  13M cassowary  1 Jan 12:34   13_MiB

+ 40 - 0
xtests/outputs/files_long_tree_icons.ansitxt

@@ -0,0 +1,40 @@
+drwxrwxr-x    - vagrant   18 Oct 00:18   /testcases/files
+.rw-r--r--    1 cassowary  1 Jan 12:34 ├──   1_bytes
+.rw-r--r-- 1.0k cassowary  1 Jan 12:34 ├──   1_KiB
+.rw-r--r-- 1.0M cassowary  1 Jan 12:34 ├──   1_MiB
+.rw-r--r--    2 cassowary  1 Jan 12:34 ├──   2_bytes
+.rw-r--r-- 2.0k cassowary  1 Jan 12:34 ├──   2_KiB
+.rw-r--r-- 2.1M cassowary  1 Jan 12:34 ├──   2_MiB
+.rw-r--r--    3 cassowary  1 Jan 12:34 ├──   3_bytes
+.rw-r--r-- 3.1k cassowary  1 Jan 12:34 ├──   3_KiB
+.rw-r--r-- 3.1M cassowary  1 Jan 12:34 ├──   3_MiB
+.rw-r--r--    4 cassowary  1 Jan 12:34 ├──   4_bytes
+.rw-r--r-- 4.1k cassowary  1 Jan 12:34 ├──   4_KiB
+.rw-r--r-- 4.2M cassowary  1 Jan 12:34 ├──   4_MiB
+.rw-r--r--    5 cassowary  1 Jan 12:34 ├──   5_bytes
+.rw-r--r-- 5.1k cassowary  1 Jan 12:34 ├──   5_KiB
+.rw-r--r-- 5.2M cassowary  1 Jan 12:34 ├──   5_MiB
+.rw-r--r--    6 cassowary  1 Jan 12:34 ├──   6_bytes
+.rw-r--r-- 6.1k cassowary  1 Jan 12:34 ├──   6_KiB
+.rw-r--r-- 6.3M cassowary  1 Jan 12:34 ├──   6_MiB
+.rw-r--r--    7 cassowary  1 Jan 12:34 ├──   7_bytes
+.rw-r--r-- 7.2k cassowary  1 Jan 12:34 ├──   7_KiB
+.rw-r--r-- 7.3M cassowary  1 Jan 12:34 ├──   7_MiB
+.rw-r--r--    8 cassowary  1 Jan 12:34 ├──   8_bytes
+.rw-r--r-- 8.2k cassowary  1 Jan 12:34 ├──   8_KiB
+.rw-r--r-- 8.4M cassowary  1 Jan 12:34 ├──   8_MiB
+.rw-r--r--    9 cassowary  1 Jan 12:34 ├──   9_bytes
+.rw-r--r-- 9.2k cassowary  1 Jan 12:34 ├──   9_KiB
+.rw-r--r-- 9.4M cassowary  1 Jan 12:34 ├──   9_MiB
+.rw-r--r--   10 cassowary  1 Jan 12:34 ├──   10_bytes
+.rw-r--r--  10k cassowary  1 Jan 12:34 ├──   10_KiB
+.rw-r--r--  10M cassowary  1 Jan 12:34 ├──   10_MiB
+.rw-r--r--   11 cassowary  1 Jan 12:34 ├──   11_bytes
+.rw-r--r--  11k cassowary  1 Jan 12:34 ├──   11_KiB
+.rw-r--r--  11M cassowary  1 Jan 12:34 ├──   11_MiB
+.rw-r--r--   12 cassowary  1 Jan 12:34 ├──   12_bytes
+.rw-r--r--  12k cassowary  1 Jan 12:34 ├──   12_KiB
+.rw-r--r--  12M cassowary  1 Jan 12:34 ├──   12_MiB
+.rw-r--r--   13 cassowary  1 Jan 12:34 ├──   13_bytes
+.rw-r--r--  13k cassowary  1 Jan 12:34 ├──   13_KiB
+.rw-r--r--  13M cassowary  1 Jan 12:34 └──   13_MiB

+ 39 - 0
xtests/outputs/files_oneline_icons.ansitxt

@@ -0,0 +1,39 @@
+  1_bytes
+  1_KiB
+  1_MiB
+  2_bytes
+  2_KiB
+  2_MiB
+  3_bytes
+  3_KiB
+  3_MiB
+  4_bytes
+  4_KiB
+  4_MiB
+  5_bytes
+  5_KiB
+  5_MiB
+  6_bytes
+  6_KiB
+  6_MiB
+  7_bytes
+  7_KiB
+  7_MiB
+  8_bytes
+  8_KiB
+  8_MiB
+  9_bytes
+  9_KiB
+  9_MiB
+  10_bytes
+  10_KiB
+  10_MiB
+  11_bytes
+  11_KiB
+  11_MiB
+  12_bytes
+  12_KiB
+  12_MiB
+  13_bytes
+  13_KiB
+  13_MiB

+ 40 - 0
xtests/outputs/files_tree_icons.ansitxt

@@ -0,0 +1,40 @@
+  /testcases/files
+├──   1_bytes
+├──   1_KiB
+├──   1_MiB
+├──   2_bytes
+├──   2_KiB
+├──   2_MiB
+├──   3_bytes
+├──   3_KiB
+├──   3_MiB
+├──   4_bytes
+├──   4_KiB
+├──   4_MiB
+├──   5_bytes
+├──   5_KiB
+├──   5_MiB
+├──   6_bytes
+├──   6_KiB
+├──   6_MiB
+├──   7_bytes
+├──   7_KiB
+├──   7_MiB
+├──   8_bytes
+├──   8_KiB
+├──   8_MiB
+├──   9_bytes
+├──   9_KiB
+├──   9_MiB
+├──   10_bytes
+├──   10_KiB
+├──   10_MiB
+├──   11_bytes
+├──   11_KiB
+├──   11_MiB
+├──   12_bytes
+├──   12_KiB
+├──   12_MiB
+├──   13_bytes
+├──   13_KiB
+└──   13_MiB

+ 10 - 0
xtests/outputs/links_oneline_icons.ansitxt

@@ -0,0 +1,10 @@
+  broken -> nowhere
+  current_dir -> .
+  forbidden -> /proc/1/root
+  itself -> itself
+  parent_dir -> ..
+  root -> /
+  some_file
+  some_file_absolute -> /testcases/links/some_file
+  some_file_relative -> some_file
+  usr -> /usr

+ 22 - 0
xtests/outputs/permissions_oneline_icons.ansitxt

@@ -0,0 +1,22 @@
+  000
+  001
+  002
+  004
+  010
+  020
+  040
+  100
+  200
+  400
+  644
+  755
+  777
+  1000
+  1001
+  2000
+  2010
+  4000
+  4100
+  7666
+  7777
+  forbidden-directory