Browse Source

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.
Benjamin Sago 8 years ago
parent
commit
e933fa6a88
9 changed files with 159 additions and 39 deletions
  1. 3 3
      Cargo.lock
  2. 1 1
      Cargo.toml
  3. 13 5
      src/exa.rs
  4. 3 3
      src/options/help.rs
  5. 8 9
      src/options/mod.rs
  6. 1 1
      src/options/version.rs
  7. 34 11
      src/options/view.rs
  8. 95 5
      src/output/grid_details.rs
  9. 1 1
      src/output/mod.rs

+ 3 - 3
Cargo.lock

@@ -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"

+ 1 - 1
Cargo.toml

@@ -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"

+ 13 - 5
src/exa.rs

@@ -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 {

+ 3 - 3
src/options/help.rs

@@ -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 isn’t passed
     }
 }

+ 8 - 9
src/options/mod.rs

@@ -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 they’ve 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))
     }
 }

+ 1 - 1
src/options/version.rs

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

+ 34 - 11
src/options/view.rs

@@ -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 view’s 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 doesn’t 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

+ 95 - 5
src/output/grid_details.rs

@@ -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 wouldn’t 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 it’s harder to see what’s 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 that’s 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 column’s 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 doesn’t 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 can’t 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 we’ve figured out how many columns can fit in the user’s
+                // terminal, and it turns out there aren’t 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>) {

+ 1 - 1
src/output/mod.rs

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