diff options
author | Ben S <ogham@bsago.me> | 2015-06-29 14:50:47 +0100 |
---|---|---|
committer | Ben S <ogham@bsago.me> | 2015-06-29 14:50:47 +0100 |
commit | 090cebe669ecb1875a0066b207e841caeb4f039d (patch) | |
tree | 91ff30f676f11668584a787fe897dacfddddf3b7 | |
parent | 8d6f62840a5cf404313c6dff06a213a4747e913e (diff) | |
parent | 56895ab84fff784b2e8c550fd47ea1dd3698814b (diff) |
Merge branch 'long-grid-view'
-rw-r--r-- | Cargo.lock | 14 | ||||
-rw-r--r-- | src/column.rs | 42 | ||||
-rw-r--r-- | src/main.rs | 7 | ||||
-rw-r--r-- | src/options.rs | 138 | ||||
-rw-r--r-- | src/output/details.rs | 68 | ||||
-rw-r--r-- | src/output/grid_details.rs | 118 | ||||
-rw-r--r-- | src/output/lines.rs | 2 | ||||
-rw-r--r-- | src/output/mod.rs | 7 |
8 files changed, 274 insertions, 122 deletions
@@ -13,7 +13,7 @@ dependencies = [ "num_cpus 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "number_prefix 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", "pad 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "term_grid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "term_grid 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "threadpool 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-width 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "users 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -71,7 +71,7 @@ dependencies = [ [[package]] name = "git2" version = "0.2.12" -source = "git+https://github.com/alexcrichton/git2-rs.git#e5a439b13f45ca6b95fbf5f47ccf4b030d37ed1c" +source = "git+https://github.com/alexcrichton/git2-rs.git#3a7a990607a766fa65a40b920d70c8289691d2f8" dependencies = [ "bitflags 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", @@ -87,12 +87,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "libgit2-sys" version = "0.2.17" -source = "git+https://github.com/alexcrichton/git2-rs.git#e5a439b13f45ca6b95fbf5f47ccf4b030d37ed1c" +source = "git+https://github.com/alexcrichton/git2-rs.git#3a7a990607a766fa65a40b920d70c8289691d2f8" dependencies = [ "libc 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", "libssh2-sys 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", "libz-sys 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "openssl-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-sys 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", "pkg-config 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -111,7 +111,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "libc 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", "libz-sys 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "openssl-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-sys 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", "pkg-config 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -186,7 +186,7 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "gcc 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", @@ -262,7 +262,7 @@ dependencies = [ [[package]] name = "term_grid" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "unicode-width 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/src/column.rs b/src/column.rs index 23e2a4b..0da3651 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,37 +56,37 @@ 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, Clone)] pub struct Cell { pub length: usize, pub text: String, } 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..91c7f0f 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,16 +249,17 @@ 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") { - if matches.opt_present("across") { + let long = || { + if matches.opt_present("across") && !matches.opt_present("grid") { Err(Useless("across", true, "long")) } else if matches.opt_present("oneline") { @@ -272,78 +274,85 @@ 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 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 { + 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() } } @@ -718,5 +727,4 @@ mod test { let opts = Options::getopts(&[ "--level".to_string(), "69105".to_string() ]); assert_eq!(opts.unwrap_err(), Misfire::Useless2("level", "recurse", "tree")) } - } diff --git a/src/output/details.rs b/src/output/details.rs index 84da4de..9988b69 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,16 @@ 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 cells = self.cells_for_file(file); + self.add_file_with_cells(cells, file, depth, last, links) + } + + pub fn add_file_with_cells(&mut self, cells: Vec<Cell>, file: &File, depth: usize, last: bool, links: bool) { let row = Row { depth: depth, - cells: self.cells_for_file(file), - name: filename(file, &self.colours), + cells: cells, + 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 +216,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 +383,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 +394,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 +418,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 +429,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..92b373c --- /dev/null +++ b/src/output/grid_details.rs @@ -0,0 +1,118 @@ +use std::iter::repeat; + +use users::OSUsers; +use term_grid as grid; + +use column::{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_for_dir = self.details.columns.for_dir(dir); + let mut first_table = Table::with_options(self.details.colours, columns_for_dir.clone()); + let cells: Vec<_> = files.iter().map(|file| first_table.cells_for_file(file)).collect(); + + let mut last_working_table = self.make_grid(1, &*columns_for_dir, files, cells.clone()); + + for column_count in 2.. { + let grid = self.make_grid(column_count, &*columns_for_dir, files, cells.clone()); + + let the_grid_fits = { + let d = grid.fit_into_columns(column_count); + d.is_complete() && d.width() <= self.grid.console_width + }; + + if the_grid_fits { + last_working_table = grid; + } + else { + print!("{}", last_working_table.fit_into_columns(column_count - 1)); + return; + } + } + } + + fn make_table(&self, columns_for_dir: &[Column]) -> Table<OSUsers> { + let mut table = Table::with_options(self.details.colours, columns_for_dir.into()); + if self.details.header { table.add_header() } + table + } + + fn make_grid(&self, column_count: usize, columns_for_dir: &[Column], files: &[File], cells: Vec<Vec<Cell>>) -> grid::Grid { + let mut tables: Vec<_> = repeat(()).map(|_| self.make_table(columns_for_dir)).take(column_count).collect(); + + let mut num_cells = cells.len(); + if self.details.header { + num_cells += column_count; + } + + let original_height = divide_rounding_up(cells.len(), column_count); + let height = divide_rounding_up(num_cells, column_count); + + for (i, (file, row)) in files.iter().zip(cells.into_iter()).enumerate() { + let index = if self.grid.across { + i % column_count + } + else { + i / original_height + }; + + tables[index].add_file_with_cells(row, file, 0, false, false); + } + + let columns: Vec<_> = tables.iter().map(|t| t.print_table(false, false)).collect(); + + 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: 4, + }); + + if self.grid.across { + for row in 0 .. height { + for column in columns.iter() { + if row < column.len() { + let cell = grid::Cell { + contents: column[row].text.clone(), + width: column[row].length, + }; + + grid.add(cell); + } + } + } + } + else { + for column in columns.iter() { + for cell in column.iter() { + let cell = grid::Cell { + contents: cell.text.clone(), + width: cell.length, + }; + + grid.add(cell); + } + } + } + + grid + } +} + + +fn divide_rounding_up(a: usize, b: usize) -> usize { + let mut result = a / b; + if a % b != 0 { result += 1; } + result +}
\ No newline at end of file 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 { |