Merge branch 'view-options'

This commit is contained in:
Benjamin Sago 2020-10-24 17:53:55 +01:00
commit b05f18cae0
32 changed files with 1480 additions and 1110 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,57 @@ impl<'args> Exa<'args> {
}
/// Prints the list of files using whichever view is selected.
/// For various annoying logistical reasons, each one handles
/// printing differently...
fn print_files(&mut self, dir: Option<&Dir>, files: Vec<File<'_>>) -> io::Result<()> {
if files.is_empty() {
return Ok(());
}
let View { ref mode, ref colours, ref style } = self.options.view;
let theme = &self.theme;
let View { ref mode, ref file_style, .. } = self.options.view;
match mode {
Mode::Lines(ref opts) => {
let r = lines::Render { files, colours, style, opts };
match (mode, self.console_width) {
(Mode::Grid(ref opts), Some(console_width)) => {
let r = grid::Render { files, theme, file_style, opts, console_width };
r.render(&mut self.writer)
}
Mode::Grid(ref opts) => {
let r = grid::Render { files, colours, style, opts };
(Mode::Grid(_), None) |
(Mode::Lines, _) => {
let r = lines::Render { files, theme, file_style };
r.render(&mut self.writer)
}
Mode::Details(ref opts) => {
(Mode::Details(ref opts), _) => {
let filter = &self.options.filter;
let recurse = self.options.dir_action.recurse_options();
let git_ignoring = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore;
let git = self.git.as_ref();
let r = details::Render { dir, files, colours, style, opts, filter, recurse, git_ignoring, git };
let r = details::Render { dir, files, theme, file_style, opts, filter, recurse, git_ignoring, git };
r.render(&mut self.writer)
}
Mode::GridDetails(ref opts) => {
(Mode::GridDetails(ref opts), Some(console_width)) => {
let grid = &opts.grid;
let filter = &self.options.filter;
let details = &opts.details;
let row_threshold = opts.row_threshold;
let filter = &self.options.filter;
let git_ignoring = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore;
let git = self.git.as_ref();
let r = grid_details::Render { dir, files, 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)
}
}

View File

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

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

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

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 {
@ -168,11 +176,12 @@ impl Options {
/// Determines the complete set of options based on the given command-line
/// arguments, after theyve been parsed.
fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
let dir_action = DirAction::deduce(matches)?;
let filter = FileFilter::deduce(matches)?;
let view = View::deduce(matches, vars)?;
let dir_action = DirAction::deduce(matches, matches!(view.mode, Mode::Details(_)))?;
let filter = FileFilter::deduce(matches)?;
let theme = ThemeOptions::deduce(matches, vars)?;
Ok(Self { dir_action, view, filter })
Ok(Self { dir_action, filter, view, theme })
}
}

View File

@ -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.
///
/// Youll have to test the resulting flag to see which argument it was.
pub fn has_where_any<P>(&self, predicate: P) -> Option<&Flag>
where P: Fn(&Flag) -> bool {
self.flags.iter().rev()
.find(|tuple| tuple.1.is_none() && predicate(&tuple.0))
.map(|tuple| &tuple.0)
}
// This code could probably be better.
// Both has and get immediately begin with a conditional, which makes
// me think the functionality could be moved to inside Strictness.

159
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,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 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.
/// Determine which viewing mode to use based on the users options.
///
/// As with the other options, arguments are scanned right-to-left and the
/// first flag found is matched, so `exa --oneline --long` will pick a
/// details view, and `exa --long --oneline` will pick the lines view.
///
/// This is complicated a little by the fact that `--grid` and `--tree`
/// can also combine with `--long`, so care has to be taken to use the
pub fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
let long = || {
if matches.is_strict() && matches.has(&flags::ACROSS)? && ! matches.has(&flags::GRID)? {
Err(OptionsError::Useless(&flags::ACROSS, true, &flags::LONG))
}
else if matches.is_strict() && matches.has(&flags::ONE_LINE)? {
Err(OptionsError::Useless(&flags::ONE_LINE, true, &flags::LONG))
}
else {
Ok(details::Options {
table: Some(TableOptions::deduce(matches, vars)?),
header: matches.has(&flags::HEADER)?,
xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?,
icons: matches.has(&flags::ICONS)?,
})
let 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 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()?;
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 hasnt been passed, then check if we need to warn the
// user about flags that wont 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: 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()
Ok(())
}
}
/// The width of the terminal requested by the user.
#[derive(PartialEq, Debug, Copy, Clone)]
enum TerminalWidth {
impl grid::Options {
fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
let grid = grid::Options {
across: matches.has(&flags::ACROSS)?,
};
/// 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,
Ok(grid)
}
}
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 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)?,
};
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)?,
})
}
}
impl TerminalWidth {
fn deduce<V: Vars>(vars: &V) -> Result<Self, OptionsError> {
use crate::options::vars;
@ -168,28 +155,14 @@ impl TerminalWidth {
Err(e) => Err(OptionsError::FailedParse(e)),
}
}
else if let Some(width) = *TERM_WIDTH {
Ok(Self::Terminal(width))
}
else {
Ok(Self::Unset)
}
}
fn width(self) -> Option<usize> {
match self {
Self::Set(width) |
Self::Terminal(width) => Some(width),
Self::Unset => None,
Ok(Self::Automatic)
}
}
}
impl RowThreshold {
/// Determine whether to use a row threshold based on the given
/// environment variables.
fn deduce<V: Vars>(vars: &V) -> Result<Self, OptionsError> {
use crate::options::vars;
@ -357,20 +330,6 @@ impl TimeTypes {
}
// Gets, then caches, the width of the terminal that exa is running in.
// This gets used multiple times above, with no real guarantee of order,
// so 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::*;
@ -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(_)));
}
}

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
@ -106,17 +106,14 @@ pub struct Options {
/// Whether to show each files extended attributes.
pub xattr: bool,
/// Whether icons mode is enabled.
pub icons: bool,
}
pub struct Render<'a> {
pub dir: Option<&'a Dir>,
pub files: Vec<File<'a>>,
pub colours: &'a Colours,
pub style: &'a FileStyle,
pub theme: &'a Theme,
pub file_style: &'a FileStyle,
pub opts: &'a Options,
/// Whether to recurse through directories with a tree view, and if so,
@ -162,7 +159,7 @@ impl<'a> Render<'a> {
(None, _) => {/* Keep Git how it is */},
}
let mut table = Table::new(table, self.git, self.colours);
let mut table = Table::new(table, self.git, &self.theme);
if self.opts.header {
let header = table.header_row();
@ -256,18 +253,21 @@ impl<'a> Render<'a> {
}
let mut dir = None;
if let Some(r) = self.recurse {
if file.is_directory() && r.tree && ! r.is_too_deep(depth.0) {
match file.to_dir() {
Ok(d) => { dir = Some(d); },
Err(e) => { errors.push((e, None)) },
Ok(d) => {
dir = Some(d);
}
Err(e) => {
errors.push((e, None));
}
}
}
};
let icon = if self.opts.icons { Some(painted_icon(file, self.style)) }
else { None };
let icon = if self.file_style.icons { Some(painted_icon(file, self.theme)) }
else { None };
let egg = Egg { table_row, xattrs, errors, dir, file, icon };
unsafe { std::ptr::write(file_eggs.lock().unwrap()[idx].as_mut_ptr(), egg) }
@ -292,7 +292,7 @@ impl<'a> Render<'a> {
name_cell.push(ANSIGenericString::from(icon), 2)
}
let style = self.style.for_file(egg.file, self.colours)
let style = self.file_style.for_file(egg.file, self.theme)
.with_link_paths()
.paint()
.promote();
@ -355,7 +355,7 @@ impl<'a> Render<'a> {
Row {
tree: TreeParams::new(TreeDepth::root(), false),
cells: Some(header),
name: TextCell::paint_str(self.colours.header, "Name"),
name: TextCell::paint_str(self.theme.ui.header, "Name"),
}
}
@ -369,12 +369,12 @@ impl<'a> Render<'a> {
// TODO: broken_symlink() 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,32 @@ use crate::output::render::FiletypeColours;
/// Basically a file name factory.
#[derive(Debug)]
pub struct FileStyle {
#[derive(Debug, Copy, Clone)]
pub struct Options {
/// Whether to append file class characters to file names.
pub classify: Classify,
/// Mapping of file extensions to colours, to highlight regular files.
pub exts: Box<dyn FileColours>,
/// Whether to prepend icon characters before file names.
pub icons: bool,
}
impl FileStyle {
impl Options {
/// Create a new `FileName` that prints the given 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 +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 were 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 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 +98,9 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
self.link_style = LinkStyle::FullLinkPaths;
self
}
}
impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
/// Paints the name of the file using the colours, resulting in a vector
/// of coloured cells that can be printed to the terminal.
@ -146,13 +141,17 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
}
if ! target.name.is_empty() {
let target_options = Options {
classify: Classify::JustFilenames,
icons: false,
};
let target = FileName {
file: target,
colours: self.colours,
target: None,
link_style: LinkStyle::FullLinkPaths,
classify: Classify::JustFilenames,
exts: self.exts,
options: target_options,
};
for bit in target.coloured_file_name() {
@ -179,7 +178,7 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
}
}
}
else if let Classify::AddFileIndicators = self.classify {
else if let Classify::AddFileIndicators = self.options.classify {
if let Some(class) = self.classify_char() {
bits.push(Style::default().paint(class));
}
@ -188,7 +187,6 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
bits.into()
}
/// Adds the bits of the parent path to the given bits vector.
/// The path gets its characters escaped based on the colours.
fn add_parent_bits(&self, bits: &mut Vec<ANSIString<'_>>, parent: &Path) {
@ -208,7 +206,6 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
}
}
/// The character to be displayed after a file when classifying is on, if
/// the files 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 files
/// name using the given set of colours.
///
@ -257,7 +253,6 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
bits
}
/// Figures out which colour to paint the filename part of the output,
/// depending on which “type” of file it appears to be — either from the
/// class on the filesystem or from its name. (Or the broken link colour,
@ -271,13 +266,7 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
}
}
self.kind_style()
.or_else(|| self.exts.colour_file(self.file))
.unwrap_or_else(|| self.colours.normal())
}
fn kind_style(&self) -> Option<Style> {
Some(match self.file {
match self.file {
f if f.is_directory() => self.colours.directory(),
f if f.is_executable_file() => self.colours.executable_file(),
f if f.is_link() => self.colours.symlink(),
@ -286,8 +275,8 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
f if f.is_char_device() => self.colours.char_device(),
f if f.is_socket() => self.colours.socket(),
f if ! f.is_file() => self.colours.special(),
_ => return None,
})
_ => self.colours.colour_file(self.file),
}
}
}
@ -319,33 +308,6 @@ pub trait Colours: FiletypeColours {
/// The style to paint a file that has its executable bit set.
fn executable_file(&self) -> Style;
}
// needs Debug because FileStyle derives it
pub trait FileColours: Debug + Sync {
fn colour_file(&self, file: &File<'_>) -> Option<Style>;
}
#[derive(PartialEq, Debug)]
pub struct NoFileColours;
impl FileColours for NoFileColours {
fn colour_file(&self, _file: &File<'_>) -> Option<Style> {
None
}
}
// When getting the colour of a file from a *pair* of colourisers, try the
// first one then try the second one. This lets the user provide their own
// file type associations, while falling back to the default set if not set
// explicitly.
impl<A, B> FileColours for (A, B)
where A: FileColours,
B: FileColours,
{
fn colour_file(&self, file: &File<'_>) -> Option<Style> {
self.0.colour_file(file)
.or_else(|| self.1.colour_file(file))
}
fn colour_file(&self, file: &File<'_>) -> Style;
}

View File

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

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);
@ -145,15 +156,15 @@ impl<'a> Render<'a> {
let file_names = self.files.iter()
.map(|file| {
if self.details.icons {
if self.file_style.icons {
let mut icon_cell = TextCell::default();
icon_cell.push(ANSIGenericString::from(painted_icon(file, self.style)), 2);
let file_cell = self.style.for_file(file, self.colours).paint().promote();
icon_cell.push(ANSIGenericString::from(painted_icon(file, self.theme)), 2);
let file_cell = self.file_style.for_file(file, self.theme).paint().promote();
icon_cell.append(file_cell);
icon_cell
}
else {
self.style.for_file(file, self.colours).paint().promote()
self.file_style.for_file(file, self.theme).paint().promote()
}
})
.collect::<Vec<_>>();
@ -167,7 +178,7 @@ impl<'a> Render<'a> {
let the_grid_fits = {
let d = grid.fit_into_columns(column_count);
d.is_complete() && d.width() <= self.grid.console_width
d.is_complete() && d.width() <= self.console_width
};
if the_grid_fits {
@ -197,7 +208,7 @@ impl<'a> Render<'a> {
(None, _) => {/* Keep Git how it is */},
}
let mut table = Table::new(options, self.git, self.colours);
let mut table = Table::new(options, self.git, &self.theme);
let mut rows = Vec::new();
if self.details.header {

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

View File

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

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,
}
@ -35,5 +32,30 @@ pub enum Mode {
Grid(grid::Options),
Details(details::Options),
GridDetails(grid_details::Options),
Lines(lines::Options),
Lines,
}
/// The width of the terminal requested by the user.
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum TerminalWidth {
/// The user requested this specific number of columns.
Set(usize),
/// Look up the terminal size at runtime.
Automatic,
}
impl TerminalWidth {
pub fn actual_terminal_width(self) -> Option<usize> {
// All of stdin, stdout, and stderr could not be connected to a
// terminal, but 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

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

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,67 +86,89 @@ impl Styles {
( true, true) => Box::new((exts, FileExtensions)) as Box<_>,
};
let style = FileStyle { classify, exts };
Ok(Self { colours, style })
Theme { ui, exts }
}
}
/// Parse the environment variables into `LS_COLORS` pairs, putting file glob
/// colours into the `ExtensionMappings` that gets returned, and using the
/// two-character UI codes to modify the mutable `Colours`.
///
/// Also returns if the `EXA_COLORS` variable should reset the existing file
/// type mappings or not. The `reset` code needs to be the first one.
fn parse_color_vars<V: Vars>(vars: &V, colours: &mut Colours) -> (ExtensionMappings, bool) {
use log::*;
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();
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 ! colours.set_ls(&pair) {
match glob::Pattern::new(pair.key) {
Ok(pat) => {
exts.add(pat, pair.to_style());
}
Err(e) => {
warn!("Couldn't parse glob pattern {:?}: {}", pair.key, e);
if let Some(lsc) = &self.ls {
LSColors(lsc).each_pair(|pair| {
if ! colours.set_ls(&pair) {
match glob::Pattern::new(pair.key) {
Ok(pat) => {
exts.add(pat, pair.to_style());
}
Err(e) => {
warn!("Couldn't parse glob pattern {:?}: {}", pair.key, e);
}
}
}
}
});
}
let mut use_default_filetypes = true;
if let Some(exa) = vars.get(vars::EXA_COLORS) {
let exa = exa.to_string_lossy();
// Is this hacky? Yes.
if exa == "reset" || exa.starts_with("reset:") {
use_default_filetypes = false;
});
}
LSColors(exa.as_ref()).each_pair(|pair| {
if ! colours.set_ls(&pair) && ! colours.set_exa(&pair) {
match glob::Pattern::new(pair.key) {
Ok(pat) => {
exts.add(pat, pair.to_style());
}
Err(e) => {
warn!("Couldn't parse glob pattern {:?}: {}", pair.key, e);
}
}
};
});
}
let mut use_default_filetypes = true;
(exts, use_default_filetypes)
if let Some(exa) = &self.exa {
// Is this hacky? Yes.
if exa == "reset" || exa.starts_with("reset:") {
use_default_filetypes = false;
}
LSColors(exa).each_pair(|pair| {
if ! colours.set_ls(&pair) && ! colours.set_exa(&pair) {
match glob::Pattern::new(pair.key) {
Ok(pat) => {
exts.add(pat, pair.to_style());
}
Err(e) => {
warn!("Couldn't parse glob pattern {:?}: {}", pair.key, e);
}
}
};
});
}
(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;
}
}

79
xtests/icons.toml Normal file
View File

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

View File

@ -0,0 +1,26 @@
 #SAVEFILE#
 backup~
 compiled.class
 compiled.coffee
 compiled.js
 compiled.o
 compressed.deb
 compressed.tar.gz
 compressed.tar.xz
 compressed.tgz
 compressed.txz
 COMPRESSED.ZIP
 crypto.asc
 crypto.signature
 document.pdf
 DOCUMENT.XLSX
 file.tmp
 IMAGE.PNG
 image.svg
 lossless.flac
 lossless.wav
 Makefile
 music.mp3
 MUSIC.OGG
 VIDEO.AVI
 video.wmv

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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