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.
This commit is contained in:
Benjamin Sago 2020-10-22 22:34:00 +01:00
parent f0c63b64ec
commit 3419afa7cf
20 changed files with 1083 additions and 1077 deletions

View File

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

View File

@ -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 isnt 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,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<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(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)
}
}

21
src/options/file_name.rs Normal file
View File

@ -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<Self, OptionsError> {
Classify::deduce(matches)
.map(|classify| Self { classify })
}
}
impl Classify {
fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
let flagged = matches.has(&flags::CLASSIFY)?;
if flagged { Ok(Self::AddFileIndicators) }
else { Ok(Self::JustFilenames) }
}
}

View File

@ -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 users 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 })
}
}

159
src/options/theme.rs Normal file
View File

@ -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));
}

View File

@ -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 views 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.
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 couldnt be matched for some reason, such
// as the programs 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 grid = grid::Options::deduce(matches)?;
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_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: Im 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 didnt request any particular terminal width.
Unset,
impl lines::Options {
fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
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 doesnt parse to an integer.
impl grid::Options {
fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
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<Self, OptionsError> {
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<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));
}
}
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<V: Vars>(vars: &V) -> Result<Self, OptionsError> {
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<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 +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 its 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 were only interested in stdout because its
// 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 {

View File

@ -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<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 +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() doesnt quite seem like the right name for
// the style thats 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,
}
}
}

View File

@ -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<dyn FileColours>,
// todo: put icons here
}
impl FileStyle {
impl Options {
/// Create a new `FileName` that prints the given files 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 were 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 its 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 +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<ANSIString<'_>>, 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 files 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 files
/// 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<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 +273,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 +306,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;
}

View File

@ -4,31 +4,38 @@ 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::output::lines;
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 }
}
pub fn to_lines_options(self) -> lines::Options {
lines::Options {
icons: self.icons
}
}
}
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,10 +48,10 @@ 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)) }
let icon = if self.opts.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() };
@ -55,7 +62,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 {
@ -64,10 +71,10 @@ impl<'a> Render<'a> {
// displays full link paths.
for file in &self.files {
if self.opts.icons {
write!(w, "{}", painted_icon(file, self.style))?;
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())?;
}

View File

@ -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 wouldnt 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 columns 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 doesnt 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);
@ -147,13 +158,13 @@ impl<'a> Render<'a> {
.map(|file| {
if self.details.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 {

View File

@ -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,12 +28,14 @@ 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| {
let c = theme.colour_file(file);
// Remove underline from icon
let painted =
if c.is_underline {
match c.foreground {
Some(color) => {
@ -46,8 +48,7 @@ pub fn painted_icon(file: &File<'_>, style: &FileStyle) -> String {
}
else {
c.paint(file_icon).to_string()
}
});
};
format!("{} ", painted)
}

View File

@ -3,10 +3,10 @@ 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)]
@ -17,19 +17,19 @@ pub struct Options {
/// 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 theme: &'a Theme,
pub file_style: &'a FileStyle,
pub opts: &'a Options,
}
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();
let name_cell = self.render_file(file);
if self.opts.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 +42,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()
}
}

View File

@ -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,
}
@ -37,3 +34,28 @@ pub enum Mode {
GridDetails(grid_details::Options),
Lines(lines::Options),
}
/// 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 were only interested in stdout because its
// where the output goes.
match self {
Self::Set(width) => Some(width),
Self::Automatic => term_size::dimensions_stdout().map(|t| t.0),
}
}
}

View File

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

View File

@ -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), theyre 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 dont 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 doesnt 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 }
}

View File

@ -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
src/theme/default_theme.rs Normal file
View File

@ -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(),
}
}
}

View File

@ -71,7 +71,8 @@ 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()))
{
return Some(RGB(r, g, b));
}
}

View File

@ -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 isnt 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,
}
#[derive(PartialEq, Debug, Default)]
pub struct Definitions {
pub ls: Option<String>,
pub exa: Option<String>,
}
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()))
}
}
pub struct Theme {
pub ui: UiStyles,
pub exts: Box<dyn FileColours>,
}
/// **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 its better to only iterate through that once, the two are deduced
/// together.
pub struct Styles {
/// The colours to paint user interface elements, like the date column,
/// and file kinds, such as directories.
pub colours: Colours,
/// The colours to paint the names of files that match glob patterns
/// (and the classify option).
pub style: FileStyle,
}
impl Styles {
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,29 +86,25 @@ 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::*;
impl Definitions {
use crate::options::vars;
use crate::style::LSColors;
/// 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) = vars.get(vars::LS_COLORS) {
let lsc = lsc.to_string_lossy();
LSColors(lsc.as_ref()).each_pair(|pair| {
if let Some(lsc) = &self.ls {
LSColors(lsc).each_pair(|pair| {
if ! colours.set_ls(&pair) {
match glob::Pattern::new(pair.key) {
Ok(pat) => {
@ -149,15 +120,13 @@ fn parse_color_vars<V: Vars>(vars: &V, colours: &mut Colours) -> (ExtensionMappi
let mut use_default_filetypes = true;
if let Some(exa) = vars.get(vars::EXA_COLORS) {
let exa = exa.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(exa.as_ref()).each_pair(|pair| {
LSColors(exa).each_pair(|pair| {
if ! colours.set_ls(&pair) && ! colours.set_exa(&pair) {
match glob::Pattern::new(pair.key) {
Ok(pat) => {
@ -172,6 +141,34 @@ fn parse_color_vars<V: Vars>(vars: &V, colours: &mut Colours) -> (ExtensionMappi
}
(exts, use_default_filetypes)
}
}
pub trait FileColours: std::marker::Sync {
fn colour_file(&self, file: &File<'_>) -> Option<Style>;
}
#[derive(PartialEq, Debug)]
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))
}
}
@ -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) }
impl render::BlocksColours for Theme {
fn block_count(&self) -> Style { self.ui.blocks }
fn no_blocks(&self) -> Style { self.ui.punctuation }
}
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 }
}
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 }
}
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 }
}
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 }
}
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::*;
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,
}
}
fn unit(&self, prefix: Option<number_prefix::Prefix>) -> Style {
use number_prefix::Prefix::*;
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,
}
}
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 }
}
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 }
}
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)
}
}
#[cfg(test)]
mod terminal_test {
use super::*;
use std::ffi::OsString;
use crate::options::flags;
use crate::options::parser::{Flag, Arg};
/// Some of the styles are **overlays**: although they have the same attribute
/// set as regular styles (foreground and background colours, bold, underline,
/// etc), theyre 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); }
use crate::options::test::parse_for_test;
use crate::options::test::Strictnesses::*;
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; }
static TEST_ARGS: &[&Arg] = &[ &flags::COLOR, &flags::COLOUR ];
macro_rules! test {
($name:ident: $inputs:expr; $stricts:expr => $result:expr) => {
#[test]
fn $name() {
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| TerminalColours::deduce(mf)) {
assert_eq!(result, $result);
}
}
};
($name:ident: $inputs:expr; $stricts:expr => err $result:expr) => {
#[test]
fn $name() {
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| TerminalColours::deduce(mf)) {
assert_eq!(result.unwrap_err(), $result);
}
}
};
}
// Default
test!(empty: []; Both => Ok(TerminalColours::default()));
// --colour
test!(u_always: ["--colour=always"]; Both => Ok(TerminalColours::Always));
test!(u_auto: ["--colour", "auto"]; Both => Ok(TerminalColours::Automatic));
test!(u_never: ["--colour=never"]; Both => Ok(TerminalColours::Never));
// --color
test!(no_u_always: ["--color", "always"]; Both => Ok(TerminalColours::Always));
test!(no_u_auto: ["--color=auto"]; Both => Ok(TerminalColours::Automatic));
test!(no_u_never: ["--color", "never"]; Both => Ok(TerminalColours::Never));
// Errors
test!(no_u_error: ["--color=upstream"]; Both => err 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!
// Overriding
test!(overridden_1: ["--colour=auto", "--colour=never"]; Last => Ok(TerminalColours::Never));
test!(overridden_2: ["--color=auto", "--colour=never"]; Last => Ok(TerminalColours::Never));
test!(overridden_3: ["--colour=auto", "--color=never"]; Last => Ok(TerminalColours::Never));
test!(overridden_4: ["--color=auto", "--color=never"]; Last => Ok(TerminalColours::Never));
test!(overridden_5: ["--colour=auto", "--colour=never"]; Complain => err 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")));
}
#[cfg(test)]
mod colour_test {
use super::*;
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: $inputs:expr, $widther:expr; $stricts:expr => $result:expr) => {
#[test]
fn $name() {
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| Styles::deduce(mf, &None, &$widther).map(|s| s.colours)) {
assert_eq!(result, $result);
}
}
};
($name:ident: $inputs:expr, $widther:expr; $stricts:expr => err $result:expr) => {
#[test]
fn $name() {
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| Styles::deduce(mf, &None, &$widther).map(|s| s.colours)) {
assert_eq!(result.unwrap_err(), $result);
}
}
};
($name:ident: $inputs:expr, $widther:expr; $stricts:expr => like $pat:pat) => {
#[test]
fn $name() {
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| Styles::deduce(mf, &None, &$widther).map(|s| s.colours)) {
println!("Testing {:?}", result);
match result {
$pat => assert!(true),
_ => assert!(false),
}
}
}
};
}
test!(width_1: ["--colour", "always"], || Some(80); Both => Ok(Colours::colourful(false)));
test!(width_2: ["--colour", "always"], || None; Both => Ok(Colours::colourful(false)));
test!(width_3: ["--colour", "never"], || Some(80); Both => Ok(Colours::plain()));
test!(width_4: ["--colour", "never"], || None; Both => Ok(Colours::plain()));
test!(width_5: ["--colour", "automatic"], || Some(80); Both => Ok(Colours::colourful(false)));
test!(width_6: ["--colour", "automatic"], || None; Both => Ok(Colours::plain()));
test!(width_7: [], || Some(80); Both => Ok(Colours::colourful(false)));
test!(width_8: [], || None; Both => Ok(Colours::plain()));
test!(scale_1: ["--color=always", "--color-scale", "--colour-scale"], || None; Last => 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)));
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
src/theme/ui_styles.rs Normal file
View File

@ -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 dont 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 doesnt 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;
}
}