Merge branch 'row-threshold'

This merge adds the EXA_GRID_ROWS environment variable, which disables the grid-details view if it doesn’t result in enough rows of output.

Fixes #138.
This commit is contained in:
Benjamin Sago 2017-08-13 11:16:11 +01:00
commit e933fa6a88
9 changed files with 159 additions and 39 deletions

6
Cargo.lock generated
View File

@ -14,7 +14,7 @@ dependencies = [
"num_cpus 1.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
"number_prefix 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
"scoped_threadpool 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
"term_grid 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"term_grid 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"term_size 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"users 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
@ -321,7 +321,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "term_grid"
version = "0.1.5"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
@ -438,7 +438,7 @@ dependencies = [
"checksum rand 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)" = "eb250fd207a4729c976794d03db689c9be1d634ab5a1c9da9492a13d8fecbcdf"
"checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda"
"checksum scoped_threadpool 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "3ef399c8893e8cb7aa9696e895427fab3a6bf265977bb96e126f24ddd2cda85a"
"checksum term_grid 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ccc202875496cf72a683a1ecd66f0742a830e73c202bdbd21867d73dfaac8343"
"checksum term_grid 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "b56a46b68f4aa347ba5512b1abc12dcb641ff0e9aa3cb49b007595a320e369c5"
"checksum term_size 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2b6b55df3198cc93372e85dd2ed817f0e38ce8cc0f22eb32391bfad9c4bf209"
"checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5"
"checksum unicode-normalization 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "51ccda9ef9efa3f7ef5d91e8f9b83bbe6955f9bf86aec89d5cce2c874625920f"

View File

@ -34,7 +34,7 @@ natord = "1.0.7"
num_cpus = "1.3.0"
number_prefix = "0.2.3"
scoped_threadpool = "0.1.*"
term_grid = "0.1.2"
term_grid = "0.1.6"
unicode-width = "0.1.4"
users = "0.5.2"
term_size = "0.3.0"

View File

@ -69,7 +69,7 @@ impl Vars for LiveVars {
impl<'args, 'w, W: Write + 'w> Exa<'args, 'w, W> {
pub fn new<I>(args: I, writer: &'w mut W) -> Result<Exa<'args, 'w, W>, Misfire>
where I: Iterator<Item=&'args OsString> {
Options::parse(args, LiveVars).map(move |(options, args)| {
Options::parse(args, &LiveVars).map(move |(options, args)| {
Exa { options, writer, args }
})
}
@ -181,10 +181,18 @@ impl<'args, 'w, W: Write + 'w> Exa<'args, 'w, W> {
let View { ref mode, ref colours, ref style } = self.options.view;
match *mode {
Mode::Lines => lines::Render { files, colours, style }.render(self.writer),
Mode::Grid(ref opts) => grid::Render { files, colours, style, opts }.render(self.writer),
Mode::Details(ref opts) => details::Render { dir, files, colours, style, opts, filter: &self.options.filter, recurse: self.options.dir_action.recurse_options() }.render(self.writer),
Mode::GridDetails(ref grid, ref details) => grid_details::Render { dir, files, colours, style, grid, details, filter: &self.options.filter }.render(self.writer),
Mode::Lines => {
lines::Render { files, colours, style }.render(self.writer)
}
Mode::Grid(ref opts) => {
grid::Render { files, colours, style, opts }.render(self.writer)
}
Mode::Details(ref opts) => {
details::Render { dir, files, colours, style, opts, filter: &self.options.filter, recurse: self.options.dir_action.recurse_options() }.render(self.writer)
}
Mode::GridDetails(ref opts) => {
grid_details::Render { dir, files, colours, style, grid: &opts.grid, details: &opts.details, filter: &self.options.filter, row_threshold: opts.row_threshold }.render(self.writer)
}
}
}
else {

View File

@ -130,21 +130,21 @@ mod test {
#[test]
fn help() {
let args = [ os("--help") ];
let opts = Options::parse(&args, None);
let opts = Options::parse(&args, &None);
assert!(opts.is_err())
}
#[test]
fn help_with_file() {
let args = [ os("--help"), os("me") ];
let opts = Options::parse(&args, None);
let opts = Options::parse(&args, &None);
assert!(opts.is_err())
}
#[test]
fn unhelpful() {
let args = [];
let opts = Options::parse(&args, None);
let opts = Options::parse(&args, &None);
assert!(opts.is_ok()) // no help when --help isnt passed
}
}

View File

@ -73,8 +73,7 @@ use std::ffi::{OsStr, OsString};
use fs::dir_action::DirAction;
use fs::filter::FileFilter;
use output::{View, Mode};
use output::details;
use output::{View, Mode, details, grid_details};
mod dir_action;
mod filter;
@ -116,7 +115,7 @@ impl Options {
/// struct and a list of free filenames, using the environment variables
/// for extra options.
#[allow(unused_results)]
pub fn parse<'args, I, V>(args: I, vars: V) -> Result<(Options, Vec<&'args OsStr>), Misfire>
pub fn parse<'args, I, V>(args: I, vars: &V) -> Result<(Options, Vec<&'args OsStr>), Misfire>
where I: IntoIterator<Item=&'args OsString>,
V: Vars {
use options::parser::{Matches, Strictness};
@ -145,14 +144,14 @@ impl Options {
pub fn should_scan_for_git(&self) -> bool {
match self.view.mode {
Mode::Details(details::Options { table: Some(ref table), .. }) |
Mode::GridDetails(_, details::Options { table: Some(ref table), .. }) => table.extra_columns.should_scan_for_git(),
Mode::GridDetails(grid_details::Options { details: details::Options { table: Some(ref table), .. }, .. }) => table.extra_columns.should_scan_for_git(),
_ => false,
}
}
/// 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<Options, Misfire> {
fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<Options, Misfire> {
let dir_action = DirAction::deduce(matches)?;
let filter = FileFilter::deduce(matches)?;
let view = View::deduce(matches, vars)?;
@ -231,28 +230,28 @@ pub mod test {
#[test]
fn files() {
let args = [ os("this file"), os("that file") ];
let outs = Options::parse(&args, None).unwrap().1;
let outs = Options::parse(&args, &None).unwrap().1;
assert_eq!(outs, vec![ &os("this file"), &os("that file") ])
}
#[test]
fn no_args() {
let nothing: Vec<OsString> = Vec::new();
let outs = Options::parse(&nothing, None).unwrap().1;
let outs = Options::parse(&nothing, &None).unwrap().1;
assert!(outs.is_empty()); // Listing the `.` directory is done in main.rs
}
#[test]
fn long_across() {
let args = [ os("--long"), os("--across") ];
let opts = Options::parse(&args, None);
let opts = Options::parse(&args, &None);
assert_eq!(opts.unwrap_err(), Misfire::Useless(&flags::ACROSS, true, &flags::LONG))
}
#[test]
fn oneline_across() {
let args = [ os("--oneline"), os("--across") ];
let opts = Options::parse(&args, None);
let opts = Options::parse(&args, &None);
assert_eq!(opts.unwrap_err(), Misfire::Useless(&flags::ACROSS, true, &flags::ONE_LINE))
}
}

View File

@ -54,7 +54,7 @@ mod test {
#[test]
fn help() {
let args = [ os("--version") ];
let opts = Options::parse(&args, None);
let opts = Options::parse(&args, &None);
assert!(opts.is_err())
}
}

View File

@ -1,5 +1,6 @@
use output::Colours;
use output::{View, Mode, grid, details};
use output::grid_details::{self, RowThreshold};
use output::table::{TimeTypes, Environment, SizeFormat, Columns, Options as TableOptions};
use output::file_name::{Classify, FileStyle};
use output::time::TimeFormat;
@ -13,7 +14,7 @@ use info::filetype::FileExtensions;
impl View {
/// Determine which view to use and all of that views arguments.
pub fn deduce<V: Vars>(matches: &MatchedFlags, vars: V) -> Result<View, Misfire> {
pub fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<View, Misfire> {
let mode = Mode::deduce(matches, vars)?;
let colours = Colours::deduce(matches)?;
let style = FileStyle::deduce(matches)?;
@ -25,7 +26,7 @@ impl View {
impl Mode {
/// Determine the mode from the command-line arguments.
pub fn deduce<V: Vars>(matches: &MatchedFlags, vars: V) -> Result<Mode, Misfire> {
pub fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<Mode, Misfire> {
use options::misfire::Misfire::*;
let long = || {
@ -95,10 +96,14 @@ impl Mode {
if matches.has(&flags::LONG)? {
let details = long()?;
if matches.has(&flags::GRID)? {
match other_options_scan()? {
Mode::Grid(grid) => return Ok(Mode::GridDetails(grid, details)),
others => return Ok(others),
};
let other_options_mode = other_options_scan()?;
if let Mode::Grid(grid) = other_options_mode {
let row_threshold = RowThreshold::deduce(vars)?;
return Ok(Mode::GridDetails(grid_details::Options { grid, details, row_threshold }));
}
else {
return Ok(other_options_mode);
}
}
else {
return Ok(Mode::Details(details));
@ -149,7 +154,7 @@ impl TerminalWidth {
/// Determine a requested terminal width from the command-line arguments.
///
/// Returns an error if a requested width doesnt parse to an integer.
fn deduce<V: Vars>(vars: V) -> Result<TerminalWidth, Misfire> {
fn deduce<V: Vars>(vars: &V) -> Result<TerminalWidth, Misfire> {
if let Some(columns) = vars.get("COLUMNS").and_then(|s| s.into_string().ok()) {
match columns.parse() {
Ok(width) => Ok(TerminalWidth::Set(width)),
@ -174,6 +179,24 @@ impl TerminalWidth {
}
impl RowThreshold {
/// Determine whether to use a row threshold based on the given
/// environment variables.
fn deduce<V: Vars>(vars: &V) -> Result<RowThreshold, Misfire> {
if let Some(columns) = vars.get("EXA_GRID_ROWS").and_then(|s| s.into_string().ok()) {
match columns.parse() {
Ok(rows) => Ok(RowThreshold::MinimumRows(rows)),
Err(e) => Err(Misfire::FailedParse(e)),
}
}
else {
Ok(RowThreshold::AlwaysGrid)
}
}
}
impl TableOptions {
fn deduce(matches: &MatchedFlags) -> Result<Self, Misfire> {
let env = Environment::load_all();
@ -478,7 +501,7 @@ mod test {
/// Like above, but with $vars.
#[test]
fn $name() {
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf, $vars)) {
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf, &$vars)) {
assert_eq!(result.unwrap_err(), $result);
}
}
@ -488,7 +511,7 @@ mod test {
/// Like further above, but with $vars.
#[test]
fn $name() {
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf, $vars)) {
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf, &$vars)) {
println!("Testing {:?}", result);
match result {
$pat => assert!(true),
@ -643,8 +666,8 @@ mod test {
test!(ell: Mode <- ["-l"], None; Both => like Ok(Mode::Details(_)));
// Grid-details views
test!(lid: Mode <- ["--long", "--grid"], None; Both => like Ok(Mode::GridDetails(_, _)));
test!(leg: Mode <- ["-lG"], None; Both => like Ok(Mode::GridDetails(_, _)));
test!(lid: Mode <- ["--long", "--grid"], None; Both => like Ok(Mode::GridDetails(_)));
test!(leg: Mode <- ["-lG"], None; Both => like Ok(Mode::GridDetails(_)));
// Options that do nothing without --long

View File

@ -1,3 +1,5 @@
//! The grid-details view lists several details views side-by-side.
use std::io::{Write, Result as IOResult};
use ansi_term::ANSIStrings;
@ -16,17 +18,72 @@ use output::table::{Table, Row as TableRow, Options as TableOptions};
use output::tree::{TreeParams, TreeDepth};
#[derive(Debug)]
pub struct Options {
pub grid: GridOptions,
pub details: DetailsOptions,
pub row_threshold: RowThreshold,
}
/// 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.
///
/// Doing this makes the resulting output look a bit better: when listing a
/// small directory of four files in four columns, the files just look spaced
/// out and its harder to see whats going on. So it can be enabled just for
/// larger directory listings.
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum RowThreshold {
/// Only use grid-details view if it would result in at least this many
/// rows of output.
MinimumRows(usize),
/// Use the grid-details view no matter what.
AlwaysGrid,
}
pub struct Render<'a> {
/// The directory thats being rendered here.
/// We need this to know which columns to put in the output.
pub dir: Option<&'a Dir>,
/// The files that have been read from the directory. They should all
/// hold a reference to it.
pub files: Vec<File<'a>>,
/// How to colour various pieces of text.
pub colours: &'a Colours,
/// How to format filenames.
pub style: &'a FileStyle,
/// The grid part of the grid-details view.
pub grid: &'a GridOptions,
/// The details part of the grid-details view.
pub details: &'a DetailsOptions,
/// How to filter files after listing a directory. The files in this
/// render will already have been filtered and sorted, but any directories
/// that we recurse into will have to have this applied.
pub filter: &'a FileFilter,
/// The minimum number of rows that there need to be before grid-details
/// mode is activated.
pub row_threshold: RowThreshold,
}
impl<'a> Render<'a> {
/// Create a temporary Details render that gets used for the columns of
/// the grid-details render that's being generated.
///
/// 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> {
DetailsRender {
dir: self.dir.clone(),
@ -39,11 +96,33 @@ impl<'a> Render<'a> {
}
}
pub fn render<W: Write>(&self, w: &mut W) -> IOResult<()> {
/// 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.
pub fn give_up(self) -> DetailsRender<'a> {
DetailsRender {
dir: self.dir,
files: self.files,
colours: self.colours,
style: self.style,
opts: self.details,
recurse: None,
filter: &self.filter,
}
}
pub fn render<W: Write>(self, w: &mut W) -> IOResult<()> {
if let Some((grid, width)) = self.find_fitting_grid() {
write!(w, "{}", grid.fit_into_columns(width))
}
else {
self.give_up().render(w)
}
}
pub fn find_fitting_grid(&self) -> Option<(grid::Grid, grid::Width)> {
let options = self.details.table.as_ref().expect("Details table options not given!");
let drender = self.clone().details();
let drender = self.details();
let (first_table, _) = self.make_table(options, &drender);
@ -57,7 +136,9 @@ impl<'a> Render<'a> {
let mut last_working_table = self.make_grid(1, options, &file_names, rows.clone(), &drender);
for column_count in 2.. {
// If we cant fit everything in a grid 100 columns wide, then
// something has gone seriously awry
for column_count in 2..100 {
let grid = self.make_grid(column_count, options, &file_names, rows.clone(), &drender);
let the_grid_fits = {
@ -69,11 +150,20 @@ impl<'a> Render<'a> {
last_working_table = grid;
}
else {
return write!(w, "{}", last_working_table.fit_into_columns(column_count - 1));
// If weve figured out how many columns can fit in the users
// terminal, and it turns out there arent enough rows to
// make it worthwhile, then just resort to the lines view.
if let RowThreshold::MinimumRows(thresh) = self.row_threshold {
if last_working_table.fit_into_columns(column_count - 1).row_count() < thresh {
return None;
}
}
return Some((last_working_table, column_count - 1));
}
}
Ok(())
None
}
fn make_table<'t>(&'a self, options: &'a TableOptions, drender: &DetailsRender) -> (Table<'a>, Vec<DetailsRow>) {

View File

@ -33,6 +33,6 @@ pub struct View {
pub enum Mode {
Grid(grid::Options),
Details(details::Options),
GridDetails(grid::Options, details::Options),
GridDetails(grid_details::Options),
Lines,
}