From 3419afa7cf93d6ef0f7703a69192f076c0ddb2c5 Mon Sep 17 00:00:00 2001 From: Benjamin Sago Date: Thu, 22 Oct 2020 22:34:00 +0100 Subject: [PATCH] Massive theming and view options refactor This commit significantly refactors the way that options are parsed. It introduces the Theme type which contains both styling and extension configuration, converts the option-parsing process into a being a pure function, and removes some rather gnarly old code. The main purpose of the refactoring is to fix GH-318, "Tests fail when not connected to a terminal". Even though exa was compiling fine on my machine and on Travis, it was failing for automated build scripts. This was because of what the option-parsing code was trying to accomplish: it wasn't just providing a struct of the user's settings, it was also checking the terminal, providing a View directly. This has been changed so that the options module now _only_ looks at the command-line arguments and environment variables. Instead of returning a View, it returns the user's _preference_, and it's then up to the 'main' module to examine the terminal width and figure out if the view is doable, downgrading it if necessary. The code that used to determine the view was horrible and I'm pleased it can be cut out. Also, the terminal width used to be in a lazy_static because it was queried multiple times, and now it's not in one because it's only queried once, which is a good sign for things going in the right direction. There are also some naming and organisational changes around themes. The blanket terms "Colours" and "Styles" have been yeeted in favour of "Theme", which handles both extensions and UI colours. The FileStyle struct has been replaced with file_name::Options, making it similar to the views in how it has an Options struct and a Render struct. Finally, eight unit tests have been removed because they turned out to be redundant (testing --colour and --color) after examining the tangled code, and the default theme has been put in its own file in preparation for more themes. --- src/info/filetype.rs | 2 +- src/main.rs | 60 ++- src/options/file_name.rs | 21 + src/options/mod.rs | 15 +- src/options/theme.rs | 159 +++++++ src/options/view.rs | 215 ++++------ src/output/details.rs | 24 +- src/output/file_name.rs | 88 ++-- src/output/grid.rs | 29 +- src/output/grid_details.rs | 43 +- src/output/icons.rs | 37 +- src/output/lines.rs | 21 +- src/output/mod.rs | 32 +- src/output/table.rs | 36 +- src/style/colours.rs | 471 --------------------- src/style/mod.rs | 6 - src/theme/default_theme.rs | 130 ++++++ src/{style => theme}/lsc.rs | 5 +- src/{options/style.rs => theme/mod.rs} | 549 ++++++++++++------------- src/theme/ui_styles.rs | 217 ++++++++++ 20 files changed, 1083 insertions(+), 1077 deletions(-) create mode 100644 src/options/file_name.rs create mode 100644 src/options/theme.rs delete mode 100644 src/style/colours.rs delete mode 100644 src/style/mod.rs create mode 100644 src/theme/default_theme.rs rename src/{style => theme}/lsc.rs (97%) rename src/{options/style.rs => theme/mod.rs} (53%) create mode 100644 src/theme/ui_styles.rs diff --git a/src/info/filetype.rs b/src/info/filetype.rs index 83f33f6..38b1ac0 100644 --- a/src/info/filetype.rs +++ b/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)] diff --git a/src/main.rs b/src/main.rs index 3f94d4c..0e2fbbb 100644 --- a/src/main.rs +++ b/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, + /// 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,62 @@ 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>) -> 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(ref opts), None) => { + let opts = &opts.to_lines_options(); + let r = lines::Render { files, theme, file_style, opts }; r.render(&mut self.writer) } - Mode::Details(ref opts) => { + (Mode::Lines(ref opts), _) => { + let r = lines::Render { files, theme, file_style, opts }; + r.render(&mut self.writer) + } + + (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, colours, style, grid, details, filter, row_threshold, git_ignoring, git }; + + 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 = details::Render { dir, files, theme, file_style, opts, filter, recurse, git_ignoring, git }; r.render(&mut self.writer) } } diff --git a/src/options/file_name.rs b/src/options/file_name.rs new file mode 100644 index 0000000..9092b8c --- /dev/null +++ b/src/options/file_name.rs @@ -0,0 +1,21 @@ +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 { + Classify::deduce(matches) + .map(|classify| Self { classify }) + } +} + +impl Classify { + fn deduce(matches: &MatchedFlags<'_>) -> Result { + let flagged = matches.has(&flags::CLASSIFY)?; + + if flagged { Ok(Self::AddFileIndicators) } + else { Ok(Self::JustFilenames) } + } +} diff --git a/src/options/mod.rs b/src/options/mod.rs index 324bfe8..2565d28 100644 --- a/src/options/mod.rs +++ b/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 { @@ -171,8 +179,9 @@ impl Options { let dir_action = DirAction::deduce(matches)?; let filter = FileFilter::deduce(matches)?; let view = View::deduce(matches, vars)?; + let theme = ThemeOptions::deduce(matches, vars)?; - Ok(Self { dir_action, view, filter }) + Ok(Self { dir_action, filter, view, theme }) } } diff --git a/src/options/theme.rs b/src/options/theme.rs new file mode 100644 index 0000000..02309f7 --- /dev/null +++ b/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(matches: &MatchedFlags<'_>, vars: &V) -> Result { + 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 { + 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 { + 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(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 { + 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)); +} diff --git a/src/options/view.rs b/src/options/view.rs index 8786e89..c8c385e 100644 --- a/src/options/view.rs +++ b/src/options/view.rs @@ -1,114 +1,34 @@ -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, lines}; 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(matches: &MatchedFlags<'_>, vars: &V) -> Result { - 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. pub fn deduce(matches: &MatchedFlags<'_>, vars: &V) -> Result { - 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)) - } - }; if matches.has(&flags::LONG)? { - let details = long()?; + let details = details::Options::deduce_long(matches, vars)?; + 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); - } + 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 { return Ok(Self::Details(details)); @@ -129,36 +49,79 @@ 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() + if matches.has(&flags::TREE)? { + let details = details::Options::deduce_tree(matches)?; + return Ok(Self::Details(details)); + } + + if matches.has(&flags::ONE_LINE)? { + let lines = lines::Options::deduce(matches)?; + return Ok(Self::Lines(lines)); + } + + let grid = grid::Options::deduce(matches)?; + Ok(Self::Grid(grid)) } } -/// The width of the terminal requested by the user. -#[derive(PartialEq, Debug, Copy, Clone)] -enum TerminalWidth { - - /// The user requested this specific number of columns. - Set(usize), - - /// The terminal was found to have this number of columns. - Terminal(usize), - - /// The user didn’t request any particular terminal width. - Unset, +impl lines::Options { + fn deduce(matches: &MatchedFlags<'_>) -> Result { + let lines = lines::Options { icons: matches.has(&flags::ICONS)? }; + Ok(lines) + } } -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 grid::Options { + fn deduce(matches: &MatchedFlags<'_>) -> Result { + let grid = grid::Options { + across: matches.has(&flags::ACROSS)?, + icons: matches.has(&flags::ICONS)?, + }; + + Ok(grid) + } +} + + +impl details::Options { + fn deduce_tree(matches: &MatchedFlags<'_>) -> Result { + let details = details::Options { + table: None, + header: false, + xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?, + icons: matches.has(&flags::ICONS)?, + }; + + Ok(details) + } + + fn deduce_long(matches: &MatchedFlags<'_>, vars: &V) -> Result { + 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)); + } + } + + 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)?, + }) + } +} + + +impl TerminalWidth { fn deduce(vars: &V) -> Result { use crate::options::vars; @@ -168,28 +131,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 { - 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(vars: &V) -> Result { use crate::options::vars; @@ -357,20 +306,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 = { - // 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::*; @@ -386,7 +321,7 @@ mod test { &flags::CREATED, &flags::ACCESSED, &flags::ICONS, &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 { diff --git a/src/output/details.rs b/src/output/details.rs index 099d524..f9a1944 100644 --- a/src/output/details.rs +++ b/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 @@ -115,8 +115,8 @@ pub struct Options { pub struct Render<'a> { pub dir: Option<&'a Dir>, pub files: Vec>, - 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 +162,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(); @@ -266,7 +266,7 @@ impl<'a> Render<'a> { } }; - let icon = if self.opts.icons { Some(painted_icon(file, self.style)) } + let icon = if self.opts.icons { Some(painted_icon(file, self.theme)) } else { None }; let egg = Egg { table_row, xattrs, errors, dir, file, icon }; @@ -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, } } } diff --git a/src/output/file_name.rs b/src/output/file_name.rs index ab80194..0165f06 100644 --- a/src/output/file_name.rs +++ b/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,31 @@ 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, + // todo: put icons here } -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 +72,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 +81,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>, + target: Option>, // 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 +97,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 +140,16 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> { } if ! target.name.is_empty() { + let target_options = Options { + classify: Classify::JustFilenames, + }; + 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 +176,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 +185,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>, parent: &Path) { @@ -208,7 +204,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 +227,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 +251,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 +264,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