Explorar o código

Move many Options structs to the output module

This cleans up the options module, moving the structs that were *only* in use for the columns view out of it.

The new OptionSet trait is used to add the ‘deduce’ methods that used to be present on the values.
Ben S %!s(int64=10) %!d(string=hai) anos
pai
achega
10468797bb
Modificáronse 8 ficheiros con 431 adicións e 366 borrados
  1. 0 92
      src/column.rs
  2. 1 1
      src/file.rs
  3. 2 3
      src/main.rs
  4. 188 263
      src/options.rs
  5. 231 0
      src/output/column.rs
  6. 5 6
      src/output/details.rs
  7. 2 1
      src/output/grid_details.rs
  8. 2 0
      src/output/mod.rs

+ 0 - 92
src/column.rs

@@ -1,92 +0,0 @@
-use ansi_term::Style;
-use unicode_width::UnicodeWidthStr;
-
-use options::{SizeFormat, TimeType};
-
-
-#[derive(PartialEq, Debug, Copy, Clone)]
-pub enum Column {
-    Permissions,
-    FileSize(SizeFormat),
-    Timestamp(TimeType),
-    Blocks,
-    User,
-    Group,
-    HardLinks,
-    Inode,
-
-    GitStatus,
-}
-
-/// Each column can pick its own **Alignment**. Usually, numbers are
-/// right-aligned, and text is left-aligned.
-#[derive(Copy, Clone)]
-pub enum Alignment {
-    Left, Right,
-}
-
-impl Column {
-
-    /// Get the alignment this column should use.
-    pub fn alignment(&self) -> Alignment {
-        match *self {
-            Column::FileSize(_) => Alignment::Right,
-            Column::HardLinks   => Alignment::Right,
-            Column::Inode       => Alignment::Right,
-            Column::Blocks      => Alignment::Right,
-            Column::GitStatus   => Alignment::Right,
-            _                   => Alignment::Left,
-        }
-    }
-
-    /// Get the text that should be printed at the top, when the user elects
-    /// to have a header row printed.
-    pub fn header(&self) -> &'static str {
-        match *self {
-            Column::Permissions   => "Permissions",
-            Column::FileSize(_)   => "Size",
-            Column::Timestamp(t)  => t.header(),
-            Column::Blocks        => "Blocks",
-            Column::User          => "User",
-            Column::Group         => "Group",
-            Column::HardLinks     => "Links",
-            Column::Inode         => "inode",
-            Column::GitStatus     => "Git",
-        }
-    }
-}
-
-
-#[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);
-    }
-}

+ 1 - 1
src/file.rs

@@ -10,7 +10,7 @@ use std::path::{Component, Path, PathBuf};
 use unicode_width::UnicodeWidthStr;
 
 use dir::Dir;
-use options::TimeType;
+use output::column::TimeType;
 
 use self::fields as f;
 

+ 2 - 3
src/main.rs

@@ -28,7 +28,6 @@ use file::File;
 use options::{Options, View};
 
 mod colours;
-mod column;
 mod dir;
 mod feature;
 mod file;
@@ -99,8 +98,8 @@ impl Exa {
                 }
             };
 
-            self.options.filter_files(&mut children);
-            self.options.sort_files(&mut children);
+            self.options.filter.filter_files(&mut children);
+            self.options.filter.sort_files(&mut children);
 
             if let Some(recurse_opts) = self.options.dir_action.recurse_options() {
                 let depth = dir.path.components().filter(|&c| c != Component::CurDir).count() + 1;

+ 188 - 263
src/options.rs

@@ -7,21 +7,26 @@ use getopts;
 use natord;
 
 use colours::Colours;
-use column::Column;
-use column::Column::*;
-use dir::Dir;
 use feature::xattr;
 use file::File;
 use output::{Grid, Details, GridDetails, Lines};
+use output::column::{Columns, TimeTypes, SizeFormat};
 use term::dimensions;
 
 
-/// The *Options* struct represents a parsed version of the user's
-/// command-line options.
+/// These **options** represent a parsed, error-checked versions of the
+/// user's command-line options.
 #[derive(PartialEq, Debug, Copy, Clone)]
 pub struct Options {
+
+    /// The action to perform when encountering a directory rather than a
+    /// regular file.
     pub dir_action: DirAction,
+
+    /// How to sort and filter files before outputting them.
     pub filter: FileFilter,
+
+    /// The type of output to use (lines, grid, or details).
     pub view: View,
 }
 
@@ -107,14 +112,6 @@ impl Options {
         }, path_strs))
     }
 
-    pub fn sort_files(&self, files: &mut Vec<File>) {
-        self.filter.sort_files(files)
-    }
-
-    pub fn filter_files(&self, files: &mut Vec<File>) {
-        self.filter.filter_files(files)
-    }
-
     /// Whether the View specified in this set of options includes a Git
     /// status column. It's only worth trying to discover a repository if the
     /// results will end up being displayed.
@@ -128,143 +125,6 @@ impl Options {
 }
 
 
-#[derive(Default, PartialEq, Debug, Copy, Clone)]
-pub struct FileFilter {
-    list_dirs_first: bool,
-    reverse: bool,
-    show_invisibles: bool,
-    sort_field: SortField,
-}
-
-impl FileFilter {
-    pub fn filter_files(&self, files: &mut Vec<File>) {
-        if !self.show_invisibles {
-            files.retain(|f| !f.is_dotfile());
-        }
-    }
-
-    pub fn sort_files(&self, files: &mut Vec<File>) {
-        files.sort_by(|a, b| self.compare_files(a, b));
-
-        if self.reverse {
-            files.reverse();
-        }
-
-        if self.list_dirs_first {
-            // This relies on the fact that sort_by is stable.
-            files.sort_by(|a, b| b.is_directory().cmp(&a.is_directory()));
-        }
-    }
-
-    pub fn compare_files(&self, a: &File, b: &File) -> cmp::Ordering {
-        match self.sort_field {
-            SortField::Unsorted      => cmp::Ordering::Equal,
-            SortField::Name          => natord::compare(&*a.name, &*b.name),
-            SortField::Size          => a.metadata.len().cmp(&b.metadata.len()),
-            SortField::FileInode     => a.metadata.ino().cmp(&b.metadata.ino()),
-            SortField::ModifiedDate  => a.metadata.mtime().cmp(&b.metadata.mtime()),
-            SortField::AccessedDate  => a.metadata.atime().cmp(&b.metadata.atime()),
-            SortField::CreatedDate   => a.metadata.ctime().cmp(&b.metadata.ctime()),
-            SortField::Extension     => match a.ext.cmp(&b.ext) {
-                cmp::Ordering::Equal  => natord::compare(&*a.name, &*b.name),
-                order                 => order,
-            },
-        }
-    }
-}
-
-/// User-supplied field to sort by.
-#[derive(PartialEq, Debug, Copy, Clone)]
-pub enum SortField {
-    Unsorted, Name, Extension, Size, FileInode,
-    ModifiedDate, AccessedDate, CreatedDate,
-}
-
-impl Default for SortField {
-    fn default() -> SortField {
-        SortField::Name
-    }
-}
-
-impl SortField {
-
-    /// Find which field to use based on a user-supplied word.
-    fn from_word(word: String) -> Result<SortField, Misfire> {
-        match &word[..] {
-            "name" | "filename"   => Ok(SortField::Name),
-            "size" | "filesize"   => Ok(SortField::Size),
-            "ext"  | "extension"  => Ok(SortField::Extension),
-            "mod"  | "modified"   => Ok(SortField::ModifiedDate),
-            "acc"  | "accessed"   => Ok(SortField::AccessedDate),
-            "cr"   | "created"    => Ok(SortField::CreatedDate),
-            "none"                => Ok(SortField::Unsorted),
-            "inode"               => Ok(SortField::FileInode),
-            field                 => Err(SortField::none(field))
-        }
-    }
-
-    /// How to display an error when the word didn't match with anything.
-    fn none(field: &str) -> Misfire {
-        Misfire::InvalidOptions(getopts::Fail::UnrecognizedOption(format!("--sort {}", field)))
-    }
-}
-
-
-/// One of these things could happen instead of listing files.
-#[derive(PartialEq, Debug)]
-pub enum Misfire {
-
-    /// The getopts crate didn't like these arguments.
-    InvalidOptions(getopts::Fail),
-
-    /// The user asked for help. This isn't strictly an error, which is why
-    /// this enum isn't named Error!
-    Help(String),
-
-    /// The user wanted the version number.
-    Version,
-
-    /// Two options were given that conflict with one another.
-    Conflict(&'static str, &'static str),
-
-    /// An option was given that does nothing when another one either is or
-    /// isn't present.
-    Useless(&'static str, bool, &'static str),
-
-    /// An option was given that does nothing when either of two other options
-    /// are not present.
-    Useless2(&'static str, &'static str, &'static str),
-
-    /// A numeric option was given that failed to be parsed as a number.
-    FailedParse(ParseIntError),
-}
-
-impl Misfire {
-    /// The OS return code this misfire should signify.
-    pub fn error_code(&self) -> i32 {
-        if let Misfire::Help(_) = *self { 2 }
-                                   else { 3 }
-    }
-}
-
-impl fmt::Display for Misfire {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        use self::Misfire::*;
-
-        match *self {
-            InvalidOptions(ref e)  => write!(f, "{}", e),
-            Help(ref text)         => write!(f, "{}", text),
-            Version                => write!(f, "exa {}", env!("CARGO_PKG_VERSION")),
-            Conflict(a, b)         => write!(f, "Option --{} conflicts with option {}.", a, b),
-            Useless(a, false, b)   => write!(f, "Option --{} is useless without option --{}.", a, b),
-            Useless(a, true, b)    => write!(f, "Option --{} is useless given option --{}.", a, b),
-            Useless2(a, b1, b2)    => write!(f, "Option --{} is useless without options --{} or --{}.", a, b1, b2),
-            FailedParse(ref e)     => write!(f, "Failed to parse number: {}", e),
-        }
-    }
-}
-
-
 #[derive(PartialEq, Debug, Copy, Clone)]
 pub enum View {
     Details(Details),
@@ -274,7 +134,7 @@ pub enum View {
 }
 
 impl View {
-    pub fn deduce(matches: &getopts::Matches, filter: FileFilter, dir_action: DirAction) -> Result<View, Misfire> {
+    fn deduce(matches: &getopts::Matches, filter: FileFilter, dir_action: DirAction) -> Result<View, Misfire> {
         use self::Misfire::*;
 
         let long = || {
@@ -356,8 +216,8 @@ impl View {
                 }
             }
             else {
-                // If the terminal width couldn't be matched for some reason, such
-                // as the program's stdout being connected to a file, then
+                // If the terminal width couldnt be matched for some reason, such
+                // as the programs stdout being connected to a file, then
                 // fallback to the lines view.
                 let lines = Lines {
                      colours: Colours::plain(),
@@ -389,68 +249,160 @@ impl View {
 }
 
 
-#[derive(PartialEq, Debug, Copy, Clone)]
-pub enum SizeFormat {
-    DecimalBytes,
-    BinaryBytes,
-    JustBytes,
+trait OptionSet: Sized {
+    fn deduce(matches: &getopts::Matches) -> Result<Self, Misfire>;
 }
 
-impl Default for SizeFormat {
-    fn default() -> SizeFormat {
-        SizeFormat::DecimalBytes
+impl OptionSet for Columns {
+    fn deduce(matches: &getopts::Matches) -> Result<Columns, Misfire> {
+        Ok(Columns {
+            size_format: try!(SizeFormat::deduce(matches)),
+            time_types:  try!(TimeTypes::deduce(matches)),
+            inode:  matches.opt_present("inode"),
+            links:  matches.opt_present("links"),
+            blocks: matches.opt_present("blocks"),
+            group:  matches.opt_present("group"),
+            git:    cfg!(feature="git") && matches.opt_present("git"),
+        })
     }
 }
 
-impl SizeFormat {
-    pub fn deduce(matches: &getopts::Matches) -> Result<SizeFormat, Misfire> {
-        let binary = matches.opt_present("binary");
-        let bytes  = matches.opt_present("bytes");
 
-        match (binary, bytes) {
-            (true,  true )  => Err(Misfire::Conflict("binary", "bytes")),
-            (true,  false)  => Ok(SizeFormat::BinaryBytes),
-            (false, true )  => Ok(SizeFormat::JustBytes),
-            (false, false)  => Ok(SizeFormat::DecimalBytes),
+/// The **file filter** processes a vector of files before outputting them,
+/// filtering and sorting the files depending on the user’s command-line
+/// flags.
+#[derive(Default, PartialEq, Debug, Copy, Clone)]
+pub struct FileFilter {
+    list_dirs_first: bool,
+    reverse: bool,
+    show_invisibles: bool,
+    sort_field: SortField,
+}
+
+impl FileFilter {
+
+    /// Remove every file in the given vector that does *not* pass the
+    /// filter predicate.
+    pub fn filter_files(&self, files: &mut Vec<File>) {
+        if !self.show_invisibles {
+            files.retain(|f| !f.is_dotfile());
+        }
+    }
+
+    /// Sort the files in the given vector based on the sort field option.
+    pub fn sort_files(&self, files: &mut Vec<File>) {
+        files.sort_by(|a, b| self.compare_files(a, b));
+
+        if self.reverse {
+            files.reverse();
+        }
+
+        if self.list_dirs_first {
+            // This relies on the fact that `sort_by` is stable.
+            files.sort_by(|a, b| b.is_directory().cmp(&a.is_directory()));
+        }
+    }
+
+    pub fn compare_files(&self, a: &File, b: &File) -> cmp::Ordering {
+        match self.sort_field {
+            SortField::Unsorted      => cmp::Ordering::Equal,
+            SortField::Name          => natord::compare(&*a.name, &*b.name),
+            SortField::Size          => a.metadata.len().cmp(&b.metadata.len()),
+            SortField::FileInode     => a.metadata.ino().cmp(&b.metadata.ino()),
+            SortField::ModifiedDate  => a.metadata.mtime().cmp(&b.metadata.mtime()),
+            SortField::AccessedDate  => a.metadata.atime().cmp(&b.metadata.atime()),
+            SortField::CreatedDate   => a.metadata.ctime().cmp(&b.metadata.ctime()),
+            SortField::Extension     => match a.ext.cmp(&b.ext) {
+                cmp::Ordering::Equal  => natord::compare(&*a.name, &*b.name),
+                order                 => order,
+            },
         }
     }
 }
 
 
+/// User-supplied field to sort by.
 #[derive(PartialEq, Debug, Copy, Clone)]
-pub enum TimeType {
-    FileAccessed,
-    FileModified,
-    FileCreated,
+pub enum SortField {
+    Unsorted, Name, Extension, Size, FileInode,
+    ModifiedDate, AccessedDate, CreatedDate,
 }
 
-impl TimeType {
-    pub fn header(&self) -> &'static str {
-        match *self {
-            TimeType::FileAccessed  => "Date Accessed",
-            TimeType::FileModified  => "Date Modified",
-            TimeType::FileCreated   => "Date Created",
+impl Default for SortField {
+    fn default() -> SortField {
+        SortField::Name
+    }
+}
+
+impl OptionSet for SortField {
+    fn deduce(matches: &getopts::Matches) -> Result<SortField, Misfire> {
+        match matches.opt_str("sort") {
+            Some(word)  => SortField::from_word(word),
+            None        => Ok(SortField::default()),
         }
     }
 }
 
+impl SortField {
 
-#[derive(PartialEq, Debug, Copy, Clone)]
-pub struct TimeTypes {
-    accessed: bool,
-    modified: bool,
-    created:  bool,
+    /// Find which field to use based on a user-supplied word.
+    fn from_word(word: String) -> Result<SortField, Misfire> {
+        match &word[..] {
+            "name" | "filename"   => Ok(SortField::Name),
+            "size" | "filesize"   => Ok(SortField::Size),
+            "ext"  | "extension"  => Ok(SortField::Extension),
+            "mod"  | "modified"   => Ok(SortField::ModifiedDate),
+            "acc"  | "accessed"   => Ok(SortField::AccessedDate),
+            "cr"   | "created"    => Ok(SortField::CreatedDate),
+            "none"                => Ok(SortField::Unsorted),
+            "inode"               => Ok(SortField::FileInode),
+            field                 => Err(SortField::none(field))
+        }
+    }
+
+    /// How to display an error when the word didn't match with anything.
+    fn none(field: &str) -> Misfire {
+        Misfire::InvalidOptions(getopts::Fail::UnrecognizedOption(format!("--sort {}", field)))
+    }
 }
 
-impl Default for TimeTypes {
-    fn default() -> TimeTypes {
-        TimeTypes { accessed: false, modified: true, created: false }
+
+impl OptionSet for SizeFormat {
+
+    /// Determine which file size to use in the file size column based on
+    /// the user’s options.
+    ///
+    /// The default mode is to use the decimal prefixes, as they are the
+    /// most commonly-understood, and don’t involve trying to parse large
+    /// strings of digits in your head. Changing the format to anything else
+    /// involves the `--binary` or `--bytes` flags, and these conflict with
+    /// each other.
+    fn deduce(matches: &getopts::Matches) -> Result<SizeFormat, Misfire> {
+        let binary = matches.opt_present("binary");
+        let bytes  = matches.opt_present("bytes");
+
+        match (binary, bytes) {
+            (true,  true )  => Err(Misfire::Conflict("binary", "bytes")),
+            (true,  false)  => Ok(SizeFormat::BinaryBytes),
+            (false, true )  => Ok(SizeFormat::JustBytes),
+            (false, false)  => Ok(SizeFormat::DecimalBytes),
+        }
     }
 }
 
-impl TimeTypes {
 
-    /// Find which field to use based on a user-supplied word.
+impl OptionSet for TimeTypes {
+
+    /// Determine which of a file’s time fields should be displayed for it
+    /// based on the user’s options.
+    ///
+    /// There are two separate ways to pick which fields to show: with a
+    /// flag (such as `--modified`) or with a parameter (such as
+    /// `--time=modified`). An error is signaled if both ways are used.
+    ///
+    /// It’s valid to show more than one column by passing in more than one
+    /// option, but passing *no* options means that the user just wants to
+    /// see the default set.
     fn deduce(matches: &getopts::Matches) -> Result<TimeTypes, Misfire> {
         let possible_word = matches.opt_str("time");
         let modified = matches.opt_present("modified");
@@ -468,11 +420,11 @@ impl TimeTypes {
                 return Err(Misfire::Useless("accessed", true, "time"));
             }
 
-            match &word[..] {
-                "mod" | "modified"  => Ok(TimeTypes { accessed: false, modified: true, created: false }),
-                "acc" | "accessed"  => Ok(TimeTypes { accessed: true, modified: false, created: false }),
-                "cr"  | "created"   => Ok(TimeTypes { accessed: false, modified: false, created: true }),
-                field   => Err(TimeTypes::none(field)),
+            match &*word {
+                "mod" | "modified"  => Ok(TimeTypes { accessed: false, modified: true,  created: false }),
+                "acc" | "accessed"  => Ok(TimeTypes { accessed: true,  modified: false, created: false }),
+                "cr"  | "created"   => Ok(TimeTypes { accessed: false, modified: false, created: true  }),
+                otherwise           => Err(Misfire::InvalidOptions(getopts::Fail::UnrecognizedOption(format!("--time {}", otherwise)))),
             }
         }
         else {
@@ -484,11 +436,6 @@ impl TimeTypes {
             }
         }
     }
-
-    /// How to display an error when the word didn't match with anything.
-    fn none(field: &str) -> Misfire {
-        Misfire::InvalidOptions(getopts::Fail::UnrecognizedOption(format!("--time {}", field)))
-    }
 }
 
 
@@ -568,83 +515,61 @@ impl RecurseOptions {
 }
 
 
-#[derive(PartialEq, Copy, Clone, Debug, Default)]
-pub struct Columns {
-    size_format: SizeFormat,
-    time_types: TimeTypes,
-    inode: bool,
-    links: bool,
-    blocks: bool,
-    group: bool,
-    git: bool
-}
-
-impl Columns {
-    pub fn deduce(matches: &getopts::Matches) -> Result<Columns, Misfire> {
-        Ok(Columns {
-            size_format: try!(SizeFormat::deduce(matches)),
-            time_types:  try!(TimeTypes::deduce(matches)),
-            inode:  matches.opt_present("inode"),
-            links:  matches.opt_present("links"),
-            blocks: matches.opt_present("blocks"),
-            group:  matches.opt_present("group"),
-            git:    cfg!(feature="git") && matches.opt_present("git"),
-        })
-    }
-
-    pub fn should_scan_for_git(&self) -> bool {
-        self.git
-    }
-
-    pub fn for_dir(&self, dir: Option<&Dir>) -> Vec<Column> {
-        let mut columns = vec![];
-
-        if self.inode {
-            columns.push(Inode);
-        }
+/// One of these things could happen instead of listing files.
+#[derive(PartialEq, Debug)]
+pub enum Misfire {
 
-        columns.push(Permissions);
+    /// The getopts crate didn't like these arguments.
+    InvalidOptions(getopts::Fail),
 
-        if self.links {
-            columns.push(HardLinks);
-        }
+    /// The user asked for help. This isn't strictly an error, which is why
+    /// this enum isn't named Error!
+    Help(String),
 
-        columns.push(FileSize(self.size_format));
+    /// The user wanted the version number.
+    Version,
 
-        if self.blocks {
-            columns.push(Blocks);
-        }
+    /// Two options were given that conflict with one another.
+    Conflict(&'static str, &'static str),
 
-        columns.push(User);
+    /// An option was given that does nothing when another one either is or
+    /// isn't present.
+    Useless(&'static str, bool, &'static str),
 
-        if self.group {
-            columns.push(Group);
-        }
+    /// An option was given that does nothing when either of two other options
+    /// are not present.
+    Useless2(&'static str, &'static str, &'static str),
 
-        if self.time_types.modified {
-            columns.push(Timestamp(TimeType::FileModified));
-        }
+    /// A numeric option was given that failed to be parsed as a number.
+    FailedParse(ParseIntError),
+}
 
-        if self.time_types.created {
-            columns.push(Timestamp(TimeType::FileCreated));
-        }
+impl Misfire {
+    /// The OS return code this misfire should signify.
+    pub fn error_code(&self) -> i32 {
+        if let Misfire::Help(_) = *self { 2 }
+                                   else { 3 }
+    }
+}
 
-        if self.time_types.accessed {
-            columns.push(Timestamp(TimeType::FileAccessed));
-        }
+impl fmt::Display for Misfire {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        use self::Misfire::*;
 
-        if cfg!(feature="git") {
-            if let Some(d) = dir {
-                if self.should_scan_for_git() && d.has_git_repo() {
-                    columns.push(GitStatus);
-                }
-            }
+        match *self {
+            InvalidOptions(ref e)  => write!(f, "{}", e),
+            Help(ref text)         => write!(f, "{}", text),
+            Version                => write!(f, "exa {}", env!("CARGO_PKG_VERSION")),
+            Conflict(a, b)         => write!(f, "Option --{} conflicts with option {}.", a, b),
+            Useless(a, false, b)   => write!(f, "Option --{} is useless without option --{}.", a, b),
+            Useless(a, true, b)    => write!(f, "Option --{} is useless given option --{}.", a, b),
+            Useless2(a, b1, b2)    => write!(f, "Option --{} is useless without options --{} or --{}.", a, b1, b2),
+            FailedParse(ref e)     => write!(f, "Failed to parse number: {}", e),
         }
-
-        columns
     }
 }
 
+
 #[cfg(test)]
 mod test {
     use super::Options;

+ 231 - 0
src/output/column.rs

@@ -0,0 +1,231 @@
+use ansi_term::Style;
+use unicode_width::UnicodeWidthStr;
+
+use dir::Dir;
+
+
+#[derive(PartialEq, Debug, Copy, Clone)]
+pub enum Column {
+    Permissions,
+    FileSize(SizeFormat),
+    Timestamp(TimeType),
+    Blocks,
+    User,
+    Group,
+    HardLinks,
+    Inode,
+
+    GitStatus,
+}
+
+/// Each column can pick its own **Alignment**. Usually, numbers are
+/// right-aligned, and text is left-aligned.
+#[derive(Copy, Clone)]
+pub enum Alignment {
+    Left, Right,
+}
+
+impl Column {
+
+    /// Get the alignment this column should use.
+    pub fn alignment(&self) -> Alignment {
+        match *self {
+            Column::FileSize(_) => Alignment::Right,
+            Column::HardLinks   => Alignment::Right,
+            Column::Inode       => Alignment::Right,
+            Column::Blocks      => Alignment::Right,
+            Column::GitStatus   => Alignment::Right,
+            _                   => Alignment::Left,
+        }
+    }
+
+    /// Get the text that should be printed at the top, when the user elects
+    /// to have a header row printed.
+    pub fn header(&self) -> &'static str {
+        match *self {
+            Column::Permissions   => "Permissions",
+            Column::FileSize(_)   => "Size",
+            Column::Timestamp(t)  => t.header(),
+            Column::Blocks        => "Blocks",
+            Column::User          => "User",
+            Column::Group         => "Group",
+            Column::HardLinks     => "Links",
+            Column::Inode         => "inode",
+            Column::GitStatus     => "Git",
+        }
+    }
+}
+
+
+#[derive(PartialEq, Copy, Clone, Debug, Default)]
+pub struct Columns {
+    pub size_format: SizeFormat,
+    pub time_types: TimeTypes,
+    pub inode: bool,
+    pub links: bool,
+    pub blocks: bool,
+    pub group: bool,
+    pub git: bool
+}
+
+impl Columns {
+    pub fn should_scan_for_git(&self) -> bool {
+        self.git
+    }
+
+    pub fn for_dir(&self, dir: Option<&Dir>) -> Vec<Column> {
+        let mut columns = vec![];
+
+        if self.inode {
+            columns.push(Column::Inode);
+        }
+
+        columns.push(Column::Permissions);
+
+        if self.links {
+            columns.push(Column::HardLinks);
+        }
+
+        columns.push(Column::FileSize(self.size_format));
+
+        if self.blocks {
+            columns.push(Column::Blocks);
+        }
+
+        columns.push(Column::User);
+
+        if self.group {
+            columns.push(Column::Group);
+        }
+
+        if self.time_types.modified {
+            columns.push(Column::Timestamp(TimeType::FileModified));
+        }
+
+        if self.time_types.created {
+            columns.push(Column::Timestamp(TimeType::FileCreated));
+        }
+
+        if self.time_types.accessed {
+            columns.push(Column::Timestamp(TimeType::FileAccessed));
+        }
+
+        if cfg!(feature="git") {
+            if let Some(d) = dir {
+                if self.should_scan_for_git() && d.has_git_repo() {
+                    columns.push(Column::GitStatus);
+                }
+            }
+        }
+
+        columns
+    }
+}
+
+
+/// Formatting options for file sizes.
+#[derive(PartialEq, Debug, Copy, Clone)]
+pub enum SizeFormat {
+
+    /// Format the file size using **decimal** prefixes, such as “kilo”,
+    /// “mega”, or “giga”.
+    DecimalBytes,
+
+    /// Format the file size using **binary** prefixes, such as “kibi”,
+    /// “mebi”, or “gibi”.
+    BinaryBytes,
+
+    /// Do no formatting and just display the size as a number of bytes.
+    JustBytes,
+}
+
+impl Default for SizeFormat {
+    fn default() -> SizeFormat {
+        SizeFormat::DecimalBytes
+    }
+}
+
+
+/// The types of a file’s time fields. These three fields are standard
+/// across most (all?) operating systems.
+#[derive(PartialEq, Debug, Copy, Clone)]
+pub enum TimeType {
+
+    /// The file’s accessed time (`st_atime`).
+    FileAccessed,
+
+    /// The file’s modified time (`st_mtime`).
+    FileModified,
+
+    /// The file’s creation time (`st_ctime`).
+    FileCreated,
+}
+
+impl TimeType {
+
+    /// Returns the text to use for a column’s heading in the columns output.
+    pub fn header(&self) -> &'static str {
+        match *self {
+            TimeType::FileAccessed  => "Date Accessed",
+            TimeType::FileModified  => "Date Modified",
+            TimeType::FileCreated   => "Date Created",
+        }
+    }
+}
+
+
+/// Fields for which of a file’s time fields should be displayed in the
+/// columns output.
+///
+/// There should always be at least one of these--there's no way to disable
+/// the time columns entirely (yet).
+#[derive(PartialEq, Debug, Copy, Clone)]
+pub struct TimeTypes {
+    pub accessed: bool,
+    pub modified: bool,
+    pub created:  bool,
+}
+
+impl Default for TimeTypes {
+
+    /// By default, display just the ‘modified’ time. This is the most
+    /// common option, which is why it has this shorthand.
+    fn default() -> 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);
+    }
+}

+ 5 - 6
src/output/details.rs

@@ -119,12 +119,12 @@ use std::ops::Add;
 use std::iter::repeat;
 
 use colours::Colours;
-use column::{Alignment, Column, Cell};
 use dir::Dir;
 use feature::xattr::{Attribute, FileAttributes};
 use file::fields as f;
 use file::File;
-use options::{Columns, FileFilter, RecurseOptions, SizeFormat};
+use options::{FileFilter, RecurseOptions};
+use output::column::{Alignment, Column, Columns, Cell, SizeFormat};
 
 use ansi_term::{ANSIString, ANSIStrings, Style};
 
@@ -153,7 +153,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, Default)]
+#[derive(PartialEq, Debug, Copy, Clone)]
 pub struct Details {
 
     /// A Columns object that says which columns should be included in the
@@ -658,7 +658,7 @@ impl<U> Table<U> where U: Users {
             .map(|n| self.rows.iter().map(|row| row.column_width(n)).max().unwrap_or(0))
             .collect();
 
-        let total_width: usize = self.columns.len() + column_widths.iter().fold(0,Add::add);
+        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();
@@ -754,8 +754,7 @@ pub mod test {
     pub use super::Table;
     pub use file::File;
     pub use file::fields as f;
-
-    pub use column::{Cell, Column};
+    pub use output::column::{Cell, Column};
 
     pub use users::{User, Group, uid_t, gid_t};
     pub use users::mock::MockUsers;

+ 2 - 1
src/output/grid_details.rs

@@ -3,10 +3,11 @@ use std::iter::repeat;
 use users::OSUsers;
 use term_grid as grid;
 
-use column::{Column, Cell};
 use dir::Dir;
 use feature::xattr::FileAttributes;
 use file::File;
+
+use output::column::{Column, Cell};
 use output::details::{Details, Table};
 use output::grid::Grid;
 

+ 2 - 0
src/output/mod.rs

@@ -13,6 +13,8 @@ mod grid;
 pub mod details;
 mod lines;
 mod grid_details;
+pub mod column;
+
 
 pub fn filename(file: &File, colours: &Colours, links: bool) -> String {
     if links && file.is_link() {