diff options
author | Ben S <ogham@bsago.me> | 2015-06-28 13:21:21 +0100 |
---|---|---|
committer | Ben S <ogham@bsago.me> | 2015-06-28 13:21:21 +0100 |
commit | ccdf9ff4a679e00298f7e0fd9bd68318ded20cb8 (patch) | |
tree | 48c7fe31268fa44046e731af13a4d6a120309ace | |
parent | 766279b803934522af6570c8e465b0d3959ac903 (diff) |
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.
-rw-r--r-- | src/column.rs | 40 | ||||
-rw-r--r-- | src/main.rs | 7 | ||||
-rw-r--r-- | src/options.rs | 129 | ||||
-rw-r--r-- | src/output/details.rs | 61 | ||||
-rw-r--r-- | src/output/grid_details.rs | 60 | ||||
-rw-r--r-- | src/output/lines.rs | 2 | ||||
-rw-r--r-- | src/output/mod.rs | 7 |
7 files changed, 195 insertions, 111 deletions
diff --git a/src/column.rs b/src/column.rs index 23e2a4b..6f4a1e7 100644 --- a/src/column.rs +++ b/src/column.rs @@ -1,5 +1,3 @@ -use std::iter::repeat; - use ansi_term::Style; 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)] pub struct Cell { @@ -85,10 +64,29 @@ pub struct Cell { } impl Cell { + pub fn empty() -> Cell { + Cell { + text: String::new(), + length: 0, + } + } + pub fn paint(style: Style, string: &str) -> Cell { Cell { text: style.paint(string).to_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); + } } diff --git a/src/main.rs b/src/main.rs index 28042f8..22af23c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -179,9 +179,10 @@ impl<'dir> Exa<'dir> { fn print(&self, dir: Option<&Dir>, files: &[File]) { 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), } } } diff --git a/src/options.rs b/src/options.rs index f0a49e2..092b6ab 100644 --- a/src/options.rs +++ b/src/options.rs @@ -13,7 +13,7 @@ use column::Column::*; use dir::Dir; use feature::Attribute; use file::File; -use output::{Grid, Details, Lines}; +use output::{Grid, Details, GridDetails, Lines}; use term::dimensions; @@ -37,6 +37,7 @@ impl Options { opts.optflag("B", "bytes", "list file sizes in bytes, without prefixes"); opts.optflag("d", "list-dirs", "list directories as regular files"); 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("h", "header", "show a header row at the top"); opts.optflag("H", "links", "show number of hard links"); @@ -248,15 +249,16 @@ impl fmt::Display for Misfire { #[derive(PartialEq, Debug, Copy, Clone)] pub enum View { Details(Details), - Lines(Lines), Grid(Grid), + GridDetails(GridDetails), + Lines(Lines), } impl View { pub fn deduce(matches: &getopts::Matches, filter: FileFilter, dir_action: DirAction) -> Result<View, Misfire> { use self::Misfire::*; - if matches.opt_present("long") { + let long = || { if matches.opt_present("across") { Err(Useless("across", true, "long")) } @@ -272,78 +274,79 @@ impl View { 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 { - 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 { - 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() } } diff --git a/src/output/details.rs b/src/output/details.rs index 84da4de..e67490d 100644 --- a/src/output/details.rs +++ b/src/output/details.rs @@ -1,3 +1,6 @@ +use std::iter::repeat; +use std::string::ToString; + use colours::Colours; use column::{Alignment, Column, Cell}; use dir::Dir; @@ -66,14 +69,16 @@ impl Details { // Then add files to the table and print it out. 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 /// is present. fn add_files_to_table<U: Users>(&self, table: &mut Table<U>, src: &[File], depth: usize) { 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 // 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 /// from the other cells, as it never requires padding. - name: String, + name: Cell, /// How many directories deep into the tree structure this is. Directories /// on top have depth 0. @@ -157,7 +162,7 @@ impl Table<OSUsers> { /// Create a new, empty Table object, setting the caching fields to their /// empty states. - fn with_options(colours: Colours, columns: Vec<Column>) -> Table<OSUsers> { + pub fn with_options(colours: Colours, columns: Vec<Column>) -> Table<OSUsers> { Table { columns: columns, 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 /// the columns, underlined. This has dummy data for the cases that aren't /// actually used, such as the depth or list of attributes. - fn add_header(&mut self) { + pub fn add_header(&mut self) { let row = Row { depth: 0, 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, attrs: Vec::new(), 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. - 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 { depth: depth, 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, attrs: file.xattrs.clone(), 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 /// 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() .map(|c| self.display(file, c)) .collect() @@ -373,8 +378,9 @@ impl<U> Table<U> where U: Users { } /// 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 cells = Vec::new(); // 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 @@ -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)) .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() { - 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 // necessary to maintain information about the previously-printed // 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 }; 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 { @@ -408,24 +424,29 @@ impl<U> Table<U> where U: Users { // If any tree characters have been printed, then add an extra // space, which makes the output look much better. if row.depth != 0 { - print!(" "); + filename.push(' '); + filename_length += 1; } } // Print the name without worrying about padding. - print!("{}\n", row.name); + filename.push_str(&*row.name.text); + filename_length += row.name.length; if xattr { let width = row.attrs.iter().map(|a| a.name().len()).max().unwrap_or(0); for attr in row.attrs.iter() { 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 } } diff --git a/src/output/grid_details.rs b/src/output/grid_details.rs new file mode 100644 index 0000000..93e498a --- /dev/null +++ b/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, + } + } +} diff --git a/src/output/lines.rs b/src/output/lines.rs index 1d6dade..7f19105 100644 --- a/src/output/lines.rs +++ b/src/output/lines.rs @@ -13,7 +13,7 @@ pub struct Lines { impl Lines { pub fn view(&self, files: &[File]) { for file in files { - println!("{}", filename(file, &self.colours)); + println!("{}", filename(file, &self.colours, true)); } } } diff --git a/src/output/mod.rs b/src/output/mod.rs index ea48507..b4d9bba 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -7,14 +7,15 @@ use filetype::file_colour; pub use self::details::Details; pub use self::grid::Grid; pub use self::lines::Lines; +pub use self::grid_details::GridDetails; mod grid; pub mod details; 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) } else { |