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..ad1a797 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,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>) -> 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, 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/dir_action.rs b/src/options/dir_action.rs index c87f131..0cc31fa 100644 --- a/src/options/dir_action.rs +++ b/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 { + pub fn deduce(matches: &MatchedFlags<'_>, can_tree: bool) -> Result { 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); } } diff --git a/src/options/file_name.rs b/src/options/file_name.rs new file mode 100644 index 0000000..283562a --- /dev/null +++ b/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 { + let classify = Classify::deduce(matches)?; + let icons = matches.has(&flags::ICONS)?; + + Ok(Self { classify, icons }) + } +} + +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..ae974c4 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 { @@ -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(matches: &MatchedFlags<'_>, vars: &V) -> Result { - 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 }) } } diff --git a/src/options/parser.rs b/src/options/parser.rs index ade5744..f43d44e 100644 --- a/src/options/parser.rs +++ b/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

(&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. 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..bb97286 100644 --- a/src/options/view.rs +++ b/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(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. + /// 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(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 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)); } }; - 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)?, - }; + 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)?; - Ok(Self::Details(details)) - } - else { - let grid = grid::Options { - across: matches.has(&flags::ACROSS)?, - console_width: width, - icons: matches.has(&flags::ICONS)?, - }; + let flag = matches.has_where_any(|f| f.matches(&flags::GRID) || f.matches(&flags::TREE)); - 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()?; - 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.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 { + let grid = grid::Options { + across: matches.has(&flags::ACROSS)?, + }; - /// 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, + Ok(grid) + } } -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 details::Options { + fn deduce_tree(matches: &MatchedFlags<'_>) -> Result { + let details = details::Options { + table: None, + header: false, + xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?, + }; + + 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)?, + }) + } +} + + +impl TerminalWidth { fn deduce(vars: &V) -> Result { 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 { - 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 +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 = { - // 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(_))); } } diff --git a/src/output/details.rs b/src/output/details.rs index 099d524..10f2404 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 @@ -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>, - 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, } } } diff --git a/src/output/file_name.rs b/src/output/file_name.rs index ab80194..70bdca8 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,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, + /// 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>, + 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 +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>, 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