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

Merge branch 'split-up-details'

Ben S 10 лет назад
Родитель
Сommit
e1d2c3f46e
15 измененных файлов с 909 добавлено и 518 удалено
  1. 55 36
      Cargo.lock
  2. 1 1
      Cargo.toml
  3. 1 12
      src/file.rs
  4. 1 25
      src/filetype.rs
  5. 2 3
      src/main.rs
  6. 1 1
      src/options.rs
  7. 205 0
      src/output/cell.rs
  8. 0 0
      src/output/colours.rs
  9. 0 38
      src/output/column.rs
  10. 362 356
      src/output/details.rs
  11. 6 5
      src/output/grid.rs
  12. 45 19
      src/output/grid_details.rs
  13. 5 3
      src/output/lines.rs
  14. 50 19
      src/output/mod.rs
  15. 175 0
      src/output/tree.rs

+ 55 - 36
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)",
 ]
 

+ 1 - 1
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"

+ 1 - 12
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<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()),

+ 1 - 25
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;

+ 2 - 3
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),
         }
     }
 }

+ 1 - 1
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;
 

+ 205 - 0
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<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.
+///
+/// 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<ANSIString<'static>>);
+
+impl From<Vec<ANSIString<'static>>> for TextCellContents {
+    fn from(strings: Vec<ANSIString<'static>>) -> 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<usize> 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
+    }
+}

+ 0 - 0
src/colours.rs → src/output/colours.rs


+ 0 - 38
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);
-    }
-}

+ 362 - 356
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 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::Style;
 
-use ansi_term::{ANSIString, ANSIStrings, Style};
-
-use datetime::local::{LocalDateTime, DatePiece};
 use datetime::format::DateFormat;
+use datetime::local::{LocalDateTime, DatePiece};
 use datetime::zoned::TimeZone;
 
 use locale;
 
-use users::{OSUsers, Users};
-use users::mock::MockUsers;
+use users::{OSUsers, Users, Groups};
 
+use dir::Dir;
+use feature::xattr::{Attribute, FileAttributes};
+use file::fields as f;
+use file::File;
+use options::{FileFilter, RecurseOptions};
+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<U: Users+Groups> {
+
+    /// 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<U>,
+}
+
+impl Default for Environment<OSUsers> {
+    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::<OSUsers>::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<U>, src: Vec<File<'dir>>, depth: usize) {
+    fn add_files_to_table<'dir, U: Users+Groups+Send>(&self, mut table: &mut Table<U>, src: Vec<File<'dir>>, 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<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));
+            let table = Arc::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();
 
                 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<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.
@@ -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<U> {
-    columns:  Vec<Column>,
-    rows:     Vec<Row>,
-
-    time:         locale::Time,
-    numeric:      locale::Numeric,
-    tz:           TimeZone,
-    users:        U,
-    colours:      Colours,
-    current_year: i64,
-}
+pub struct Table<'a, U: Users+Groups+'a> {
+    pub rows: Vec<Row>,
 
-impl Default for Table<MockUsers> {
-    fn default() -> Table<MockUsers> {
-        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,
-        }
-    }
+    pub columns: &'a [Column],
+    pub opts: &'a Details,
+    pub env: Arc<Environment<U>>,
 }
 
-impl Table<OSUsers> {
-
-    /// Create a new, empty Table object, setting the caching fields to their
-    /// empty states.
-    pub fn with_options(colours: Colours, columns: Vec<Column>) -> Table<OSUsers> {
-        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<U> Table<U> 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<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.opts.colours.header, c.header())).collect()),
+            name:     TextCell::paint_str(self.opts.colours.header, "Name"),
             last:     false,
         };
 
@@ -446,7 +406,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.opts.colours.broken_arrow, error_message),
             last:     last,
         };
 
@@ -457,19 +417,28 @@ 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.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<Cell>, 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<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 +446,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(&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(&self, file: &File, column: &Column, xattrs: bool) -> TextCell {
         use output::column::TimeType::*;
 
         match *column {
@@ -501,158 +470,181 @@ impl<U> Table<U> 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"),
+        };
 
-    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"),
+        TextCell {
+            width: DisplayWidth::from(2),
+            contents: vec![
+                git_char(git.staged),
+                git_char(git.unstaged)
+            ].into(),
         }
     }
 
-    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,
+    fn render_user(&self, user: f::User) -> TextCell {
+        let users = self.env.users.lock().unwrap();
+
+
+        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(&current_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(&current_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<Cell> {
-        let mut stack = Vec::new();
+    pub fn print_table(self) -> Vec<TextCell> {
+        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<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.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<U> Table<U> where U: Users {
                 cell.add_spaces(total_width)
             }
 
-            let mut filename = String::new();
-            let mut filename_length = 0;
-
-            // A stack tracks which tree characters should be printed. It's
-            // necessary to maintain information about the previously-printed
-            // lines, as the output will change based on whether the
-            // *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();
-            }
-
-            stack[row.depth] = if row.last { TreePart::Corner } else { TreePart::Edge };
+            let mut filename = TextCell::default();
 
-            for i in 1 .. row.depth + 1 {
-                filename.push_str(&*self.colours.punctuation.paint(stack[i].ascii_art()).to_string());
-                filename_length += 4;
+            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::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<U> Table<U> 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<MockUsers> {
+        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::<MockUsers>::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));
         }
     }

+ 6 - 5
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),
             });
         }
 

+ 45 - 19
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<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 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::<Vec<_>>();
+
+            let file_names = files.into_iter()
+                                  .map(|file| first_table.filename_cell(file, false))
+                                  .collect::<Vec<_>>();
+
+            (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<OSUsers> {
-        let mut table = Table::with_options(self.details.colours, columns_for_dir.into());
+    fn make_table<'a>(&'a self, env: Arc<Environment<OSUsers>>, columns_for_dir: &'a [Column]) -> Table<OSUsers> {
+        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<Vec<Cell>>) -> 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<Environment<OSUsers>>, column_count: usize, columns_for_dir: &'a [Column], file_names: &[TextCell], cells: Vec<Vec<TextCell>>) -> 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);

+ 5 - 3
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<File>) {
         for file in files {
-            println!("{}", filename(file, &self.colours, true));
+            println!("{}", ANSIStrings(&filename(file, &self.colours, true)));
         }
     }
 }

+ 50 - 19
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) ])),
-
-        Err(filename) => format!("{} {} {}",
-                                 file_colour(colours, file).paint(&*file.name),
-                                 colours.broken_arrow.paint("->"),
-                                 colours.broken_filename.paint(filename)),
+        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) => 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,
+    }
+}

+ 175 - 0
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<TreePart>,
+
+    /// 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 ]);
+    }
+}