Просмотр исходного кода

Add --grid --long option

This commit adds --grid, which, when used with --long, will split the details into multiple columns. Currently this is just 2 columns, but in the future it will be based on the width of the terminal.

In order to do this, I had to do two things:

1. Add a `links` parameter to the filename function, which disables the printing of the arrow and link target in the details view. When this is active, the columns get way too large, and it becomes not worth it.
2. Change the `print_table` function from actually printing the table to stdout to returning a list of `Cells` based on the table. This list then gets its width measured to calculate the width of the resulting table.
Ben S 10 лет назад
Родитель
Сommit
ccdf9ff4a6
7 измененных файлов с 195 добавлено и 111 удалено
  1. 19 21
      src/column.rs
  2. 4 3
      src/main.rs
  3. 66 63
      src/options.rs
  4. 41 20
      src/output/details.rs
  5. 60 0
      src/output/grid_details.rs
  6. 1 1
      src/output/lines.rs
  7. 4 3
      src/output/mod.rs

+ 19 - 21
src/column.rs

@@ -1,5 +1,3 @@
-use std::iter::repeat;
-
 use ansi_term::Style;
 use ansi_term::Style;
 use unicode_width::UnicodeWidthStr;
 use unicode_width::UnicodeWidthStr;
 
 
@@ -58,25 +56,6 @@ impl Column {
     }
     }
 }
 }
 
 
-/// Pad a string with the given number of spaces.
-fn spaces(length: usize) -> String {
-    repeat(" ").take(length).collect()
-}
-
-impl Alignment {
-    /// Pad a string with the given alignment and number of spaces.
-    ///
-    /// This doesn't take the width the string *should* be, rather the number
-    /// of spaces to add: this is because the strings are usually full of
-    /// invisible control characters, so getting the displayed width of the
-    /// string is not as simple as just getting its length.
-    pub fn pad_string(&self, string: &str, padding: usize) -> String {
-        match *self {
-            Alignment::Left  => format!("{}{}", string, spaces(padding)),
-            Alignment::Right => format!("{}{}", spaces(padding), string),
-        }
-    }
-}
 
 
 #[derive(PartialEq, Debug)]
 #[derive(PartialEq, Debug)]
 pub struct Cell {
 pub struct Cell {
@@ -85,10 +64,29 @@ pub struct Cell {
 }
 }
 
 
 impl Cell {
 impl Cell {
+    pub fn empty() -> Cell {
+        Cell {
+            text: String::new(),
+            length: 0,
+        }
+    }
+
     pub fn paint(style: Style, string: &str) -> Cell {
     pub fn paint(style: Style, string: &str) -> Cell {
         Cell {
         Cell {
             text: style.paint(string).to_string(),
             text: style.paint(string).to_string(),
             length: UnicodeWidthStr::width(string),
             length: UnicodeWidthStr::width(string),
         }
         }
     }
     }
+
+    pub fn add_spaces(&mut self, count: usize) {
+        self.length += count;
+        for _ in 0 .. count {
+            self.text.push(' ');
+        }
+    }
+
+    pub fn append(&mut self, other: &Cell) {
+        self.length += other.length;
+        self.text.push_str(&*other.text);
+    }
 }
 }

+ 4 - 3
src/main.rs

@@ -179,9 +179,10 @@ impl<'dir> Exa<'dir> {
 
 
     fn print(&self, dir: Option<&Dir>, files: &[File]) {
     fn print(&self, dir: Option<&Dir>, files: &[File]) {
         match self.options.view {
         match self.options.view {
-            View::Grid(g)     => g.view(files),
-            View::Details(d)  => d.view(dir, files),
-            View::Lines(l)    => l.view(files),
+            View::Grid(g)         => g.view(files),
+            View::Details(d)      => d.view(dir, files),
+            View::GridDetails(gd) => gd.view(dir, files),
+            View::Lines(l)        => l.view(files),
         }
         }
     }
     }
 }
 }

+ 66 - 63
src/options.rs

@@ -13,7 +13,7 @@ use column::Column::*;
 use dir::Dir;
 use dir::Dir;
 use feature::Attribute;
 use feature::Attribute;
 use file::File;
 use file::File;
-use output::{Grid, Details, Lines};
+use output::{Grid, Details, GridDetails, Lines};
 use term::dimensions;
 use term::dimensions;
 
 
 
 
@@ -37,6 +37,7 @@ impl Options {
         opts.optflag("B", "bytes",     "list file sizes in bytes, without prefixes");
         opts.optflag("B", "bytes",     "list file sizes in bytes, without prefixes");
         opts.optflag("d", "list-dirs", "list directories as regular files");
         opts.optflag("d", "list-dirs", "list directories as regular files");
         opts.optflag("g", "group",     "show group as well as user");
         opts.optflag("g", "group",     "show group as well as user");
+        opts.optflag("G", "grid",      "display entries in a grid view (default)");
         opts.optflag("",  "group-directories-first", "list directories before other files");
         opts.optflag("",  "group-directories-first", "list directories before other files");
         opts.optflag("h", "header",    "show a header row at the top");
         opts.optflag("h", "header",    "show a header row at the top");
         opts.optflag("H", "links",     "show number of hard links");
         opts.optflag("H", "links",     "show number of hard links");
@@ -248,15 +249,16 @@ impl fmt::Display for Misfire {
 #[derive(PartialEq, Debug, Copy, Clone)]
 #[derive(PartialEq, Debug, Copy, Clone)]
 pub enum View {
 pub enum View {
     Details(Details),
     Details(Details),
-    Lines(Lines),
     Grid(Grid),
     Grid(Grid),
+    GridDetails(GridDetails),
+    Lines(Lines),
 }
 }
 
 
 impl View {
 impl View {
     pub fn deduce(matches: &getopts::Matches, filter: FileFilter, dir_action: DirAction) -> Result<View, Misfire> {
     pub fn deduce(matches: &getopts::Matches, filter: FileFilter, dir_action: DirAction) -> Result<View, Misfire> {
         use self::Misfire::*;
         use self::Misfire::*;
 
 
-        if matches.opt_present("long") {
+        let long = || {
             if matches.opt_present("across") {
             if matches.opt_present("across") {
                 Err(Useless("across", true, "long"))
                 Err(Useless("across", true, "long"))
             }
             }
@@ -272,78 +274,79 @@ impl View {
                         colours: if dimensions().is_some() { Colours::colourful() } else { Colours::plain() },
                         colours: if dimensions().is_some() { Colours::colourful() } else { Colours::plain() },
                 };
                 };
 
 
-                Ok(View::Details(details))
+                Ok(details)
             }
             }
-        }
-        else if matches.opt_present("binary") {
-            Err(Useless("binary", false, "long"))
-        }
-        else if matches.opt_present("bytes") {
-            Err(Useless("bytes", false, "long"))
-        }
-        else if matches.opt_present("inode") {
-            Err(Useless("inode", false, "long"))
-        }
-        else if matches.opt_present("links") {
-            Err(Useless("links", false, "long"))
-        }
-        else if matches.opt_present("header") {
-            Err(Useless("header", false, "long"))
-        }
-        else if matches.opt_present("blocks") {
-            Err(Useless("blocks", false, "long"))
-        }
-        else if cfg!(feature="git") && matches.opt_present("git") {
-            Err(Useless("git", false, "long"))
-        }
-        else if matches.opt_present("time") {
-            Err(Useless("time", false, "long"))
-        }
-        else if matches.opt_present("tree") {
-            Err(Useless("tree", false, "long"))
-        }
-        else if matches.opt_present("group") {
-            Err(Useless("group", false, "long"))
-        }
-        else if matches.opt_present("level") && !matches.opt_present("recurse") {
-            Err(Useless2("level", "recurse", "tree"))
-        }
-        else if Attribute::feature_implemented() && matches.opt_present("extended") {
-            Err(Useless("extended", false, "long"))
-        }
-        else if let Some((width, _)) = dimensions() {
-            if matches.opt_present("oneline") {
-                if matches.opt_present("across") {
-                    Err(Useless("across", true, "oneline"))
+        };
+
+        let long_options_scan = || {
+            for option in &[ "binary", "bytes", "inode", "links", "header", "blocks", "time", "tree", "group" ] {
+                if matches.opt_present(option) {
+                    return Err(Useless(option, false, "long"));
+                }
+            }
+
+            if cfg!(feature="git") && matches.opt_present("git") {
+                Err(Useless("git", false, "long"))
+            }
+            else {
+                Ok(())
+            }
+        };
+
+        let other_options_scan = || {
+            if let Some((width, _)) = dimensions() {
+                if matches.opt_present("oneline") {
+                    if matches.opt_present("across") {
+                        Err(Useless("across", true, "oneline"))
+                    }
+                    else {
+                        let lines = Lines {
+                             colours: Colours::colourful(),
+                        };
+
+                        Ok(View::Lines(lines))
+                    }
                 }
                 }
                 else {
                 else {
-                    let lines = Lines {
-                         colours: Colours::colourful(),
+                    let grid = Grid {
+                        across: matches.opt_present("across"),
+                        console_width: width,
+                        colours: Colours::colourful(),
                     };
                     };
 
 
-                    Ok(View::Lines(lines))
+                    Ok(View::Grid(grid))
                 }
                 }
             }
             }
             else {
             else {
-                let grid = Grid {
-                    across: matches.opt_present("across"),
-                    console_width: width,
-                    colours: Colours::colourful(),
+                // If the terminal width couldn't be matched for some reason, such
+                // as the program's stdout being connected to a file, then
+                // fallback to the lines view.
+                let lines = Lines {
+                     colours: Colours::plain(),
                 };
                 };
 
 
-                Ok(View::Grid(grid))
+                Ok(View::Lines(lines))
+            }
+        };
+
+        if matches.opt_present("long") {
+            let long_options = try!(long());
+
+            if matches.opt_present("grid") {
+                match other_options_scan() {
+                    Ok(View::Grid(grid)) => return Ok(View::GridDetails(GridDetails { grid: grid, details: long_options })),
+                    Ok(lines)            => return Ok(lines),
+                    Err(e)               => return Err(e),
+                };
+            }
+            else {
+                return Ok(View::Details(long_options));
             }
             }
         }
         }
-        else {
-            // If the terminal width couldn't be matched for some reason, such
-            // as the program's stdout being connected to a file, then
-            // fallback to the lines view.
-            let lines = Lines {
-                 colours: Colours::plain(),
-            };
-
-            Ok(View::Lines(lines))
-        }
+
+        try!(long_options_scan());
+
+        other_options_scan()
     }
     }
 }
 }
 
 

+ 41 - 20
src/output/details.rs

@@ -1,3 +1,6 @@
+use std::iter::repeat;
+use std::string::ToString;
+
 use colours::Colours;
 use colours::Colours;
 use column::{Alignment, Column, Cell};
 use column::{Alignment, Column, Cell};
 use dir::Dir;
 use dir::Dir;
@@ -66,14 +69,16 @@ impl Details {
 
 
         // Then add files to the table and print it out.
         // Then add files to the table and print it out.
         self.add_files_to_table(&mut table, files, 0);
         self.add_files_to_table(&mut table, files, 0);
-        table.print_table(self.xattr, self.recurse.is_some());
+        for cell in table.print_table(self.xattr, self.recurse.is_some()) {
+            println!("{}", cell.text);
+        }
     }
     }
 
 
     /// Adds files to the table - recursively, if the `recurse` option
     /// Adds files to the table - recursively, if the `recurse` option
     /// is present.
     /// is present.
     fn add_files_to_table<U: Users>(&self, table: &mut Table<U>, src: &[File], depth: usize) {
     fn add_files_to_table<U: Users>(&self, table: &mut Table<U>, src: &[File], depth: usize) {
         for (index, file) in src.iter().enumerate() {
         for (index, file) in src.iter().enumerate() {
-            table.add_file(file, depth, index == src.len() - 1);
+            table.add_file(file, depth, index == src.len() - 1, true);
 
 
             // There are two types of recursion that exa supports: a tree
             // There are two types of recursion that exa supports: a tree
             // view, which is dealt with here, and multiple listings, which is
             // view, which is dealt with here, and multiple listings, which is
@@ -105,7 +110,7 @@ struct Row {
 
 
     /// This file's name, in coloured output. The name is treated separately
     /// This file's name, in coloured output. The name is treated separately
     /// from the other cells, as it never requires padding.
     /// from the other cells, as it never requires padding.
-    name:     String,
+    name:     Cell,
 
 
     /// How many directories deep into the tree structure this is. Directories
     /// How many directories deep into the tree structure this is. Directories
     /// on top have depth 0.
     /// on top have depth 0.
@@ -157,7 +162,7 @@ impl Table<OSUsers> {
 
 
     /// Create a new, empty Table object, setting the caching fields to their
     /// Create a new, empty Table object, setting the caching fields to their
     /// empty states.
     /// empty states.
-    fn with_options(colours: Colours, columns: Vec<Column>) -> Table<OSUsers> {
+    pub fn with_options(colours: Colours, columns: Vec<Column>) -> Table<OSUsers> {
         Table {
         Table {
             columns: columns,
             columns: columns,
             rows:    Vec::new(),
             rows:    Vec::new(),
@@ -177,11 +182,11 @@ impl<U> Table<U> where U: Users {
     /// Add a dummy "header" row to the table, which contains the names of all
     /// Add a dummy "header" row to the table, which contains the names of all
     /// the columns, underlined. This has dummy data for the cases that aren't
     /// the columns, underlined. This has dummy data for the cases that aren't
     /// actually used, such as the depth or list of attributes.
     /// actually used, such as the depth or list of attributes.
-    fn add_header(&mut self) {
+    pub fn add_header(&mut self) {
         let row = Row {
         let row = Row {
             depth:    0,
             depth:    0,
             cells:    self.columns.iter().map(|c| Cell::paint(self.colours.header, c.header())).collect(),
             cells:    self.columns.iter().map(|c| Cell::paint(self.colours.header, c.header())).collect(),
-            name:     self.colours.header.paint("Name").to_string(),
+            name:     Cell::paint(self.colours.header, "Name"),
             last:     false,
             last:     false,
             attrs:    Vec::new(),
             attrs:    Vec::new(),
             children: false,
             children: false,
@@ -191,11 +196,11 @@ impl<U> Table<U> where U: Users {
     }
     }
 
 
     /// Get the cells for the given file, and add the result to the table.
     /// Get the cells for the given file, and add the result to the table.
-    fn add_file(&mut self, file: &File, depth: usize, last: bool) {
+    pub fn add_file(&mut self, file: &File, depth: usize, last: bool, links: bool) {
         let row = Row {
         let row = Row {
             depth:    depth,
             depth:    depth,
             cells:    self.cells_for_file(file),
             cells:    self.cells_for_file(file),
-            name:     filename(file, &self.colours),
+            name:     Cell { text: filename(file, &self.colours, links), length: file.file_name_width() },
             last:     last,
             last:     last,
             attrs:    file.xattrs.clone(),
             attrs:    file.xattrs.clone(),
             children: file.this.is_some(),
             children: file.this.is_some(),
@@ -206,7 +211,7 @@ impl<U> Table<U> where U: Users {
 
 
     /// Use the list of columns to find which cells should be produced for
     /// Use the list of columns to find which cells should be produced for
     /// this file, per-column.
     /// this file, per-column.
-    fn cells_for_file(&mut self, file: &File) -> Vec<Cell> {
+    pub fn cells_for_file(&mut self, file: &File) -> Vec<Cell> {
         self.columns.clone().iter()
         self.columns.clone().iter()
                     .map(|c| self.display(file, c))
                     .map(|c| self.display(file, c))
                     .collect()
                     .collect()
@@ -373,8 +378,9 @@ impl<U> Table<U> where U: Users {
     }
     }
 
 
     /// Print the table to standard output, consuming it in the process.
     /// Print the table to standard output, consuming it in the process.
-    fn print_table(self, xattr: bool, show_children: bool) {
+    pub fn print_table(&self, xattr: bool, show_children: bool) -> Vec<Cell> {
         let mut stack = Vec::new();
         let mut stack = Vec::new();
+        let mut cells = Vec::new();
 
 
         // Work out the list of column widths by finding the longest cell for
         // Work out the list of column widths by finding the longest cell for
         // each column, then formatting each cell in that column to be the
         // each column, then formatting each cell in that column to be the
@@ -383,12 +389,21 @@ impl<U> Table<U> where U: Users {
             .map(|n| self.rows.iter().map(|row| row.cells[n].length).max().unwrap_or(0))
             .map(|n| self.rows.iter().map(|row| row.cells[n].length).max().unwrap_or(0))
             .collect();
             .collect();
 
 
-        for row in self.rows.into_iter() {
+        for row in self.rows.iter() {
+            let mut cell = Cell::empty();
+
             for (n, width) in column_widths.iter().enumerate() {
             for (n, width) in column_widths.iter().enumerate() {
-                let padding = width - row.cells[n].length;
-                print!("{} ", self.columns[n].alignment().pad_string(&row.cells[n].text, padding));
+                match self.columns[n].alignment() {
+                    Alignment::Left  => { cell.append(&row.cells[n]); cell.add_spaces(width - row.cells[n].length); }
+                    Alignment::Right => { cell.add_spaces(width - row.cells[n].length); cell.append(&row.cells[n]); }
+                }
+
+                cell.add_spaces(1);
             }
             }
 
 
+            let mut filename = String::new();
+            let mut filename_length = 0;
+
             // A stack tracks which tree characters should be printed. It's
             // A stack tracks which tree characters should be printed. It's
             // necessary to maintain information about the previously-printed
             // necessary to maintain information about the previously-printed
             // lines, as the output will change based on whether the
             // lines, as the output will change based on whether the
@@ -398,7 +413,8 @@ impl<U> Table<U> where U: Users {
                 stack[row.depth] = if row.last { TreePart::Corner } else { TreePart::Edge };
                 stack[row.depth] = if row.last { TreePart::Corner } else { TreePart::Edge };
 
 
                 for i in 1 .. row.depth + 1 {
                 for i in 1 .. row.depth + 1 {
-                    print!("{}", self.colours.punctuation.paint(stack[i].ascii_art()));
+                    filename.push_str(&*self.colours.punctuation.paint(stack[i].ascii_art()).to_string());
+                    filename_length += 4;
                 }
                 }
 
 
                 if row.children {
                 if row.children {
@@ -408,24 +424,29 @@ impl<U> Table<U> where U: Users {
                 // If any tree characters have been printed, then add an extra
                 // If any tree characters have been printed, then add an extra
                 // space, which makes the output look much better.
                 // space, which makes the output look much better.
                 if row.depth != 0 {
                 if row.depth != 0 {
-                    print!(" ");
+                    filename.push(' ');
+                    filename_length += 1;
                 }
                 }
             }
             }
 
 
             // Print the name without worrying about padding.
             // Print the name without worrying about padding.
-            print!("{}\n", row.name);
+            filename.push_str(&*row.name.text);
+            filename_length += row.name.length;
 
 
             if xattr {
             if xattr {
                 let width = row.attrs.iter().map(|a| a.name().len()).max().unwrap_or(0);
                 let width = row.attrs.iter().map(|a| a.name().len()).max().unwrap_or(0);
                 for attr in row.attrs.iter() {
                 for attr in row.attrs.iter() {
                     let name = attr.name();
                     let name = attr.name();
-                    println!("{}\t{}",
-                        Alignment::Left.pad_string(name, width - name.len()),
-                        attr.size()
-                    )
+                    let spaces: String = repeat(" ").take(width - name.len()).collect();
+                    filename.push_str(&*format!("\n{}{}  {}", name, spaces, attr.size()))
                 }
                 }
             }
             }
+
+            cell.append(&Cell { text: filename, length: filename_length });
+            cells.push(cell);
         }
         }
+
+        cells
     }
     }
 }
 }
 
 

+ 60 - 0
src/output/grid_details.rs

@@ -0,0 +1,60 @@
+use std::convert;
+use std::iter::repeat;
+
+use term_grid as grid;
+
+use column::Cell;
+use dir::Dir;
+use file::File;
+use output::details::{Details, Table};
+use output::grid::Grid;
+
+#[derive(PartialEq, Debug, Copy, Clone)]
+pub struct GridDetails {
+    pub grid: Grid,
+    pub details: Details,
+}
+
+impl GridDetails {
+    pub fn view(&self, dir: Option<&Dir>, files: &[File]) {
+
+        let columns = 2;
+
+        let make_table = || {
+            let mut table = Table::with_options(self.details.colours, self.details.columns.for_dir(dir));
+            if self.details.header { table.add_header() }
+            table
+        };
+
+        let mut tables: Vec<_> = repeat(()).map(|_| make_table()).take(columns).collect();
+
+        for (i, file) in files.iter().enumerate() {
+            tables[i % columns].add_file(file, 0, false, false);
+        }
+
+        let direction = if self.grid.across { grid::Direction::LeftToRight }
+                                       else { grid::Direction::TopToBottom };
+
+        let mut grid = grid::Grid::new(grid::GridOptions {
+            direction:        direction,
+            separator_width:  2,
+        });
+
+        for table in tables {
+            for cell in table.print_table(false, false).into_iter() {
+                grid.add(cell.into());
+            }
+        }
+
+        print!("{}", grid.fit_into_columns(columns));
+    }
+}
+
+impl convert::From<Cell> for grid::Cell {
+    fn from(input: Cell) -> Self {
+        grid::Cell {
+            contents: input.text,
+            width:    input.length,
+        }
+    }
+}

+ 1 - 1
src/output/lines.rs

@@ -13,7 +13,7 @@ pub struct Lines {
 impl Lines {
 impl Lines {
     pub fn view(&self, files: &[File]) {
     pub fn view(&self, files: &[File]) {
         for file in files {
         for file in files {
-            println!("{}", filename(file, &self.colours));
+            println!("{}", filename(file, &self.colours, true));
         }
         }
     }
     }
 }
 }

+ 4 - 3
src/output/mod.rs

@@ -7,14 +7,15 @@ use filetype::file_colour;
 pub use self::details::Details;
 pub use self::details::Details;
 pub use self::grid::Grid;
 pub use self::grid::Grid;
 pub use self::lines::Lines;
 pub use self::lines::Lines;
+pub use self::grid_details::GridDetails;
 
 
 mod grid;
 mod grid;
 pub mod details;
 pub mod details;
 mod lines;
 mod lines;
+mod grid_details;
 
 
-
-pub fn filename(file: &File, colours: &Colours) -> String {
-    if file.is_link() {
+pub fn filename(file: &File, colours: &Colours, links: bool) -> String {
+    if links && file.is_link() {
         symlink_filename(file, colours)
         symlink_filename(file, colours)
     }
     }
     else {
     else {