diff --git a/Cargo.lock b/Cargo.lock index ceafcad..ef1f2c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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)", + "git2 0.3.4 (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.5 (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]] @@ -53,10 +53,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "cmake" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "gcc 0.3.20 (registry+https://github.com/rust-lang/crates.io-index)", + "gcc 0.3.21 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -66,21 +66,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "libc 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", "locale 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", - "num 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)", + "num 0.1.30 (registry+https://github.com/rust-lang/crates.io-index)", "pad 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.1.47 (registry+https://github.com/rust-lang/crates.io-index)", "tz 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "gcc" -version = "0.3.20" +version = "0.3.21" 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)", "winapi 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "gdi32-sys" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "getopts" version = "0.2.14" @@ -88,12 +96,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "git2" -version = "0.3.3" +version = "0.3.4" 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)", - "libgit2-sys 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", + "libgit2-sys 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", "url 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -118,19 +126,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "libc" -version = "0.2.2" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "libgit2-sys" -version = "0.3.8" +version = "0.3.9" 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)", + "cmake 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.5 (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.4 (registry+https://github.com/rust-lang/crates.io-index)", "pkg-config 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -147,10 +155,10 @@ name = "libssh2-sys" 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)", + "cmake 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.5 (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.4 (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)", @@ -161,8 +169,8 @@ name = "libz-sys" 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)", + "gcc 0.3.21 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", "pkg-config 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -172,7 +180,7 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "libc 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", - "num 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)", + "num 0.1.30 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -185,7 +193,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.5 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -195,10 +203,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "num" -version = "0.1.28" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "rand 0.3.12 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -208,7 +216,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.5 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -217,17 +225,19 @@ name = "number_prefix" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "num 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)", + "num 0.1.30 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "openssl-sys" -version = "0.7.1" +version = "0.7.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)", + "gdi32-sys 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.5 (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)", + "user32-sys 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -253,17 +263,17 @@ dependencies = [ [[package]] name = "rand" -version = "0.3.12" +version = "0.3.13" 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.5 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "regex" -version = "0.1.43" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "aho-corasick 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -307,7 +317,7 @@ name = "tempdir" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "rand 0.3.12 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -341,12 +351,21 @@ dependencies = [ "uuid 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "user32-sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] 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.5 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -354,7 +373,7 @@ name = "uuid" version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "rand 0.3.12 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/Cargo.toml b/Cargo.toml index 01ef139..f328769 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/file.rs b/src/file.rs index 08c45bb..53265d7 100644 --- a/src/file.rs +++ b/src/file.rs @@ -7,8 +7,6 @@ use std::io::Result as IOResult; use std::os::unix::fs::{MetadataExt, PermissionsExt}; use std::path::{Component, Path, PathBuf}; -use unicode_width::UnicodeWidthStr; - use dir::Dir; use self::fields as f; @@ -180,22 +178,13 @@ impl<'dir> File<'dir> { path_prefix } - /// The Unicode 'display width' of the filename. - /// - /// This is related to the number of graphemes in the string: most - /// characters are 1 columns wide, but in some contexts, certain - /// characters are actually 2 columns wide. - pub fn file_name_width(&self) -> usize { - UnicodeWidthStr::width(&self.name[..]) - } - /// Assuming the current file is a symlink, follows the link and /// returns a File object from the path the link points to. /// /// 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 { + pub fn link_target(&self) -> Result, String> { let path = match fs::read_link(&self.path) { Ok(path) => path, Err(_) => return Err(self.name.clone()), diff --git a/src/filetype.rs b/src/filetype.rs index ed2f369..b8427d4 100644 --- a/src/filetype.rs +++ b/src/filetype.rs @@ -1,31 +1,7 @@ -use ansi_term::Style; - use file::File; -use colours::Colours; -pub fn file_colour(colours: &Colours, file: &File) -> Style { - match file { - f if f.is_directory() => colours.filetypes.directory, - f if f.is_executable_file() => colours.filetypes.executable, - f if f.is_link() => colours.filetypes.symlink, - f if !f.is_file() => colours.filetypes.special, - f if f.is_immediate() => colours.filetypes.immediate, - f if f.is_image() => colours.filetypes.image, - f if f.is_video() => colours.filetypes.video, - f if f.is_music() => colours.filetypes.music, - f if f.is_lossless() => colours.filetypes.lossless, - f if f.is_crypto() => colours.filetypes.crypto, - f if f.is_document() => colours.filetypes.document, - f if f.is_compressed() => colours.filetypes.compressed, - f if f.is_temp() => colours.filetypes.temp, - f if f.is_compiled() => colours.filetypes.compiled, - _ => colours.filetypes.normal, - } -} - - -trait FileTypes { +pub trait FileTypes { fn is_immediate(&self) -> bool; fn is_image(&self) -> bool; fn is_video(&self) -> bool; diff --git a/src/main.rs b/src/main.rs index 1e9f76c..e290853 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,7 +26,6 @@ use dir::Dir; use file::File; use options::{Options, View}; -mod colours; mod dir; mod feature; mod file; @@ -135,8 +134,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), } } } diff --git a/src/options.rs b/src/options.rs index adffef1..c9baa2e 100644 --- a/src/options.rs +++ b/src/options.rs @@ -7,10 +7,10 @@ use std::os::unix::fs::MetadataExt; use getopts; use natord; -use colours::Colours; use feature::xattr; use file::File; use output::{Grid, Details, GridDetails, Lines}; +use output::Colours; use output::column::{Columns, TimeTypes, SizeFormat}; use term::dimensions; diff --git a/src/output/cell.rs b/src/output/cell.rs new file mode 100644 index 0000000..d900f6d --- /dev/null +++ b/src/output/cell.rs @@ -0,0 +1,205 @@ +//! The `TextCell` type for the details and lines views. + +use std::ops::{Deref, DerefMut}; + +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 there’s 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. + pub width: DisplayWidth, +} + +impl Deref for TextCell { + type Target = TextCellContents; + + fn deref<'a>(&'a self) -> &'a Self::Target { + &self.contents + } +} + +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 { + let width = DisplayWidth::from(&*text); + + TextCell { + contents: vec![ style.paint(text) ].into(), + width: width, + } + } + + /// 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 { + let width = DisplayWidth::from(text); + + TextCell { + contents: vec![ style.paint(text) ].into(), + width: width, + } + } + + /// 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 { + contents: vec![ style.paint("-") ].into(), + width: DisplayWidth::from(1), + } + } + + /// 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.width) += count; + + let spaces: String = repeat(' ').take(count).collect(); + self.contents.0.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>, extra_width: usize) { + self.contents.0.push(string); + (*self.width) += extra_width; + } + + /// Adds all the contents of another `TextCell` to the end of this cell. + pub fn append(&mut self, other: TextCell) { + (*self.width) += *other.width; + self.contents.0.extend(other.contents.0); + } +} + + +// I’d 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 wouldn’t 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>`. +// 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. +/// +/// It’s possible to use this type directly in the case where you want a +/// `TextCell` but aren’t concerned with tracking its width, because it occurs +/// in the final cell of a table or grid and there’s no point padding it. This +/// happens when dealing with file names. +#[derive(PartialEq, Debug, Clone, Default)] +pub struct TextCellContents(Vec>); + +impl From>> for TextCellContents { + fn from(strings: Vec>) -> TextCellContents { + TextCellContents(strings) + } +} + +impl Deref for TextCellContents { + type Target = [ANSIString<'static>]; + + fn deref<'a>(&'a self) -> &'a Self::Target { + &*self.0 + } +} + +// No DerefMut implementation here -- it would be publicly accessible, and as +// the contents only get changed in this module, the mutators in the struct +// above can just access the value directly. + +impl TextCellContents { + + /// 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.0) + } +} + + +/// The Unicode “display width” of a string. +/// +/// This is related to the number of *graphemes* of a string, rather than the +/// number of *characters*, or *bytes*: although most characters are one +/// column wide, a few can be two columns wide, and this is important to note +/// when calculating widths for displaying tables in a terminal. +/// +/// This type is used to ensure that the width, rather than the length, is +/// used when constructing a `TextCell` -- it's too easy to write something +/// like `file_name.len()` and assume it will work! +/// +/// It has `From` impls that convert an input string or fixed with to values +/// of this type, and will `Deref` to the contained `usize` value. +#[derive(PartialEq, Debug, Clone, Copy, Default)] +pub struct DisplayWidth(usize); + +impl<'a> From<&'a str> for DisplayWidth { + fn from(input: &'a str) -> DisplayWidth { + DisplayWidth(UnicodeWidthStr::width(input)) + } +} + +impl From for DisplayWidth { + fn from(width: usize) -> DisplayWidth { + DisplayWidth(width) + } +} + +impl Deref for DisplayWidth { + type Target = usize; + + fn deref<'a>(&'a self) -> &'a Self::Target { + &self.0 + } +} + +impl DerefMut for DisplayWidth { + fn deref_mut<'a>(&'a mut self) -> &'a mut Self::Target { + &mut self.0 + } +} \ No newline at end of file diff --git a/src/colours.rs b/src/output/colours.rs similarity index 100% rename from src/colours.rs rename to src/output/colours.rs diff --git a/src/output/column.rs b/src/output/column.rs index c2b8568..cfec1f9 100644 --- a/src/output/column.rs +++ b/src/output/column.rs @@ -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); - } -} diff --git a/src/output/details.rs b/src/output/details.rs index a7d1da9..c292217 100644 --- a/src/output/details.rs +++ b/src/output/details.rs @@ -56,46 +56,6 @@ //! can be displayed, in order to make sure that every column is wide enough. //! //! -//! ## Constructing Tree Views -//! -//! When using the `--tree` argument, instead of a vector of cells, each row has a -//! `depth` field that indicates how far deep in the tree it is: the top level has -//! depth 0, its children have depth 1, and *their* children have depth 2, and so -//! on. -//! -//! On top of this, it also has a `last` field that specifies whether this is the -//! last row of this particular consecutive set of rows. This doesn't affect the -//! file's information; it's just used to display a different set of Unicode tree -//! characters! The resulting table looks like this: -//! -//! ┌───────┬───────┬───────────────────────┐ -//! │ Depth │ Last │ Output │ -//! ├───────┼───────┼───────────────────────┤ -//! │ 0 │ │ documents │ -//! │ 1 │ false │ ├── this_file.txt │ -//! │ 1 │ false │ ├── that_file.txt │ -//! │ 1 │ false │ ├── features │ -//! │ 2 │ false │ │ ├── feature_1.rs │ -//! │ 2 │ false │ │ ├── feature_2.rs │ -//! │ 2 │ true │ │ └── feature_3.rs │ -//! │ 1 │ true │ └── pictures │ -//! │ 2 │ false │ ├── garden.jpg │ -//! │ 2 │ false │ ├── flowers.jpg │ -//! │ 2 │ false │ ├── library.png │ -//! │ 2 │ true │ └── space.tiff │ -//! └───────┴───────┴───────────────────────┘ -//! -//! Creating the table like this means that each file has to be tested to see if -//! it's the last one in the group. This is usually done by putting all the files -//! in a vector beforehand, getting its length, then comparing the index of each -//! file to see if it's the last one. (As some files may not be successfully -//! `stat`ted, we don't know how many files are going to exist in each directory) -//! -//! These rows have a `None` value for their vector of cells, instead of a `Some` -//! vector containing any. It's possible to have *both* a vector of cells and -//! depth and last flags when the user specifies `--tree` *and* `--long`. -//! -//! //! ## Extended Attributes and Errors //! //! Finally, files' extended attributes and any errors that occur while statting @@ -113,30 +73,30 @@ use std::error::Error; use std::io; +use std::ops::Add; use std::path::PathBuf; use std::string::ToString; -use std::ops::Add; -use std::iter::repeat; +use std::sync::{Arc, Mutex}; + +use ansi_term::Style; + +use datetime::format::DateFormat; +use datetime::local::{LocalDateTime, DatePiece}; +use datetime::zoned::TimeZone; + +use locale; + +use users::{OSUsers, Users, Groups}; -use colours::Colours; use dir::Dir; 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 ansi_term::{ANSIString, ANSIStrings, Style}; - -use datetime::local::{LocalDateTime, DatePiece}; -use datetime::format::DateFormat; -use datetime::zoned::TimeZone; - -use locale; - -use users::{OSUsers, Users}; -use users::mock::MockUsers; - +use output::colours::Colours; +use output::column::{Alignment, Column, Columns, SizeFormat}; +use output::cell::{TextCell, DisplayWidth}; +use output::tree::TreeTrunk; use super::filename; @@ -151,7 +111,7 @@ use super::filename; /// /// Almost all the heavy lifting is done in a Table object, which handles the /// columns for each row. -#[derive(PartialEq, Debug, Copy, Clone)] +#[derive(PartialEq, Debug, Copy, Clone, Default)] pub struct Details { /// A Columns object that says which columns should be included in the @@ -178,6 +138,42 @@ pub struct Details { pub colours: Colours, } +/// The **environment** struct contains any data that could change between +/// running instances of exa, depending on the user's computer's configuration. +/// +/// Any environment field should be able to be mocked up for test runs. +pub struct Environment { + + /// The year of the current time. This gets used to determine which date + /// format to use. + current_year: i64, + + /// Localisation rules for formatting numbers. + numeric: locale::Numeric, + + /// Localisation rules for formatting timestamps. + time: locale::Time, + + /// The computer's current time zone. This gets used to determine how to + /// offset files' timestamps. + tz: TimeZone, + + /// Mapping cache of user IDs to usernames. + users: Mutex, +} + +impl Default for Environment { + fn default() -> Self { + Environment { + current_year: LocalDateTime::now().year(), + numeric: locale::Numeric::load_user_locale().unwrap_or_else(|_| locale::Numeric::english()), + time: locale::Time::load_user_locale().unwrap_or_else(|_| locale::Time::english()), + tz: TimeZone::localtime().unwrap(), + users: Mutex::new(OSUsers::empty_cache()), + } + } +} + impl Details { /// Print the details of the given vector of files -- all of which will @@ -191,70 +187,70 @@ impl Details { None => Vec::new(), }; + // Then, retrieve various environment variables. + let env = Arc::new(Environment::::default()); + + // Build the table to put rows in. + let mut table = Table { + columns: &*columns_for_dir, + opts: &self, + env: env, + rows: Vec::new(), + }; + // Next, add a header if the user requests it. - let mut table = Table::with_options(self.colours, columns_for_dir); if self.header { table.add_header() } // 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()); } } /// Adds files to the table, possibly recursively. This is easily /// parallelisable, and uses a pool of threads. - fn add_files_to_table<'dir, U: Users+Send>(&self, mut table: &mut Table, src: Vec>, depth: usize) { + fn add_files_to_table<'dir, U: Users+Groups+Send>(&self, mut table: &mut Table, src: Vec>, depth: usize) { use num_cpus; use scoped_threadpool::Pool; use std::sync::{Arc, Mutex}; + use feature::xattr; let mut pool = Pool::new(num_cpus::get() as u32); let mut file_eggs = Vec::new(); struct Egg<'_> { - cells: Vec, - name: Cell, + cells: Vec, xattrs: Vec, errors: Vec<(io::Error, Option)>, dir: Option, - file: Arc>, + file: File<'_>, } pool.scoped(|scoped| { let file_eggs = Arc::new(Mutex::new(&mut file_eggs)); - let table = Arc::new(Mutex::new(&mut table)); + let table = Arc::new(&mut table); - for file in src.into_iter() { - let file: Arc = Arc::new(file); + for file in src { let file_eggs = file_eggs.clone(); let table = table.clone(); scoped.execute(move || { let mut errors = Vec::new(); - let mut xattrs = Vec::new(); - match file.path.attributes() { - Ok(xs) => { - if self.xattr { - for xattr in xs { - xattrs.push(xattr); - } - } - }, - Err(e) => { - if self.xattr { - errors.push((e, None)); - } - }, - }; - let cells = table.lock().unwrap().cells_for_file(&file, !xattrs.is_empty()); + if xattr::ENABLED { + match file.path.attributes() { + Ok(xs) => xattrs.extend(xs), + Err(e) => errors.push((e, None)), + }; + } - let name = Cell { - text: filename(&file, &self.colours, true), - length: file.file_name_width() - }; + let cells = table.cells_for_file(&file, !xattrs.is_empty()); + + if !table.opts.xattr { + xattrs.clear(); + } let mut dir = None; @@ -268,7 +264,6 @@ impl Details { let egg = Egg { cells: cells, - name: name, xattrs: xattrs, errors: errors, dir: dir, @@ -280,17 +275,23 @@ 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 width = DisplayWidth::from(&*egg.file.name); + + let name = TextCell { + contents: filename(egg.file, &self.colours, true), + width: width, + }; let row = Row { depth: depth, cells: Some(egg.cells), - name: egg.name, + name: name, last: index == num_eggs - 1, }; @@ -334,7 +335,7 @@ impl Details { } -struct Row { +pub struct Row { /// Vector of cells to display. /// @@ -342,14 +343,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>, - - // Did You Know? - // A Vec and an Option> actually have the same byte size! + cells: Option>, /// 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. @@ -366,7 +364,7 @@ impl Row { /// not, returns 0. fn column_width(&self, index: usize) -> usize { match self.cells { - Some(ref cells) => cells[index].length, + Some(ref cells) => *cells[index].width, None => 0, } } @@ -375,53 +373,15 @@ impl Row { /// A **Table** object gets built up by the view as it lists files and /// directories. -pub struct Table { - columns: Vec, - rows: Vec, +pub struct Table<'a, U: Users+Groups+'a> { + pub rows: Vec, - time: locale::Time, - numeric: locale::Numeric, - tz: TimeZone, - users: U, - colours: Colours, - current_year: i64, + pub columns: &'a [Column], + pub opts: &'a Details, + pub env: Arc>, } -impl Default for Table { - fn default() -> Table { - Table { - columns: Columns::default().for_dir(None), - rows: Vec::new(), - time: locale::Time::english(), - numeric: locale::Numeric::english(), - tz: TimeZone::localtime().unwrap(), - users: MockUsers::with_current_uid(0), - colours: Colours::default(), - current_year: 1234, - } - } -} - -impl Table { - - /// Create a new, empty Table object, setting the caching fields to their - /// empty states. - pub fn with_options(colours: Colours, columns: Vec) -> Table { - Table { - columns: columns, - rows: Vec::new(), - - time: locale::Time::load_user_locale().unwrap_or_else(|_| locale::Time::english()), - numeric: locale::Numeric::load_user_locale().unwrap_or_else(|_| locale::Numeric::english()), - tz: TimeZone::localtime().unwrap(), - users: OSUsers::empty_cache(), - colours: colours, - current_year: LocalDateTime::now().year(), - } - } -} - -impl Table where U: Users { +impl<'a, U: Users+Groups+'a> Table<'a, U> { /// 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 @@ -429,8 +389,8 @@ impl Table 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.opts.colours.header, c.header())).collect()), + name: TextCell::paint_str(self.opts.colours.header, "Name"), last: false, }; @@ -446,7 +406,7 @@ impl Table where U: Users { let row = Row { depth: depth, cells: None, - name: Cell::paint(self.colours.broken_arrow, &error_message), + name: TextCell::paint(self.opts.colours.broken_arrow, error_message), last: last, }; @@ -457,19 +417,28 @@ impl Table 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.opts.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, file: &File, depth: usize, last: bool, links: bool) { + pub fn filename_cell(&self, file: File, links: bool) -> TextCell { + let width = DisplayWidth::from(&*file.name); + + TextCell { + contents: filename(file, &self.opts.colours, links), + width: width, + } + } + + pub fn add_file_with_cells(&mut self, cells: Vec, 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 +446,13 @@ impl Table 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 { + pub fn cells_for_file(&self, file: &File, xattrs: bool) -> Vec { 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(&self, file: &File, column: &Column, xattrs: bool) -> TextCell { use output::column::TimeType::*; match *column { @@ -501,158 +470,181 @@ impl Table where U: Users { } } - fn render_permissions(&self, permissions: f::Permissions, xattrs: bool) -> Cell { - let c = self.colours.perms; + fn render_permissions(&self, permissions: f::Permissions, xattrs: bool) -> TextCell { + let perms = self.opts.colours.perms; + let types = self.opts.colours.filetypes; + let bit = |bit, chr: &'static str, style: Style| { - if bit { style.paint(chr) } else { self.colours.punctuation.paint("-") } + if bit { style.paint(chr) } else { self.opts.colours.punctuation.paint("-") } }; let file_type = match permissions.file_type { - f::Type::File => self.colours.filetypes.normal.paint("."), - f::Type::Directory => self.colours.filetypes.directory.paint("d"), - f::Type::Pipe => self.colours.filetypes.special.paint("|"), - f::Type::Link => self.colours.filetypes.symlink.paint("l"), - f::Type::Special => self.colours.filetypes.special.paint("?"), + f::Type::File => types.normal.paint("."), + f::Type::Directory => types.directory.paint("d"), + f::Type::Pipe => types.special.paint("|"), + f::Type::Link => types.symlink.paint("l"), + f::Type::Special => types.special.paint("?"), }; - let x_colour = if let f::Type::File = permissions.file_type { c.user_execute_file } - else { c.user_execute_other }; + let x_colour = if let f::Type::File = permissions.file_type { perms.user_execute_file } + else { perms.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), + bit(permissions.user_read, "r", perms.user_read), + bit(permissions.user_write, "w", perms.user_write), bit(permissions.user_execute, "x", x_colour), - bit(permissions.group_read, "r", c.group_read), - bit(permissions.group_write, "w", c.group_write), - bit(permissions.group_execute, "x", c.group_execute), - bit(permissions.other_read, "r", c.other_read), - bit(permissions.other_write, "w", c.other_write), - bit(permissions.other_execute, "x", c.other_execute), + bit(permissions.group_read, "r", perms.group_read), + bit(permissions.group_write, "w", perms.group_write), + bit(permissions.group_execute, "x", perms.group_execute), + bit(permissions.other_read, "r", perms.other_read), + bit(permissions.other_write, "w", perms.other_write), + bit(permissions.other_execute, "x", perms.other_execute), ]; if xattrs { - columns.push(c.attribute.paint("@")); + chars.push(perms.attribute.paint("@")); } - Cell { - text: ANSIStrings(&columns).to_string(), - length: columns.len(), + // As these are all ASCII characters, we can guarantee that they’re + // all going to be one character wide, and don’t need to compute the + // cell’s display width. + let width = DisplayWidth::from(chars.len()); + + TextCell { + contents: chars.into(), + width: width, } } - fn render_links(&self, links: f::Links) -> Cell { - let style = if links.multiple { self.colours.links.multi_link_file } - else { self.colours.links.normal }; + fn render_links(&self, links: f::Links) -> TextCell { + let style = if links.multiple { self.opts.colours.links.multi_link_file } + else { self.opts.colours.links.normal }; - Cell::paint(style, &self.numeric.format_int(links.count)) + TextCell::paint(style, self.env.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.opts.colours.blocks, blk.to_string()), + f::Blocks::None => TextCell::blank(self.opts.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.opts.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.opts.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.env.numeric.format_int(size); + return TextCell::paint(self.opts.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.opts.colours.size.numbers, b.to_string()), + Prefixed(p, n) => (p, n) + }; + + let symbol = prefix.symbol(); + let number = if n < 10f64 { self.env.numeric.format_float(n, 1) } + else { self.env.numeric.format_int(n as isize) }; + + // The numbers and symbols are guaranteed to be written in ASCII, so + // we can skip the display width calculation. + let width = DisplayWidth::from(number.len() + symbol.len()); + + TextCell { + width: width, + contents: vec![ + self.opts.colours.size.numbers.paint(number), + self.opts.colours.size.unit.paint(symbol), + ].into(), } } #[allow(trivial_numeric_casts)] - fn render_time(&self, timestamp: f::Time) -> Cell { - let date = self.tz.at(LocalDateTime::at(timestamp.0 as i64)); + fn render_time(&self, timestamp: f::Time) -> TextCell { + let date = self.env.tz.at(LocalDateTime::at(timestamp.0 as i64)); - let datestamp = if date.year() == self.current_year { - DATE_AND_TIME.format(&date, &self.time) + let datestamp = if date.year() == self.env.current_year { + DATE_AND_TIME.format(&date, &self.env.time) } else { - DATE_AND_YEAR.format(&date, &self.time) + DATE_AND_YEAR.format(&date, &self.env.time) }; - Cell::paint(self.colours.date, &datestamp) + TextCell::paint(self.opts.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_status(&self, git: f::Git) -> TextCell { + let git_char = |status| match status { + f::GitStatus::NotModified => self.opts.colours.punctuation.paint("-"), + f::GitStatus::New => self.opts.colours.git.new.paint("N"), + f::GitStatus::Modified => self.opts.colours.git.modified.paint("M"), + f::GitStatus::Deleted => self.opts.colours.git.deleted.paint("D"), + f::GitStatus::Renamed => self.opts.colours.git.renamed.paint("R"), + f::GitStatus::TypeChange => self.opts.colours.git.typechange.paint("T"), + }; + + TextCell { + width: DisplayWidth::from(2), + contents: vec![ + git_char(git.staged), + git_char(git.unstaged) + ].into(), } } - fn render_git_char(&self, status: f::GitStatus) -> ANSIString { - 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"), - } - } + fn render_user(&self, user: f::User) -> TextCell { + let users = self.env.users.lock().unwrap(); - fn render_user(&mut self, user: f::User) -> Cell { - let user_name = match self.users.get_user_by_uid(user.0) { - Some(user) => user.name, + + let user_name = match users.get_user_by_uid(user.0) { + Some(user) => (*user.name).clone(), 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) + let style = if users.get_current_uid() == user.0 { self.opts.colours.users.user_you } + else { self.opts.colours.users.user_someone_else }; + TextCell::paint(style, user_name) } - fn render_group(&mut self, group: f::Group) -> Cell { - let mut style = self.colours.users.group_not_yours; + fn render_group(&self, group: f::Group) -> TextCell { + let mut style = self.opts.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(¤t_user.name) { - style = self.colours.users.group_yours; - } - } - group.name - }, - None => group.0.to_string(), + let users = self.env.users.lock().unwrap(); + let group = match users.get_group_by_gid(group.0) { + Some(g) => (*g).clone(), + None => return TextCell::paint(style, group.0.to_string()), }; - Cell::paint(style, &*group_name) + let current_uid = users.get_current_uid(); + if let Some(current_user) = users.get_user_by_uid(current_uid) { + if current_user.primary_group == group.gid + || group.members.contains(¤t_user.name) { + style = self.opts.colours.users.group_yours; + } + } + + TextCell::paint(style, (*group.name).clone()) } /// Render the table as a vector of Cells, to be displayed on standard output. - pub fn print_table(&self) -> Vec { - let mut stack = Vec::new(); + pub fn print_table(self) -> Vec { + let mut tree_trunk = TreeTrunk::default(); let mut cells = Vec::new(); // Work out the list of column widths by finding the longest cell for @@ -664,14 +656,16 @@ impl Table 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.width; - 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,42 +675,22 @@ impl Table 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 - // lines, as the output will change based on whether the - // *previous* entry was the last in its directory. - // TODO: Replace this by Vec::resize() when it becomes stable (1.5.0) - let stack_len = stack.len(); - if row.depth + 1 > stack_len { - stack.extend(repeat(TreePart::Edge).take(row.depth + 1 - stack_len)); - } else { - stack = stack[..(row.depth + 1)].into(); + for tree_part in tree_trunk.new_row(row.depth, row.last) { + filename.push(self.opts.colours.punctuation.paint(tree_part.ascii_art()), 4); } - 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; - } - - stack[row.depth] = if row.last { TreePart::Blank } else { TreePart::Line }; - // 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); } @@ -725,34 +699,6 @@ impl Table where U: Users { } -#[derive(PartialEq, Debug, Clone)] -enum TreePart { - - /// Rightmost column, *not* the last in the directory. - Edge, - - /// Not the rightmost column, and the directory has not finished yet. - Line, - - /// Rightmost column, and the last in the directory. - Corner, - - /// Not the rightmost column, and the directory *has* finished. - Blank, -} - -impl TreePart { - fn ascii_art(&self) -> &'static str { - match *self { - TreePart::Edge => "├──", - TreePart::Line => "│ ", - TreePart::Corner => "└──", - TreePart::Blank => " ", - } - } -} - - lazy_static! { static ref DATE_AND_TIME: DateFormat<'static> = DateFormat::parse("{2>:D} {:M} {2>:h}:{02>:m}").unwrap(); @@ -764,10 +710,13 @@ lazy_static! { #[cfg(test)] pub mod test { - pub use super::Table; + pub use super::{Table, Environment, Details}; + pub use std::sync::Mutex; + pub use file::File; pub use file::fields as f; - pub use output::column::{Cell, Column}; + pub use output::column::{Column, Columns}; + pub use output::cell::TextCell; pub use users::{User, Group, uid_t, gid_t}; pub use users::mock::MockUsers; @@ -775,82 +724,123 @@ pub mod test { pub use ansi_term::Style; pub use ansi_term::Colour::*; + impl Default for Environment { + fn default() -> Self { + use locale; + use datetime::zoned::TimeZone; + use users::mock::MockUsers; + use std::sync::Mutex; + + Environment { + current_year: 1234, + numeric: locale::Numeric::english(), + time: locale::Time::english(), + tz: TimeZone::localtime().unwrap(), + users: Mutex::new(MockUsers::with_current_uid(0)), + } + } + } + + pub fn new_table<'a>(columns: &'a [Column], details: &'a Details) -> Table<'a, MockUsers> { + use std::sync::Arc; + + Table { + columns: columns, + opts: details, + env: Arc::new(Environment::::default()), + rows: Vec::new(), + } + } + + pub fn newser(uid: uid_t, name: &str, group: gid_t) -> User { + use std::sync::Arc; + User { uid: uid, - name: name.to_string(), + name: Arc::new(name.to_string()), primary_group: group, home_dir: String::new(), shell: String::new(), } } - // These tests create a new, default Table object, then fill in the - // expected style in a certain way. This means we can check that the - // right style is being used, as otherwise, it would just be plain. - // - // Doing things with fields is way easier than having to fake the entire - // Metadata struct, which is what I was doing before! - mod users { #![allow(unused_results)] use super::*; + use std::sync::Arc; #[test] fn named() { - let mut table = Table::default(); - table.colours.users.user_you = Red.bold(); + let columns = Columns::default().for_dir(None); + let mut details = Details::default(); + details.colours.users.user_you = Red.bold(); + + let mut table = new_table(&columns, &details); let mut users = MockUsers::with_current_uid(1000); users.add_user(newser(1000, "enoch", 100)); - table.users = users; + Arc::get_mut(&mut table.env).unwrap().users = Mutex::new(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)) } #[test] fn unnamed() { - let mut table = Table::default(); - table.colours.users.user_you = Cyan.bold(); + let columns = Columns::default().for_dir(None); + let mut details = Details::default(); + details.colours.users.user_you = Cyan.bold(); + + let mut table = new_table(&columns, &details); let users = MockUsers::with_current_uid(1000); - table.users = users; + Arc::get_mut(&mut table.env).unwrap().users = Mutex::new(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)); } #[test] fn different_named() { - let mut table = Table::default(); - table.colours.users.user_someone_else = Green.bold(); - table.users.add_user(newser(1000, "enoch", 100)); + let columns = Columns::default().for_dir(None); + let mut details = Details::default(); + details.colours.users.user_someone_else = Green.bold(); + + let table = new_table(&columns, &details); + + table.env.users.lock().unwrap().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)); } #[test] fn different_unnamed() { - let mut table = Table::default(); - table.colours.users.user_someone_else = Red.normal(); + let columns = Columns::default().for_dir(None); + let mut details = Details::default(); + details.colours.users.user_someone_else = Red.normal(); + + let table = new_table(&columns, &details); 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)); } #[test] fn overflow() { - let mut table = Table::default(); - table.colours.users.user_someone_else = Blue.underline(); + let columns = Columns::default().for_dir(None); + let mut details = Details::default(); + details.colours.users.user_someone_else = Blue.underline(); + + let table = new_table(&columns, &details); 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)); } } @@ -858,71 +848,87 @@ pub mod test { mod groups { #![allow(unused_results)] use super::*; + use std::sync::Arc; #[test] fn named() { - let mut table = Table::default(); - table.colours.users.group_not_yours = Fixed(101).normal(); + let columns = Columns::default().for_dir(None); + let mut details = Details::default(); + details.colours.users.group_not_yours = Fixed(101).normal(); + + let mut table = new_table(&columns, &details); let mut users = MockUsers::with_current_uid(1000); - users.add_group(Group { gid: 100, name: "folk".to_string(), members: vec![] }); - table.users = users; + users.add_group(Group { gid: 100, name: Arc::new("folk".to_string()), members: vec![] }); + Arc::get_mut(&mut table.env).unwrap().users = Mutex::new(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)) } #[test] fn unnamed() { - let mut table = Table::default(); - table.colours.users.group_not_yours = Fixed(87).normal(); + let columns = Columns::default().for_dir(None); + let mut details = Details::default(); + details.colours.users.group_not_yours = Fixed(87).normal(); + + let mut table = new_table(&columns, &details); let users = MockUsers::with_current_uid(1000); - table.users = users; + Arc::get_mut(&mut table.env).unwrap().users = Mutex::new(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)); } #[test] fn primary() { - let mut table = Table::default(); - table.colours.users.group_yours = Fixed(64).normal(); + let columns = Columns::default().for_dir(None); + let mut details = Details::default(); + details.colours.users.group_yours = Fixed(64).normal(); + + let mut table = new_table(&columns, &details); let mut users = MockUsers::with_current_uid(2); users.add_user(newser(2, "eve", 100)); - users.add_group(Group { gid: 100, name: "folk".to_string(), members: vec![] }); - table.users = users; + users.add_group(Group { gid: 100, name: Arc::new("folk".to_string()), members: vec![] }); + Arc::get_mut(&mut table.env).unwrap().users = Mutex::new(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)) } #[test] fn secondary() { - let mut table = Table::default(); - table.colours.users.group_yours = Fixed(31).normal(); + let columns = Columns::default().for_dir(None); + let mut details = Details::default(); + details.colours.users.group_yours = Fixed(31).normal(); + + let mut table = new_table(&columns, &details); let mut users = MockUsers::with_current_uid(2); users.add_user(newser(2, "eve", 666)); - users.add_group(Group { gid: 100, name: "folk".to_string(), members: vec![ "eve".to_string() ] }); - table.users = users; + users.add_group(Group { gid: 100, name: Arc::new("folk".to_string()), members: vec![ "eve".to_string() ] }); + Arc::get_mut(&mut table.env).unwrap().users = Mutex::new(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)) } #[test] fn overflow() { - let mut table = Table::default(); - table.colours.users.group_not_yours = Blue.underline(); + let columns = Columns::default().for_dir(None); + let mut details = Details::default(); + details.colours.users.group_not_yours = Blue.underline(); + + let table = new_table(&columns, &details); 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)); } } diff --git a/src/output/grid.rs b/src/output/grid.rs index 13d2f8d..f944fbb 100644 --- a/src/output/grid.rs +++ b/src/output/grid.rs @@ -1,9 +1,10 @@ -use colours::Colours; -use file::File; -use filetype::file_colour; - use term_grid as grid; +use file::File; +use output::DisplayWidth; +use output::colours::Colours; +use super::file_colour; + #[derive(PartialEq, Debug, Copy, Clone)] pub struct Grid { @@ -27,7 +28,7 @@ impl Grid { for file in files.iter() { grid.add(grid::Cell { contents: file_colour(&self.colours, file).paint(&*file.name).to_string(), - width: file.file_name_width(), + width: *DisplayWidth::from(&*file.name), }); } diff --git a/src/output/grid_details.rs b/src/output/grid_details.rs index e482464..d3cda3e 100644 --- a/src/output/grid_details.rs +++ b/src/output/grid_details.rs @@ -1,5 +1,6 @@ -use std::iter::repeat; +use std::sync::Arc; +use ansi_term::ANSIStrings; use users::OSUsers; use term_grid as grid; @@ -7,8 +8,9 @@ use dir::Dir; use feature::xattr::FileAttributes; use file::File; -use output::column::{Column, Cell}; -use output::details::{Details, Table}; +use output::cell::TextCell; +use output::column::Column; +use output::details::{Details, Table, Environment}; use output::grid::Grid; #[derive(PartialEq, Debug, Copy, Clone)] @@ -25,19 +27,33 @@ 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) { 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 env = Arc::new(Environment::default()); - let mut last_working_table = self.make_grid(1, &*columns_for_dir, files, cells.clone()); + let (cells, file_names) = { + + let first_table = self.make_table(env.clone(), &*columns_for_dir); + + let cells = files.iter() + .map(|file| first_table.cells_for_file(file, file_has_xattrs(file))) + .collect::>(); + + let file_names = files.into_iter() + .map(|file| first_table.filename_cell(file, false)) + .collect::>(); + + (cells, file_names) + }; + + let mut last_working_table = self.make_grid(env.clone(), 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(env.clone(), column_count, &columns_for_dir, &file_names, cells.clone()); let the_grid_fits = { let d = grid.fit_into_columns(column_count); @@ -54,14 +70,24 @@ impl GridDetails { } } - fn make_table(&self, columns_for_dir: &[Column]) -> Table { - let mut table = Table::with_options(self.details.colours, columns_for_dir.into()); + fn make_table<'a>(&'a self, env: Arc>, columns_for_dir: &'a [Column]) -> Table { + let mut table = Table { + columns: columns_for_dir, + opts: &self.details, + env: env, + + rows: Vec::new(), + }; + if self.details.header { table.add_header() } table } - fn make_grid(&self, column_count: usize, columns_for_dir: &[Column], files: &[File], cells: Vec>) -> grid::Grid { - let mut tables: Vec<_> = repeat(()).map(|_| self.make_table(columns_for_dir)).take(column_count).collect(); + fn make_grid<'a>(&'a self, env: Arc>, column_count: usize, columns_for_dir: &'a [Column], file_names: &[TextCell], cells: Vec>) -> grid::Grid { + let mut tables = Vec::new(); + for _ in 0 .. column_count { + tables.push(self.make_table(env.clone(), columns_for_dir)); + } let mut num_cells = cells.len(); if self.details.header { @@ -71,7 +97,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 +105,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,8 +123,8 @@ impl GridDetails { for column in columns.iter() { if row < column.len() { let cell = grid::Cell { - contents: column[row].text.clone(), - width: column[row].length, + contents: ANSIStrings(&column[row].contents).to_string(), + width: *column[row].width, }; grid.add(cell); @@ -110,8 +136,8 @@ impl GridDetails { for column in columns.iter() { for cell in column.iter() { let cell = grid::Cell { - contents: cell.text.clone(), - width: cell.length, + contents: ANSIStrings(&cell.contents).to_string(), + width: *cell.width, }; grid.add(cell); diff --git a/src/output/lines.rs b/src/output/lines.rs index 7f19105..07c4351 100644 --- a/src/output/lines.rs +++ b/src/output/lines.rs @@ -1,7 +1,9 @@ -use colours::Colours; +use ansi_term::ANSIStrings; + use file::File; use super::filename; +use super::colours::Colours; #[derive(Clone, Copy, Debug, PartialEq)] @@ -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) { for file in files { - println!("{}", filename(file, &self.colours, true)); + println!("{}", ANSIStrings(&filename(file, &self.colours, true))); } } } diff --git a/src/output/mod.rs b/src/output/mod.rs index c875cde..b5bc422 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -1,42 +1,73 @@ -use ansi_term::ANSIStrings; +use ansi_term::Style; -use colours::Colours; use file::File; -use filetype::file_colour; +pub use self::cell::{TextCell, TextCellContents, DisplayWidth}; +pub use self::colours::Colours; pub use self::details::Details; +pub use self::grid_details::GridDetails; 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 mod column; +mod cell; +mod colours; +mod tree; - -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) + ].into() } } -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) + ].into(), - 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), + ].into(), } } + +pub fn file_colour(colours: &Colours, file: &File) -> Style { + use filetype::FileTypes; + + match file { + f if f.is_directory() => colours.filetypes.directory, + f if f.is_executable_file() => colours.filetypes.executable, + f if f.is_link() => colours.filetypes.symlink, + f if !f.is_file() => colours.filetypes.special, + f if f.is_immediate() => colours.filetypes.immediate, + f if f.is_image() => colours.filetypes.image, + f if f.is_video() => colours.filetypes.video, + f if f.is_music() => colours.filetypes.music, + f if f.is_lossless() => colours.filetypes.lossless, + f if f.is_crypto() => colours.filetypes.crypto, + f if f.is_document() => colours.filetypes.document, + f if f.is_compressed() => colours.filetypes.compressed, + f if f.is_temp() => colours.filetypes.temp, + f if f.is_compiled() => colours.filetypes.compiled, + _ => colours.filetypes.normal, + } +} \ No newline at end of file diff --git a/src/output/tree.rs b/src/output/tree.rs new file mode 100644 index 0000000..1cac117 --- /dev/null +++ b/src/output/tree.rs @@ -0,0 +1,175 @@ +//! Tree structures, such as `├──` or `└──`, used in a tree view. +//! +//! ## Constructing Tree Views +//! +//! When using the `--tree` argument, instead of a vector of cells, each row +//! has a `depth` field that indicates how far deep in the tree it is: the top +//! level has depth 0, its children have depth 1, and *their* children have +//! depth 2, and so on. +//! +//! On top of this, it also has a `last` field that specifies whether this is +//! the last row of this particular consecutive set of rows. This doesn’t +//! affect the file’s information; it’s just used to display a different set of +//! Unicode tree characters! The resulting table looks like this: +//! +//! ┌───────┬───────┬───────────────────────┐ +//! │ Depth │ Last │ Output │ +//! ├───────┼───────┼───────────────────────┤ +//! │ 0 │ │ documents │ +//! │ 1 │ false │ ├── this_file.txt │ +//! │ 1 │ false │ ├── that_file.txt │ +//! │ 1 │ false │ ├── features │ +//! │ 2 │ false │ │ ├── feature_1.rs │ +//! │ 2 │ false │ │ ├── feature_2.rs │ +//! │ 2 │ true │ │ └── feature_3.rs │ +//! │ 1 │ true │ └── pictures │ +//! │ 2 │ false │ ├── garden.jpg │ +//! │ 2 │ false │ ├── flowers.jpg │ +//! │ 2 │ false │ ├── library.png │ +//! │ 2 │ true │ └── space.tiff │ +//! └───────┴───────┴───────────────────────┘ +//! +//! Creating the table like this means that each file has to be tested to see +//! if it’s the last one in the group. This is usually done by putting all the +//! files in a vector beforehand, getting its length, then comparing the index +//! of each file to see if it’s the last one. (As some files may not be +//! successfully `stat`ted, we don’t know how many files are going to exist in +//! each directory) + +#[derive(PartialEq, Debug, Clone)] +pub enum TreePart { + + /// Rightmost column, *not* the last in the directory. + Edge, + + /// Not the rightmost column, and the directory has not finished yet. + Line, + + /// Rightmost column, and the last in the directory. + Corner, + + /// Not the rightmost column, and the directory *has* finished. + Blank, +} + +impl TreePart { + + /// Turn this tree part into ASCII-licious box drawing characters! + /// (Warning: not actually ASCII) + pub fn ascii_art(&self) -> &'static str { + match *self { + TreePart::Edge => "├──", + TreePart::Line => "│ ", + TreePart::Corner => "└──", + TreePart::Blank => " ", + } + } +} + + +/// A **tree trunk** builds up arrays of tree parts over multiple depths. +#[derive(Debug, Default)] +pub struct TreeTrunk { + + /// 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 any previous entries. + stack: Vec, + + /// A tuple for the last ‘depth’ and ‘last’ parameters that are passed in. + last_params: Option<(usize, bool)>, +} + +impl TreeTrunk { + + /// Calculates the tree parts for an entry at the given depth and + /// last-ness. The depth is used to determine where in the stack the tree + /// part should be inserted, and the last-ness is used to determine which + /// type of tree part to insert. + /// + /// This takes a `&mut self` because the results of each file are stored + /// and used in future rows. + pub fn new_row(&mut self, depth: usize, last: bool) -> &[TreePart] { + + // If this isn’t our first iteration, then update the tree parts thus + // far to account for there being another row after it. + if let Some((last_depth, last_last)) = self.last_params { + self.stack[last_depth] = if last_last { TreePart::Blank } else { TreePart::Line }; + } + + // Make sure the stack has enough space, then add or modify another + // part into it. + self.stack.resize(depth + 1, TreePart::Edge); + self.stack[depth] = if last { TreePart::Corner } else { TreePart::Edge }; + self.last_params = Some((depth, last)); + + // Return the tree parts as a slice of the stack. + // + // Ignoring the first component is specific to exa: when a user prints + // a tree view for multiple directories, we don’t want there to be a + // ‘zeroth level’ connecting the initial directories. Otherwise, not + // only are unrelated directories seemingly connected to each other, + // but the tree part of the first row doesn’t connect to anything: + // + // with [0..] with [1..] + // ========== ========== + // ├──folder folder + // │ └──file └──file + // └──folder folder + // └──file └──file + &self.stack[1..] + } +} + + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn empty_at_first() { + let mut tt = TreeTrunk::default(); + assert_eq!(tt.new_row(0, true), &[]); + } + + #[test] + fn one_child() { + let mut tt = TreeTrunk::default(); + assert_eq!(tt.new_row(0, true), &[]); + assert_eq!(tt.new_row(1, true), &[ TreePart::Corner ]); + } + + #[test] + fn two_children() { + let mut tt = TreeTrunk::default(); + assert_eq!(tt.new_row(0, true), &[]); + assert_eq!(tt.new_row(1, false), &[ TreePart::Edge ]); + assert_eq!(tt.new_row(1, true), &[ TreePart::Corner ]); + } + + #[test] + fn two_times_two_children() { + let mut tt = TreeTrunk::default(); + assert_eq!(tt.new_row(0, false), &[]); + assert_eq!(tt.new_row(1, false), &[ TreePart::Edge ]); + assert_eq!(tt.new_row(1, true), &[ TreePart::Corner ]); + + assert_eq!(tt.new_row(0, true), &[]); + assert_eq!(tt.new_row(1, false), &[ TreePart::Edge ]); + assert_eq!(tt.new_row(1, true), &[ TreePart::Corner ]); + } + + #[test] + fn two_times_two_nested_children() { + let mut tt = TreeTrunk::default(); + assert_eq!(tt.new_row(0, true), &[]); + + assert_eq!(tt.new_row(1, false), &[ TreePart::Edge ]); + assert_eq!(tt.new_row(2, false), &[ TreePart::Line, TreePart::Edge ]); + assert_eq!(tt.new_row(2, true), &[ TreePart::Line, TreePart::Corner ]); + + assert_eq!(tt.new_row(1, true), &[ TreePart::Corner ]); + assert_eq!(tt.new_row(2, false), &[ TreePart::Blank, TreePart::Edge ]); + assert_eq!(tt.new_row(2, true), &[ TreePart::Blank, TreePart::Corner ]); + } +}