Replace Cells with growable TextCells

A recent change to ansi-term [1] means that `ANSIString`s can now hold either
owned *or* borrowed data (Rust calls this the Cow type). This means that we
can delay formatting ANSIStrings into ANSI-control-code-formatted strings
until it's absolutely necessary. The process for doing this was:

1. Replace the `Cell` type with a `TextCell` type that holds a vector of
   `ANSIString` values instead of a formatted string. It still does the
   width tracking.

2. Rework the details module's `render` functions to emit values of this
   type.

3. Similarly, rework the functions that produce cells containing filenames
   to use a `File` value's `name` field, which is an owned `String` that
   can now be re-used.

4. Update the printing, formatting, and width-calculating code in the
   details and grid-details views to produce a table by adding vectors
   together instead of adding strings together, delaying the formatting as
   long as it can.

This results in fewer allocations (as fewer `String` values are produced), and
makes the API tidier (as fewer `String` values are being passed around without
having their contents specified).

This also paves the way to Windows support, or at least support for
non-ANSI terminals: by delaying the time until strings are formatted,
it'll now be easier to change *how* they are formatted.

Casualties include:

- Bump to ansi_term v0.7.1, which impls `PartialEq` and `Debug` on
  `ANSIString`.
- The grid_details and lines views now need to take a vector of files, rather
  than a borrowed slice, so the filename cells produced now own the filename
  strings that get taken from files.
- Fixed the signature of `File#link_target` to specify that the
  file produced refers to the same directory, rather than some phantom
  directory with the same lifetime as the file. (This was wrong from the
  start, but it broke nothing until now)

References:

[1]: ansi-term@f6a6579ba8174de1cae64d181ec04af32ba2a4f0
This commit is contained in:
Benjamin Sago 2015-12-17 08:25:20 +08:00
parent 95c0d63045
commit c911b5f6e4
10 changed files with 327 additions and 207 deletions

32
Cargo.lock generated
View File

@ -2,13 +2,13 @@
name = "exa"
version = "0.4.0"
dependencies = [
"ansi_term 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"ansi_term 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
"bitflags 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"datetime 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)",
"git2 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"locale 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
"natord 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
"num_cpus 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
@ -38,7 +38,7 @@ dependencies = [
[[package]]
name = "ansi_term"
version = "0.7.0"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
@ -92,7 +92,7 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bitflags 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"libgit2-sys 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
"url 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -118,7 +118,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "libc"
version = "0.2.2"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
@ -127,10 +127,10 @@ version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cmake 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"libssh2-sys 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)",
"libz-sys 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"openssl-sys 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
"openssl-sys 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
"pkg-config 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -148,9 +148,9 @@ version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cmake 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"libz-sys 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"openssl-sys 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
"openssl-sys 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
"pkg-config 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",
"ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
@ -162,7 +162,7 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"gcc 0.3.20 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"pkg-config 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -185,7 +185,7 @@ name = "memchr"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -208,7 +208,7 @@ version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"kernel32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -222,10 +222,10 @@ dependencies = [
[[package]]
name = "openssl-sys"
version = "0.7.1"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"libressl-pnacl-sys 2.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"pkg-config 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -257,7 +257,7 @@ version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"advapi32-sys 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -346,7 +346,7 @@ name = "users"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]

View File

@ -7,7 +7,7 @@ authors = [ "ogham@bsago.me" ]
name = "exa"
[dependencies]
ansi_term = "0.7.0"
ansi_term = "0.7.1"
bitflags = "0.1"
datetime = "0.4.1"
getopts = "0.2.14"

View File

@ -195,7 +195,7 @@ impl<'dir> File<'dir> {
/// If statting the file fails (usually because the file on the
/// other end doesn't exist), returns the *filename* of the file
/// that should be there.
pub fn link_target(&self) -> Result<File, String> {
pub fn link_target(&self) -> Result<File<'dir>, String> {
let path = match fs::read_link(&self.path) {
Ok(path) => path,
Err(_) => return Err(self.name.clone()),

View File

@ -135,8 +135,8 @@ impl Exa {
match self.options.view {
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),
View::GridDetails(gd) => gd.view(dir, files),
View::Lines(l) => l.view(files),
}
}
}

130
src/output/cell.rs Normal file
View File

@ -0,0 +1,130 @@
//! The `TextCell` type for the details and lines views.
use ansi_term::{Style, ANSIString, ANSIStrings};
use unicode_width::UnicodeWidthStr;
/// An individual cell that holds text in a table, used in the details and
/// lines views to store ANSI-terminal-formatted data before it is printed.
///
/// A text cell is made up of zero or more strings coupled with the
/// pre-computed length of all the strings combined. When constructing details
/// or grid-details tables, the length will have to be queried multiple times,
/// so it makes sense to cache it.
///
/// (This used to be called `Cell`, but was renamed because theres a Rust
/// type by that name too.)
#[derive(PartialEq, Debug, Clone, Default)]
pub struct TextCell {
/// The contents of this cell, as a vector of ANSI-styled strings.
pub contents: TextCellContents,
/// The Unicode “display width” of this cell, in characters.
///
/// As with the `File` types width, this is related to the number of
/// *graphemes*, rather than *characters*, in the cell: most are 1 column
/// wide, but in some contexts, certain characters are two columns wide.
pub length: usize,
}
impl TextCell {
/// Creates a new text cell that holds the given text in the given style,
/// computing the Unicode width of the text.
pub fn paint(style: Style, text: String) -> Self {
TextCell {
length: text.width(),
contents: vec![ style.paint(text) ],
}
}
/// Creates a new text cell that holds the given text in the given style,
/// computing the Unicode width of the text. (This could be merged with
/// `paint`, but.)
pub fn paint_str(style: Style, text: &'static str) -> Self {
TextCell {
length: text.len(),
contents: vec![ style.paint(text) ],
}
}
/// Creates a new “blank” text cell that contains a single hyphen in the
/// given style, which should be the “punctuation” style from a `Colours`
/// value.
///
/// This is used in place of empty table cells, as it is easier to read
/// tabular data when there is *something* in each cell.
pub fn blank(style: Style) -> Self {
TextCell {
length: 1,
contents: vec![ style.paint("-") ],
}
}
/// Adds the given number of unstyled spaces after this cell.
///
/// This method allocates a `String` to hold the spaces.
pub fn add_spaces(&mut self, count: usize) {
use std::iter::repeat;
self.length += count;
let spaces: String = repeat(' ').take(count).collect();
self.contents.push(Style::default().paint(spaces));
}
/// Adds the contents of another `ANSIString` to the end of this cell.
pub fn push(&mut self, string: ANSIString<'static>, length: usize) {
self.contents.push(string);
self.length += length;
}
/// Adds all the contents of another `TextCell` to the end of this cell.
pub fn append(&mut self, other: TextCell) {
self.length += other.length;
self.contents.extend(other.contents);
}
/// Produces an `ANSIStrings` value that can be used to print the styled
/// values of this cell as an ANSI-terminal-formatted string.
pub fn strings(&self) -> ANSIStrings {
ANSIStrings(&self.contents)
}
}
// Id like to eventually abstract cells so that instead of *every* cell
// storing a vector, only variable-length cells would, and individual cells
// would just store an array of a fixed length (which would usually be just 1
// or 2), which wouldnt require a heap allocation.
//
// For examples, look at the `render_*` methods in the `Table` object in the
// details view:
//
// - `render_blocks`, `inode`, and `links` will always return a
// one-string-long TextCell;
// - `render_size` will return one or two strings in a TextCell, depending on
// the size and whether one is present;
// - `render_permissions` will return ten or eleven strings;
// - `filename` and `symlink_filename` in the output module root return six or
// five strings.
//
// In none of these cases are we dealing with a *truly variable* number of
// strings: it is only when the strings are concatenated together do we need a
// growable, heap-allocated buffer.
//
// So it would be nice to abstract the `TextCell` type so instead of a `Vec`,
// it can use anything of type `T: IntoIterator<Item=ANSIString<static>>`.
// This would allow us to still hold all the data, but allocate less.
//
// But exa still has bugs and I need to fix those first :(
/// The contents of a text cell, as a vector of ANSI-styled strings.
///
/// Its possible to use this type directly in the case where you want a
/// `TextCell` but arent concerned with tracking its width, because it occurs
/// in the final cell of a table or grid and theres no point padding it. This
/// happens when dealing with file names.
pub type TextCellContents = Vec<ANSIString<'static>>;

View File

@ -1,6 +1,3 @@
use ansi_term::Style;
use unicode_width::UnicodeWidthStr;
use dir::Dir;
@ -194,38 +191,3 @@ impl Default for TimeTypes {
TimeTypes { accessed: false, modified: true, created: false }
}
}
#[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);
}
}

View File

@ -124,9 +124,10 @@ use feature::xattr::{Attribute, FileAttributes};
use file::fields as f;
use file::File;
use options::{FileFilter, RecurseOptions};
use output::column::{Alignment, Column, Columns, Cell, SizeFormat};
use output::column::{Alignment, Column, Columns, SizeFormat};
use output::cell::TextCell;
use ansi_term::{ANSIString, ANSIStrings, Style};
use ansi_term::Style;
use datetime::local::{LocalDateTime, DatePiece};
use datetime::format::DateFormat;
@ -198,7 +199,7 @@ impl Details {
// Then add files to the table and print it out.
self.add_files_to_table(&mut table, files, 0);
for cell in table.print_table() {
println!("{}", cell.text);
println!("{}", cell.strings());
}
}
@ -213,20 +214,18 @@ impl Details {
let mut file_eggs = Vec::new();
struct Egg<'_> {
cells: Vec<Cell>,
name: Cell,
cells: Vec<TextCell>,
xattrs: Vec<Attribute>,
errors: Vec<(io::Error, Option<PathBuf>)>,
dir: Option<Dir>,
file: Arc<File<'_>>,
file: File<'_>,
}
pool.scoped(|scoped| {
let file_eggs = Arc::new(Mutex::new(&mut file_eggs));
let table = Arc::new(Mutex::new(&mut table));
for file in src.into_iter() {
let file: Arc<File> = Arc::new(file);
for file in src {
let file_eggs = file_eggs.clone();
let table = table.clone();
@ -251,11 +250,6 @@ impl Details {
let cells = table.lock().unwrap().cells_for_file(&file, !xattrs.is_empty());
let name = Cell {
text: filename(&file, &self.colours, true),
length: file.file_name_width()
};
let mut dir = None;
if let Some(r) = self.recurse {
@ -268,7 +262,6 @@ impl Details {
let egg = Egg {
cells: cells,
name: name,
xattrs: xattrs,
errors: errors,
dir: dir,
@ -280,17 +273,22 @@ impl Details {
}
});
file_eggs.sort_by(|a, b| self.filter.compare_files(&*a.file, &*b.file));
file_eggs.sort_by(|a, b| self.filter.compare_files(&a.file, &b.file));
let num_eggs = file_eggs.len();
for (index, egg) in file_eggs.into_iter().enumerate() {
let mut files = Vec::new();
let mut errors = egg.errors;
let name = TextCell {
length: egg.file.file_name_width(),
contents: filename(egg.file, &self.colours, true),
};
let row = Row {
depth: depth,
cells: Some(egg.cells),
name: egg.name,
name: name,
last: index == num_eggs - 1,
};
@ -342,14 +340,11 @@ struct Row {
/// almost always be `Some`, containing a vector of cells. It will only be
/// `None` for a row displaying an attribute or error, neither of which
/// have cells.
cells: Option<Vec<Cell>>,
// Did You Know?
// A Vec<Cell> and an Option<Vec<Cell>> actually have the same byte size!
cells: Option<Vec<TextCell>>,
/// This file's name, in coloured output. The name is treated separately
/// from the other cells, as it never requires padding.
name: Cell,
name: TextCell,
/// How many directories deep into the tree structure this is. Directories
/// on top have depth 0.
@ -429,8 +424,8 @@ impl<U> Table<U> where U: Users {
pub fn add_header(&mut self) {
let row = Row {
depth: 0,
cells: Some(self.columns.iter().map(|c| Cell::paint(self.colours.header, c.header())).collect()),
name: Cell::paint(self.colours.header, "Name"),
cells: Some(self.columns.iter().map(|c| TextCell::paint_str(self.colours.header, c.header())).collect()),
name: TextCell::paint_str(self.colours.header, "Name"),
last: false,
};
@ -446,7 +441,7 @@ impl<U> Table<U> where U: Users {
let row = Row {
depth: depth,
cells: None,
name: Cell::paint(self.colours.broken_arrow, &error_message),
name: TextCell::paint(self.colours.broken_arrow, error_message),
last: last,
};
@ -457,19 +452,26 @@ impl<U> Table<U> where U: Users {
let row = Row {
depth: depth,
cells: None,
name: Cell::paint(self.colours.perms.attribute, &format!("{} (len {})", xattr.name, xattr.size)),
name: TextCell::paint(self.colours.perms.attribute, format!("{} (len {})", xattr.name, xattr.size)),
last: last,
};
self.rows.push(row);
}
pub fn add_file_with_cells(&mut self, cells: Vec<Cell>, file: &File, depth: usize, last: bool, links: bool) {
pub fn filename_cell(&self, file: File, links: bool) -> TextCell {
TextCell {
length: file.file_name_width(),
contents: filename(file, &self.colours, links),
}
}
pub fn add_file_with_cells(&mut self, cells: Vec<TextCell>, name_cell: TextCell, depth: usize, last: bool) {
let row = Row {
depth: depth,
cells: Some(cells),
name: Cell { text: filename(file, &self.colours, links), length: file.file_name_width() },
last: last,
depth: depth,
cells: Some(cells),
name: name_cell,
last: last,
};
self.rows.push(row);
@ -477,13 +479,13 @@ impl<U> Table<U> where U: Users {
/// Use the list of columns to find which cells should be produced for
/// this file, per-column.
pub fn cells_for_file(&mut self, file: &File, xattrs: bool) -> Vec<Cell> {
pub fn cells_for_file(&mut self, file: &File, xattrs: bool) -> Vec<TextCell> {
self.columns.clone().iter()
.map(|c| self.display(file, c, xattrs))
.collect()
}
fn display(&mut self, file: &File, column: &Column, xattrs: bool) -> Cell {
fn display(&mut self, file: &File, column: &Column, xattrs: bool) -> TextCell {
use output::column::TimeType::*;
match *column {
@ -501,7 +503,7 @@ impl<U> Table<U> where U: Users {
}
}
fn render_permissions(&self, permissions: f::Permissions, xattrs: bool) -> Cell {
fn render_permissions(&self, permissions: f::Permissions, xattrs: bool) -> TextCell {
let c = self.colours.perms;
let bit = |bit, chr: &'static str, style: Style| {
if bit { style.paint(chr) } else { self.colours.punctuation.paint("-") }
@ -518,7 +520,7 @@ impl<U> Table<U> where U: Users {
let x_colour = if let f::Type::File = permissions.file_type { c.user_execute_file }
else { c.user_execute_other };
let mut columns = vec![
let mut chars = vec![
file_type,
bit(permissions.user_read, "r", c.user_read),
bit(permissions.user_write, "w", c.user_write),
@ -532,63 +534,71 @@ impl<U> Table<U> where U: Users {
];
if xattrs {
columns.push(c.attribute.paint("@"));
chars.push(c.attribute.paint("@"));
}
Cell {
text: ANSIStrings(&columns).to_string(),
length: columns.len(),
TextCell {
length: chars.len(),
contents: chars,
}
}
fn render_links(&self, links: f::Links) -> Cell {
fn render_links(&self, links: f::Links) -> TextCell {
let style = if links.multiple { self.colours.links.multi_link_file }
else { self.colours.links.normal };
Cell::paint(style, &self.numeric.format_int(links.count))
TextCell::paint(style, self.numeric.format_int(links.count))
}
fn render_blocks(&self, blocks: f::Blocks) -> Cell {
fn render_blocks(&self, blocks: f::Blocks) -> TextCell {
match blocks {
f::Blocks::Some(blocks) => Cell::paint(self.colours.blocks, &blocks.to_string()),
f::Blocks::None => Cell::paint(self.colours.punctuation, "-"),
f::Blocks::Some(blk) => TextCell::paint(self.colours.blocks, blk.to_string()),
f::Blocks::None => TextCell::blank(self.colours.punctuation),
}
}
fn render_inode(&self, inode: f::Inode) -> Cell {
Cell::paint(self.colours.inode, &inode.0.to_string())
fn render_inode(&self, inode: f::Inode) -> TextCell {
TextCell::paint(self.colours.inode, inode.0.to_string())
}
fn render_size(&self, size: f::Size, size_format: SizeFormat) -> Cell {
use number_prefix::{binary_prefix, decimal_prefix, Prefixed, Standalone, PrefixNames};
fn render_size(&self, size: f::Size, size_format: SizeFormat) -> TextCell {
use number_prefix::{binary_prefix, decimal_prefix};
use number_prefix::{Prefixed, Standalone, PrefixNames};
if let f::Size::Some(offset) = size {
let result = match size_format {
SizeFormat::DecimalBytes => decimal_prefix(offset as f64),
SizeFormat::BinaryBytes => binary_prefix(offset as f64),
SizeFormat::JustBytes => return Cell::paint(self.colours.size.numbers, &self.numeric.format_int(offset)),
};
let size = match size {
f::Size::Some(s) => s,
f::Size::None => return TextCell::blank(self.colours.punctuation),
};
match result {
Standalone(bytes) => Cell::paint(self.colours.size.numbers, &*bytes.to_string()),
Prefixed(prefix, n) => {
let number = if n < 10f64 { self.numeric.format_float(n, 1) } else { self.numeric.format_int(n as isize) };
let symbol = prefix.symbol();
let result = match size_format {
SizeFormat::DecimalBytes => decimal_prefix(size as f64),
SizeFormat::BinaryBytes => binary_prefix(size as f64),
SizeFormat::JustBytes => {
let string = self.numeric.format_int(size);
return TextCell::paint(self.colours.size.numbers, string);
},
};
Cell {
text: ANSIStrings( &[ self.colours.size.numbers.paint(&number[..]), self.colours.size.unit.paint(symbol) ]).to_string(),
length: number.len() + symbol.len(),
}
}
}
}
else {
Cell::paint(self.colours.punctuation, "-")
let (prefix, n) = match result {
Standalone(b) => return TextCell::paint(self.colours.size.numbers, b.to_string()),
Prefixed(p, n) => (p, n)
};
let symbol = prefix.symbol();
let number = if n < 10f64 { self.numeric.format_float(n, 1) }
else { self.numeric.format_int(n as isize) };
TextCell {
length: number.len() + symbol.len(),
contents: vec![
self.colours.size.numbers.paint(number),
self.colours.size.unit.paint(symbol),
],
}
}
#[allow(trivial_numeric_casts)]
fn render_time(&self, timestamp: f::Time) -> Cell {
fn render_time(&self, timestamp: f::Time) -> TextCell {
let date = self.tz.at(LocalDateTime::at(timestamp.0 as i64));
let datestamp = if date.year() == self.current_year {
@ -598,60 +608,60 @@ impl<U> Table<U> where U: Users {
DATE_AND_YEAR.format(&date, &self.time)
};
Cell::paint(self.colours.date, &datestamp)
TextCell::paint(self.colours.date, datestamp)
}
fn render_git_status(&self, git: f::Git) -> Cell {
Cell {
text: ANSIStrings(&[ self.render_git_char(git.staged),
self.render_git_char(git.unstaged) ]).to_string(),
length: 2,
}
}
fn render_git_char(&self, status: f::GitStatus) -> ANSIString {
match status {
fn render_git_status(&self, git: f::Git) -> TextCell {
let git_char = |status| match status {
f::GitStatus::NotModified => self.colours.punctuation.paint("-"),
f::GitStatus::New => self.colours.git.new.paint("N"),
f::GitStatus::Modified => self.colours.git.modified.paint("M"),
f::GitStatus::Deleted => self.colours.git.deleted.paint("D"),
f::GitStatus::Renamed => self.colours.git.renamed.paint("R"),
f::GitStatus::TypeChange => self.colours.git.typechange.paint("T"),
};
TextCell {
length: 2,
contents: vec![
git_char(git.staged),
git_char(git.unstaged)
],
}
}
fn render_user(&mut self, user: f::User) -> Cell {
fn render_user(&mut self, user: f::User) -> TextCell {
let user_name = match self.users.get_user_by_uid(user.0) {
Some(user) => user.name,
None => user.0.to_string(),
};
let style = if self.users.get_current_uid() == user.0 { self.colours.users.user_you }
else { self.colours.users.user_someone_else };
Cell::paint(style, &*user_name)
else { self.colours.users.user_someone_else };
TextCell::paint(style, user_name)
}
fn render_group(&mut self, group: f::Group) -> Cell {
fn render_group(&mut self, group: f::Group) -> TextCell {
let mut style = self.colours.users.group_not_yours;
let group_name = match self.users.get_group_by_gid(group.0) {
Some(group) => {
let current_uid = self.users.get_current_uid();
if let Some(current_user) = self.users.get_user_by_uid(current_uid) {
if current_user.primary_group == group.gid || group.members.contains(&current_user.name) {
style = self.colours.users.group_yours;
}
}
group.name
},
None => group.0.to_string(),
let group = match self.users.get_group_by_gid(group.0) {
Some(g) => g,
None => return TextCell::paint(style, group.0.to_string()),
};
Cell::paint(style, &*group_name)
let current_uid = self.users.get_current_uid();
if let Some(current_user) = self.users.get_user_by_uid(current_uid) {
if current_user.primary_group == group.gid
|| group.members.contains(&current_user.name) {
style = self.colours.users.group_yours;
}
}
TextCell::paint(style, group.name)
}
/// Render the table as a vector of Cells, to be displayed on standard output.
pub fn print_table(&self) -> Vec<Cell> {
pub fn print_table(self) -> Vec<TextCell> {
let mut stack = Vec::new();
let mut cells = Vec::new();
@ -664,14 +674,16 @@ impl<U> Table<U> where U: Users {
let total_width: usize = self.columns.len() + column_widths.iter().fold(0, Add::add);
for row in self.rows.iter() {
let mut cell = Cell::empty();
for row in self.rows {
let mut cell = TextCell::default();
if let Some(cells) = row.cells {
for (n, (this_cell, width)) in cells.into_iter().zip(column_widths.iter()).enumerate() {
let padding = width - this_cell.length;
if let Some(ref cells) = row.cells {
for (n, width) in column_widths.iter().enumerate() {
match self.columns[n].alignment() {
Alignment::Left => { cell.append(&cells[n]); cell.add_spaces(width - cells[n].length); }
Alignment::Right => { cell.add_spaces(width - cells[n].length); cell.append(&cells[n]); }
Alignment::Left => { cell.append(this_cell); cell.add_spaces(padding); }
Alignment::Right => { cell.add_spaces(padding); cell.append(this_cell); }
}
cell.add_spaces(1);
@ -681,8 +693,7 @@ impl<U> Table<U> where U: Users {
cell.add_spaces(total_width)
}
let mut filename = String::new();
let mut filename_length = 0;
let mut filename = TextCell::default();
// A stack tracks which tree characters should be printed. It's
// necessary to maintain information about the previously-printed
@ -699,8 +710,7 @@ 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 {
filename.push_str(&*self.colours.punctuation.paint(stack[i].ascii_art()).to_string());
filename_length += 4;
filename.push(self.colours.punctuation.paint(stack[i].ascii_art()), 4);
}
stack[row.depth] = if row.last { TreePart::Blank } else { TreePart::Line };
@ -708,15 +718,13 @@ 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 {
filename.push(' ');
filename_length += 1;
filename.add_spaces(1);
}
// Print the name without worrying about padding.
filename.push_str(&*row.name.text);
filename_length += row.name.length;
filename.append(row.name);
cell.append(&Cell { text: filename, length: filename_length });
cell.append(filename);
cells.push(cell);
}
@ -767,7 +775,8 @@ pub mod test {
pub use super::Table;
pub use file::File;
pub use file::fields as f;
pub use output::column::{Cell, Column};
pub use output::column::Column;
pub use output::cell::TextCell;
pub use users::{User, Group, uid_t, gid_t};
pub use users::mock::MockUsers;
@ -806,7 +815,7 @@ pub mod test {
table.users = users;
let user = f::User(1000);
let expected = Cell::paint(Red.bold(), "enoch");
let expected = TextCell::paint_str(Red.bold(), "enoch");
assert_eq!(expected, table.render_user(user))
}
@ -819,7 +828,7 @@ pub mod test {
table.users = users;
let user = f::User(1000);
let expected = Cell::paint(Cyan.bold(), "1000");
let expected = TextCell::paint_str(Cyan.bold(), "1000");
assert_eq!(expected, table.render_user(user));
}
@ -830,7 +839,7 @@ pub mod test {
table.users.add_user(newser(1000, "enoch", 100));
let user = f::User(1000);
let expected = Cell::paint(Green.bold(), "enoch");
let expected = TextCell::paint_str(Green.bold(), "enoch");
assert_eq!(expected, table.render_user(user));
}
@ -840,7 +849,7 @@ pub mod test {
table.colours.users.user_someone_else = Red.normal();
let user = f::User(1000);
let expected = Cell::paint(Red.normal(), "1000");
let expected = TextCell::paint_str(Red.normal(), "1000");
assert_eq!(expected, table.render_user(user));
}
@ -850,7 +859,7 @@ pub mod test {
table.colours.users.user_someone_else = Blue.underline();
let user = f::User(2_147_483_648);
let expected = Cell::paint(Blue.underline(), "2147483648");
let expected = TextCell::paint_str(Blue.underline(), "2147483648");
assert_eq!(expected, table.render_user(user));
}
}
@ -869,7 +878,7 @@ pub mod test {
table.users = users;
let group = f::Group(100);
let expected = Cell::paint(Fixed(101).normal(), "folk");
let expected = TextCell::paint_str(Fixed(101).normal(), "folk");
assert_eq!(expected, table.render_group(group))
}
@ -882,7 +891,7 @@ pub mod test {
table.users = users;
let group = f::Group(100);
let expected = Cell::paint(Fixed(87).normal(), "100");
let expected = TextCell::paint_str(Fixed(87).normal(), "100");
assert_eq!(expected, table.render_group(group));
}
@ -897,7 +906,7 @@ pub mod test {
table.users = users;
let group = f::Group(100);
let expected = Cell::paint(Fixed(64).normal(), "folk");
let expected = TextCell::paint_str(Fixed(64).normal(), "folk");
assert_eq!(expected, table.render_group(group))
}
@ -912,7 +921,7 @@ pub mod test {
table.users = users;
let group = f::Group(100);
let expected = Cell::paint(Fixed(31).normal(), "folk");
let expected = TextCell::paint_str(Fixed(31).normal(), "folk");
assert_eq!(expected, table.render_group(group))
}
@ -922,7 +931,7 @@ pub mod test {
table.colours.users.group_not_yours = Blue.underline();
let group = f::Group(2_147_483_648);
let expected = Cell::paint(Blue.underline(), "2147483648");
let expected = TextCell::paint_str(Blue.underline(), "2147483648");
assert_eq!(expected, table.render_group(group));
}
}

View File

@ -1,5 +1,6 @@
use std::iter::repeat;
use ansi_term::ANSIStrings;
use users::OSUsers;
use term_grid as grid;
@ -7,7 +8,8 @@ use dir::Dir;
use feature::xattr::FileAttributes;
use file::File;
use output::column::{Column, Cell};
use output::cell::TextCell;
use output::column::Column;
use output::details::{Details, Table};
use output::grid::Grid;
@ -25,19 +27,25 @@ fn file_has_xattrs(file: &File) -> bool {
}
impl GridDetails {
pub fn view(&self, dir: Option<&Dir>, files: &[File]) {
pub fn view(&self, dir: Option<&Dir>, files: Vec<File>) {
let columns_for_dir = match self.details.columns {
Some(cols) => cols.for_dir(dir),
None => Vec::new(),
};
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, file_has_xattrs(file))).collect();
let cells = files.iter()
.map(|file| first_table.cells_for_file(file, file_has_xattrs(file)))
.collect::<Vec<_>>();
let mut last_working_table = self.make_grid(1, &*columns_for_dir, files, cells.clone());
let file_names = files.into_iter()
.map(|file| first_table.filename_cell(file, false))
.collect::<Vec<_>>();
let mut last_working_table = self.make_grid(1, &columns_for_dir, &file_names, cells.clone());
for column_count in 2.. {
let grid = self.make_grid(column_count, &*columns_for_dir, files, cells.clone());
let grid = self.make_grid(column_count, &columns_for_dir, &file_names, cells.clone());
let the_grid_fits = {
let d = grid.fit_into_columns(column_count);
@ -60,7 +68,7 @@ impl GridDetails {
table
}
fn make_grid(&self, column_count: usize, columns_for_dir: &[Column], files: &[File], cells: Vec<Vec<Cell>>) -> grid::Grid {
fn make_grid(&self, column_count: usize, columns_for_dir: &[Column], file_names: &[TextCell], cells: Vec<Vec<TextCell>>) -> grid::Grid {
let mut tables: Vec<_> = repeat(()).map(|_| self.make_table(columns_for_dir)).take(column_count).collect();
let mut num_cells = cells.len();
@ -71,7 +79,7 @@ impl GridDetails {
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() {
for (i, (file_name, row)) in file_names.iter().zip(cells.into_iter()).enumerate() {
let index = if self.grid.across {
i % column_count
}
@ -79,10 +87,10 @@ impl GridDetails {
i / original_height
};
tables[index].add_file_with_cells(row, file, 0, false, false);
tables[index].add_file_with_cells(row, file_name.clone(), 0, false);
}
let columns: Vec<_> = tables.iter().map(|t| t.print_table()).collect();
let columns: Vec<_> = tables.into_iter().map(|t| t.print_table()).collect();
let direction = if self.grid.across { grid::Direction::LeftToRight }
else { grid::Direction::TopToBottom };
@ -97,7 +105,7 @@ impl GridDetails {
for column in columns.iter() {
if row < column.len() {
let cell = grid::Cell {
contents: column[row].text.clone(),
contents: ANSIStrings(&column[row].contents).to_string(),
width: column[row].length,
};
@ -110,7 +118,7 @@ impl GridDetails {
for column in columns.iter() {
for cell in column.iter() {
let cell = grid::Cell {
contents: cell.text.clone(),
contents: ANSIStrings(&cell.contents).to_string(),
width: cell.length,
};

View File

@ -1,6 +1,8 @@
use colours::Colours;
use file::File;
use ansi_term::ANSIStrings;
use super::filename;
@ -11,9 +13,9 @@ pub struct Lines {
/// The lines view literally just displays each file, line-by-line.
impl Lines {
pub fn view(&self, files: &[File]) {
pub fn view(&self, files: Vec<File>) {
for file in files {
println!("{}", filename(file, &self.colours, true));
println!("{}", ANSIStrings(&filename(file, &self.colours, true)));
}
}
}

View File

@ -1,9 +1,10 @@
use ansi_term::ANSIStrings;
use ansi_term::Style;
use colours::Colours;
use file::File;
use filetype::file_colour;
pub use self::cell::{TextCell, TextCellContents};
pub use self::details::Details;
pub use self::grid::Grid;
pub use self::lines::Lines;
@ -14,29 +15,37 @@ pub mod details;
mod lines;
mod grid_details;
pub mod column;
mod cell;
pub fn filename(file: &File, colours: &Colours, links: bool) -> String {
pub fn filename(file: File, colours: &Colours, links: bool) -> TextCellContents {
if links && file.is_link() {
symlink_filename(file, colours)
}
else {
let style = file_colour(colours, file);
style.paint(&*file.name).to_string()
vec![
file_colour(colours, &file).paint(file.name)
]
}
}
fn symlink_filename(file: &File, colours: &Colours) -> String {
fn symlink_filename(file: File, colours: &Colours) -> TextCellContents {
match file.link_target() {
Ok(target) => format!("{} {} {}",
file_colour(colours, file).paint(&*file.name),
colours.punctuation.paint("->"),
ANSIStrings(&[ colours.symlink_path.paint(target.path_prefix()),
file_colour(colours, &target).paint(target.name) ])),
Ok(target) => vec![
file_colour(colours, &file).paint(file.name),
Style::default().paint(" "),
colours.punctuation.paint("->"),
Style::default().paint(" "),
colours.symlink_path.paint(target.path_prefix()),
file_colour(colours, &target).paint(target.name)
],
Err(filename) => format!("{} {} {}",
file_colour(colours, file).paint(&*file.name),
colours.broken_arrow.paint("->"),
colours.broken_filename.paint(filename)),
Err(filename) => vec![
file_colour(colours, &file).paint(file.name),
Style::default().paint(" "),
colours.broken_arrow.paint("->"),
Style::default().paint(" "),
colours.broken_filename.paint(filename),
],
}
}