Browse Source

Convert docs to standard format

Ben S 11 years ago
parent
commit
2ba0b3bd5f
7 changed files with 321 additions and 240 deletions
  1. 13 8
      src/column.rs
  2. 12 6
      src/dir.rs
  3. 190 149
      src/file.rs
  4. 3 0
      src/filetype.rs
  5. 99 73
      src/options.rs
  6. 2 1
      src/output.rs
  7. 2 3
      src/term.rs

+ 13 - 8
src/column.rs

@@ -23,9 +23,8 @@ pub enum SizeFormat {
 
 impl Copy for SizeFormat { }
 
-// Each column can pick its own alignment. Usually, numbers are
-// right-aligned, and text is left-aligned.
-
+/// Each column can pick its own **Alignment**. Usually, numbers are
+/// right-aligned, and text is left-aligned.
 pub enum Alignment {
     Left, Right,
 }
@@ -33,6 +32,8 @@ pub enum Alignment {
 impl Copy for Alignment { }
 
 impl Column {
+
+    /// Get the alignment this column should use.
     pub fn alignment(&self) -> Alignment {
         match *self {
             Column::FileSize(_) => Alignment::Right,
@@ -43,6 +44,8 @@ impl Column {
         }
     }
 
+    /// 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",
@@ -57,16 +60,18 @@ impl Column {
     }
 }
 
+/// Pad a string with the given number of spaces.
 fn spaces(length: usize) -> String {
     repeat(" ").take(length).collect()
 }
 
-// An Alignment is used to pad a string to a certain length, letting
-// it pick which end it puts the text on. It takes the amount of
-// padding to apply, rather than the width the text should end up,
-// because these strings are usually full of control characters.
-
 impl Alignment {
+    /// Pad a string with the given alignment and number of spaces.
+    ///
+    /// This doesn't take the width the string *should* be, rather the number
+    /// of spaces to add: this is because the strings are usually full of
+    /// invisible control characters, so getting the displayed width of the
+    /// string is not as simple as just getting its length.
     pub fn pad_string(&self, string: &String, padding: usize) -> String {
         match *self {
             Alignment::Left  => format!("{}{}", string, spaces(padding).as_slice()),

+ 12 - 6
src/dir.rs

@@ -1,18 +1,21 @@
 use std::io::{fs, IoResult};
 use file::File;
 
-// The purpose of a Dir is to provide a cached list of the file paths
-// in the directory being searched for. This object is then passed to
-// the Files themselves, which can then check the status of their
-// surrounding files, such as whether it needs to be coloured
-// differently if a certain other file exists.
-
+/// A **Dir** provides a cached list of the file paths in a directory that's
+/// being listed.
+///
+/// This object gets passed to the Files themselves, in order for them to
+/// check the existence of surrounding files, then highlight themselves
+/// accordingly. (See `File#get_source_files`)
 pub struct Dir {
     pub contents: Vec<Path>,
     pub path: Path,
 }
 
 impl Dir {
+    /// Create a new Dir object filled with all the files in the directory
+    /// pointed to by the given path. Fails if the directory can't be read, or
+    /// isn't actually a directory.
     pub fn readdir(path: Path) -> IoResult<Dir> {
         fs::readdir(&path).map(|paths| Dir {
             contents: paths,
@@ -20,6 +23,8 @@ impl Dir {
         })
     }
 
+    /// Produce a vector of File objects from an initialised directory,
+    /// printing out an error if any of the Files fail to be created.
     pub fn files(&self) -> Vec<File> {
         let mut files = vec![];
 
@@ -33,6 +38,7 @@ impl Dir {
         files
     }
 
+    /// Whether this directory contains a file with the given path.
     pub fn contains(&self, path: &Path) -> bool {
         self.contents.contains(path)
     }

+ 190 - 149
src/file.rs

@@ -14,15 +14,17 @@ use column::Column::*;
 use dir::Dir;
 use filetype::HasType;
 
+/// This grey value is directly in between white and black, so it's guaranteed
+/// to show up on either backgrounded terminal.
 pub static GREY: Colour = Fixed(244);
 
-// Instead of working with Rust's Paths, we have our own File object
-// that holds the Path and various cached information. Each file is
-// definitely going to have its filename used at least once, its stat
-// information queried at least once, and its file extension extracted
-// at least once, so we may as well carry around that information with
-// the actual path.
-
+/// A **File** is a wrapper around one of Rust's Path objects, along with
+/// associated data about the file.
+///
+/// Each file is definitely going to have its filename displayed at least
+/// once, have its file extension extracted at least once, and have its stat
+/// information queried at least once, so it makes sense to do all this at the
+/// start and hold on to all the information.
 pub struct File<'a> {
     pub name:  String,
     pub dir:   Option<&'a Dir>,
@@ -32,13 +34,15 @@ pub struct File<'a> {
 }
 
 impl<'a> File<'a> {
+    /// Create a new File object from the given Path, inside the given Dir, if
+    /// appropriate. Paths specified directly on the command-line have no Dirs.
+    ///
+    /// This uses lstat instead of stat, which doesn't follow symbolic links.
     pub fn from_path(path: &Path, parent: Option<&'a Dir>) -> IoResult<File<'a>> {
-        // Use lstat here instead of file.stat(), as it doesn't follow
-        // symbolic links. Otherwise, the stat() call will fail if it
-        // encounters a link that's target is non-existent.
         fs::lstat(path).map(|stat| File::with_stat(stat, path, parent))
     }
 
+    /// Create a new File object from the given Stat result, and other data.
     pub fn with_stat(stat: io::FileStat, path: &Path, parent: Option<&'a Dir>) -> File<'a> {
         let v = path.filename().unwrap();  // fails if / or . or ..
         let filename = String::from_utf8_lossy(v);
@@ -48,132 +52,43 @@ impl<'a> File<'a> {
             dir:   parent,
             stat:  stat,
             name:  filename.to_string(),
-            ext:   File::ext(filename.as_slice()),
+            ext:   ext(filename.as_slice()),
         }
     }
 
-    fn ext(name: &'a str) -> Option<String> {
-        // The extension is the series of characters after a dot at
-        // the end of a filename. This deliberately also counts
-        // dotfiles - the ".git" folder has the extension "git".
-        name.rfind('.').map(|p| name[p+1..].to_string())
-    }
-
+    /// Whether this file is a dotfile or not.
     pub fn is_dotfile(&self) -> bool {
         self.name.as_slice().starts_with(".")
     }
 
+    /// Whether this file is a temporary file or not.
     pub fn is_tmpfile(&self) -> bool {
         let name = self.name.as_slice();
         name.ends_with("~") || (name.starts_with("#") && name.ends_with("#"))
     }
 
-    // Highlight the compiled versions of files. Some of them, like .o,
-    // get special highlighting when they're alone because there's no
-    // point in existing without their source. Others can be perfectly
-    // content without their source files, such as how .js is valid
-    // without a .coffee.
-
-    pub fn get_source_files(&self) -> Vec<Path> {
-        if let Some(ref ext) = self.ext {
-            let ext = ext.as_slice();
-            match ext {
-                "class" => vec![self.path.with_extension("java")],  // Java
-                "css"   => vec![self.path.with_extension("sass"),   self.path.with_extension("less")],  // SASS, Less
-                "elc"   => vec![self.path.with_extension("el")],    // Emacs Lisp
-                "hi"    => vec![self.path.with_extension("hs")],    // Haskell
-                "js"    => vec![self.path.with_extension("coffee"), self.path.with_extension("ts")],  // CoffeeScript, TypeScript
-                "o"     => vec![self.path.with_extension("c"),      self.path.with_extension("cpp")], // C, C++
-                "pyc"   => vec![self.path.with_extension("py")],    // Python
-
-                "aux" => vec![self.path.with_extension("tex")],  // TeX: auxiliary file
-                "bbl" => vec![self.path.with_extension("tex")],  // BibTeX bibliography file
-                "blg" => vec![self.path.with_extension("tex")],  // BibTeX log file
-                "lof" => vec![self.path.with_extension("tex")],  // TeX list of figures
-                "log" => vec![self.path.with_extension("tex")],  // TeX log file
-                "lot" => vec![self.path.with_extension("tex")],  // TeX list of tables
-                "toc" => vec![self.path.with_extension("tex")],  // TeX table of contents
-
-                _ => vec![],  // No source files if none of the above
-            }
-        }
-        else {
-            vec![]  // No source files if there's no extension, either!
-        }
-    }
-
+    /// Get the data for a column, formatted as a coloured string.
     pub fn display<U: Users>(&self, column: &Column, users_cache: &mut U) -> String {
         match *column {
-            Permissions => {
-                self.permissions_string()
-            },
-
-            FileName => {
-                self.file_name()
-            },
-
-            FileSize(use_iec) => {
-                self.file_size(use_iec)
-            },
-
-            // A file with multiple links is interesting, but
-            // directories and suchlike can have multiple links all
-            // the time.
-            HardLinks => {
-                let style = if self.has_multiple_links() { Red.on(Yellow) } else { Red.normal() };
-                style.paint(self.stat.unstable.nlink.to_string().as_slice()).to_string()
-            },
-
-            Inode => {
-                Purple.paint(self.stat.unstable.inode.to_string().as_slice()).to_string()
-            },
-
-            Blocks => {
-                if self.stat.kind == io::FileType::RegularFile || self.stat.kind == io::FileType::Symlink {
-                    Cyan.paint(self.stat.unstable.blocks.to_string().as_slice()).to_string()
-                }
-                else {
-                    GREY.paint("-").to_string()
-                }
-            },
-
-            // Display the ID if the user/group doesn't exist, which
-            // usually means it was deleted but its files weren't.
-            User => {
-                let uid = self.stat.unstable.uid as i32;
-
-                let user_name = match users_cache.get_user_by_uid(uid) {
-                    Some(user) => user.name,
-                    None => uid.to_string(),
-                };
-
-                let style = if users_cache.get_current_uid() == uid { Yellow.bold() } else { Plain };
-                style.paint(user_name.as_slice()).to_string()
-            },
-
-            Group => {
-                let gid = self.stat.unstable.gid as u32;
-                let mut style = Plain;
-
-                let group_name = match users_cache.get_group_by_gid(gid) {
-                    Some(group) => {
-                        let current_uid = users_cache.get_current_uid();
-                        if let Some(current_user) = users_cache.get_user_by_uid(current_uid) {
-                            if current_user.primary_group == group.gid || group.members.contains(&current_user.name) {
-                                style = Yellow.bold();
-                            }
-                        }
-                        group.name
-                    },
-                    None => gid.to_string(),
-                };
-
-                style.paint(group_name.as_slice()).to_string()
-            },
+            Permissions  => self.permissions_string(),
+            FileName     => self.file_name_view(),
+            FileSize(f)  => self.file_size(f),
+            HardLinks    => self.hard_links(),
+            Inode        => self.inode(),
+            Blocks       => self.blocks(),
+            User         => self.user(users_cache),
+            Group        => self.group(users_cache),
         }
     }
 
-    pub fn file_name(&self) -> String {
+    /// The "file name view" is what's displayed in the column and lines
+    /// views, but *not* in the grid view.
+    ///
+    /// It consists of the file name coloured in the appropriate style, and,
+    /// if it's a symlink, an arrow pointing to the file it links to, also
+    /// coloured in the appropriate style. Files that don't exist will be
+    /// coloured red.
+    pub fn file_name_view(&self) -> String {
         let name = self.name.as_slice();
         let displayed_name = self.file_colour().paint(name);
         if self.stat.kind == io::FileType::Symlink {
@@ -193,6 +108,16 @@ impl<'a> File<'a> {
         }
     }
 
+    /// The `ansi_term::Style` that this file's name should be painted.
+    pub fn file_colour(&self) -> Style {
+        self.get_type().style()
+    }
+
+    /// 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 {
         self.name.as_slice().width(false)
     }
@@ -207,7 +132,7 @@ impl<'a> File<'a> {
             dir:   self.dir,
             stat:  stat,
             name:  filename.to_string(),
-            ext:   File::ext(filename.as_slice()),
+            ext:   ext(filename.as_slice()),
         });
 
         // Statting a path usually fails because the file at the
@@ -223,9 +148,83 @@ impl<'a> File<'a> {
         }
     }
 
+    /// This file's number of hard links as a coloured string.
+    fn hard_links(&self) -> String {
+        let style = if self.has_multiple_links() { Red.on(Yellow) } else { Red.normal() };
+        style.paint(self.stat.unstable.nlink.to_string().as_slice()).to_string()
+    }
+
+    /// Whether this is a regular file with more than one link.
+    ///
+    /// This is important, because a file with multiple links is uncommon,
+    /// while you can come across directories and other types with multiple
+    /// links much more often.
+    fn has_multiple_links(&self) -> bool {
+        self.stat.kind == io::FileType::RegularFile && self.stat.unstable.nlink > 1
+    }
+
+    /// This file's inode as a coloured string.
+    fn inode(&self) -> String {
+        Purple.paint(self.stat.unstable.inode.to_string().as_slice()).to_string()
+    }
+
+    /// This file's number of filesystem blocks (if available) as a coloured string.
+    fn blocks(&self) -> String {
+        if self.stat.kind == io::FileType::RegularFile || self.stat.kind == io::FileType::Symlink {
+            Cyan.paint(self.stat.unstable.blocks.to_string().as_slice()).to_string()
+        }
+        else {
+            GREY.paint("-").to_string()
+        }
+    }
+
+    /// This file's owner's username as a coloured string.
+    ///
+    /// If the user is not present, then it formats the uid as a number
+    /// instead. This usually happens when a user is deleted, but still owns
+    /// files.
+    fn user<U: Users>(&self, users_cache: &mut U) -> String {
+        let uid = self.stat.unstable.uid as i32;
+
+        let user_name = match users_cache.get_user_by_uid(uid) {
+            Some(user) => user.name,
+            None => uid.to_string(),
+        };
+
+        let style = if users_cache.get_current_uid() == uid { Yellow.bold() } else { Plain };
+        style.paint(user_name.as_slice()).to_string()
+    }
+
+    /// This file's group name as a coloured string.
+    ///
+    /// As above, if not present, it formats the gid as a number instead.
+    fn group<U: Users>(&self, users_cache: &mut U) -> String {
+        let gid = self.stat.unstable.gid as u32;
+        let mut style = Plain;
+
+        let group_name = match users_cache.get_group_by_gid(gid) {
+            Some(group) => {
+                let current_uid = users_cache.get_current_uid();
+                if let Some(current_user) = users_cache.get_user_by_uid(current_uid) {
+                    if current_user.primary_group == group.gid || group.members.contains(&current_user.name) {
+                        style = Yellow.bold();
+                    }
+                }
+                group.name
+            },
+            None => gid.to_string(),
+        };
+
+        style.paint(group_name.as_slice()).to_string()
+    }
+
+    /// This file's size, formatted using the given way, as a coloured string.
+    ///
+    /// For directories, no size is given. Although they do have a size on
+    /// some filesystems, I've never looked at one of those numbers and gained
+    /// any information from it, so by emitting "-" instead, the table is less
+    /// cluttered with numbers.
     fn file_size(&self, size_format: SizeFormat) -> String {
-        // Don't report file sizes for directories. I've never looked
-        // at one of those numbers and gained any information from it.
         if self.stat.kind == io::FileType::Directory {
             GREY.paint("-").to_string()
         }
@@ -246,25 +245,26 @@ impl<'a> File<'a> {
         }
     }
 
+    /// This file's type, represented by a coloured character.
+    ///
+    /// Although the file type can usually be guessed from the colour of the
+    /// file, `ls` puts this character there, so people will expect it.
     fn type_char(&self) -> ANSIString {
         return match self.stat.kind {
-            io::FileType::RegularFile  => Plain.paint("."),
-            io::FileType::Directory    => Blue.paint("d"),
-            io::FileType::NamedPipe    => Yellow.paint("|"),
-            io::FileType::BlockSpecial => Purple.paint("s"),
-            io::FileType::Symlink      => Cyan.paint("l"),
-            io::FileType::Unknown      => Plain.paint("?"),
+            io::FileType::RegularFile   => Plain.paint("."),
+            io::FileType::Directory     => Blue.paint("d"),
+            io::FileType::NamedPipe     => Yellow.paint("|"),
+            io::FileType::BlockSpecial  => Purple.paint("s"),
+            io::FileType::Symlink       => Cyan.paint("l"),
+            io::FileType::Unknown       => Plain.paint("?"),
         }
     }
 
-    pub fn file_colour(&self) -> Style {
-        self.get_type().style()
-    }
-
-    fn has_multiple_links(&self) -> bool {
-        self.stat.kind == io::FileType::RegularFile && self.stat.unstable.nlink > 1
-    }
-
+    /// Generate the "rwxrwxrwx" permissions string, like how ls does it.
+    ///
+    /// Each character is given its own colour. The first three permission
+    /// bits are bold because they're the ones used most often, and executable
+    /// files are underlined to make them stand out more.
     fn permissions_string(&self) -> String {
         let bits = self.stat.perm;
         let executable_colour = match self.stat.kind {
@@ -274,22 +274,20 @@ impl<'a> File<'a> {
 
         return format!("{}{}{}{}{}{}{}{}{}{}",
             self.type_char(),
-
-            // The first three are bold because they're the ones used
-            // most often.
-            File::permission_bit(bits, io::USER_READ,     "r", Yellow.bold()),
-            File::permission_bit(bits, io::USER_WRITE,    "w", Red.bold()),
-            File::permission_bit(bits, io::USER_EXECUTE,  "x", executable_colour),
-            File::permission_bit(bits, io::GROUP_READ,    "r", Yellow.normal()),
-            File::permission_bit(bits, io::GROUP_WRITE,   "w", Red.normal()),
-            File::permission_bit(bits, io::GROUP_EXECUTE, "x", Green.normal()),
-            File::permission_bit(bits, io::OTHER_READ,    "r", Yellow.normal()),
-            File::permission_bit(bits, io::OTHER_WRITE,   "w", Red.normal()),
-            File::permission_bit(bits, io::OTHER_EXECUTE, "x", Green.normal()),
+            File::permission_bit(&bits, io::USER_READ,     "r", Yellow.bold()),
+            File::permission_bit(&bits, io::USER_WRITE,    "w", Red.bold()),
+            File::permission_bit(&bits, io::USER_EXECUTE,  "x", executable_colour),
+            File::permission_bit(&bits, io::GROUP_READ,    "r", Yellow.normal()),
+            File::permission_bit(&bits, io::GROUP_WRITE,   "w", Red.normal()),
+            File::permission_bit(&bits, io::GROUP_EXECUTE, "x", Green.normal()),
+            File::permission_bit(&bits, io::OTHER_READ,    "r", Yellow.normal()),
+            File::permission_bit(&bits, io::OTHER_WRITE,   "w", Red.normal()),
+            File::permission_bit(&bits, io::OTHER_EXECUTE, "x", Green.normal()),
        );
     }
 
-    fn permission_bit(bits: io::FilePermission, bit: io::FilePermission, character: &'static str, style: Style) -> ANSIString {
+    /// Helper method for the permissions string.
+    fn permission_bit(bits: &io::FilePermission, bit: io::FilePermission, character: &'static str, style: Style) -> ANSIString<'static> {
         if bits.contains(bit) {
             style.paint(character)
         }
@@ -297,4 +295,47 @@ impl<'a> File<'a> {
             GREY.paint("-")
         }
     }
+
+    /// For this file, return a vector of alternate file paths that, if any of
+    /// them exist, mean that *this* file should be coloured as `Compiled`.
+    ///
+    /// The point of this is to highlight compiled files such as `foo.o` when
+    /// their source file `foo.c` exists in the same directory. It's too
+    /// dangerous to highlight *all* compiled, so the paths in this vector
+    /// are checked for existence first: for example, `foo.js` is perfectly
+    /// valid without `foo.coffee`.
+    pub fn get_source_files(&self) -> Vec<Path> {
+        if let Some(ref ext) = self.ext {
+            match ext.as_slice() {
+                "class" => vec![self.path.with_extension("java")],  // Java
+                "css"   => vec![self.path.with_extension("sass"),   self.path.with_extension("less")],  // SASS, Less
+                "elc"   => vec![self.path.with_extension("el")],    // Emacs Lisp
+                "hi"    => vec![self.path.with_extension("hs")],    // Haskell
+                "js"    => vec![self.path.with_extension("coffee"), self.path.with_extension("ts")],  // CoffeeScript, TypeScript
+                "o"     => vec![self.path.with_extension("c"),      self.path.with_extension("cpp")], // C, C++
+                "pyc"   => vec![self.path.with_extension("py")],    // Python
+
+                "aux" => vec![self.path.with_extension("tex")],  // TeX: auxiliary file
+                "bbl" => vec![self.path.with_extension("tex")],  // BibTeX bibliography file
+                "blg" => vec![self.path.with_extension("tex")],  // BibTeX log file
+                "lof" => vec![self.path.with_extension("tex")],  // TeX list of figures
+                "log" => vec![self.path.with_extension("tex")],  // TeX log file
+                "lot" => vec![self.path.with_extension("tex")],  // TeX list of tables
+                "toc" => vec![self.path.with_extension("tex")],  // TeX table of contents
+
+                _ => vec![],  // No source files if none of the above
+            }
+        }
+        else {
+            vec![]  // No source files if there's no extension, either!
+        }
+    }
+}
+
+/// Extract an extension from a string, if one is present.
+///
+/// The extension is the series of characters after the last dot. This
+/// deliberately counts dotfiles, so the ".git" folder has the extension "git".
+fn ext<'a>(name: &'a str) -> Option<String> {
+    name.rfind('.').map(|p| name[p+1..].to_string())
 }

+ 3 - 0
src/filetype.rs

@@ -55,6 +55,8 @@ static BUILD_TYPES: &'static [&'static str] = &[
     "Gruntfile.coffee" ];
 
 impl FileType {
+
+    /// Get the `ansi_term::Style` that a file of this type should use.
     pub fn style(&self) -> Style {
         match *self {
             Normal     => Plain,
@@ -77,6 +79,7 @@ impl FileType {
 }
 
 pub trait HasType {
+    /// For a given file, find out what type it has.
     fn get_type(&self) -> FileType;
 }
 

+ 99 - 73
src/options.rs

@@ -11,32 +11,10 @@ use std::ascii::AsciiExt;
 use std::slice::Iter;
 use std::fmt;
 
-use self::Error::*;
-
-#[derive(PartialEq, Debug)]
-pub enum SortField {
-    Unsorted, Name, Extension, Size, FileInode
-}
-
-impl Copy for SortField { }
-
-impl SortField {
-    fn from_word(word: String) -> Result<SortField, Error> {
-        match word.as_slice() {
-            "name"  => Ok(SortField::Name),
-            "size"  => Ok(SortField::Size),
-            "ext"   => Ok(SortField::Extension),
-            "none"  => Ok(SortField::Unsorted),
-            "inode" => Ok(SortField::FileInode),
-            field   => Err(no_sort_field(field))
-        }
-    }
-}
-
-fn no_sort_field(field: &str) -> Error {
-    Error::InvalidOptions(getopts::Fail::UnrecognizedOption(format!("--sort {}", field)))
-}
+use self::Misfire::*;
 
+/// The *Options* struct represents a parsed version of the user's
+/// command-line options.
 #[derive(PartialEq, Debug)]
 pub struct Options {
     pub list_dirs: bool,
@@ -47,35 +25,10 @@ pub struct Options {
     pub view: View,
 }
 
-#[derive(PartialEq, Debug)]
-pub enum Error {
-    InvalidOptions(getopts::Fail),
-    Help(String),
-    Conflict(&'static str, &'static str),
-    Useless(&'static str, bool, &'static str),
-}
-
-impl Error {
-    pub fn error_code(&self) -> isize {
-        if let Help(_) = *self { 2 }
-                          else { 3 }
-    }
-}
-
-impl fmt::Display for Error {
-        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-                match *self {
-                    InvalidOptions(ref e) => write!(f, "{}", e),
-                    Help(ref text)        => write!(f, "{}", text),
-                    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),
-                }
-        }
-}
-
 impl Options {
-    pub fn getopts(args: &[String]) -> Result<Options, Error> {
+
+    /// Call getopts on the given slice of command-line strings.
+    pub fn getopts(args: &[String]) -> Result<Options, Misfire> {
         let opts = &[
             getopts::optflag("1", "oneline",   "display one entry per line"),
             getopts::optflag("a", "all",       "show dot-files"),
@@ -96,11 +49,11 @@ impl Options {
 
         let matches = match getopts::getopts(args, opts) {
             Ok(m) => m,
-            Err(e) => return Err(Error::InvalidOptions(e)),
+            Err(e) => return Err(Misfire::InvalidOptions(e)),
         };
 
         if matches.opt_present("help") {
-            return Err(Error::Help(getopts::usage("Usage:\n  exa [options] [files...]", opts)));
+            return Err(Misfire::Help(getopts::usage("Usage:\n  exa [options] [files...]", opts)));
         }
 
         let sort_field = match matches.opt_str("sort") {
@@ -122,6 +75,7 @@ impl Options {
         self.path_strs.iter()
     }
 
+    /// Transform the files somehow before listing them.
     pub fn transform_files<'a>(&self, unordered_files: Vec<File<'a>>) -> Vec<File<'a>> {
         let mut files: Vec<File<'a>> = unordered_files.into_iter()
             .filter(|f| self.should_display(f))
@@ -156,27 +110,95 @@ impl Options {
     }
 }
 
-fn view(matches: &getopts::Matches) -> Result<View, Error> {
+/// User-supplied field to sort by
+#[derive(PartialEq, Debug)]
+pub enum SortField {
+    Unsorted, Name, Extension, Size, FileInode
+}
+
+impl Copy for SortField { }
+
+impl SortField {
+
+    /// Find which field to use based on a user-supplied word.
+    fn from_word(word: String) -> Result<SortField, Misfire> {
+        match word.as_slice() {
+            "name"  => Ok(SortField::Name),
+            "size"  => Ok(SortField::Size),
+            "ext"   => Ok(SortField::Extension),
+            "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),
+
+    /// 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),
+}
+
+impl Misfire {
+    /// The OS return code this misfire should signify.
+    pub fn error_code(&self) -> isize {
+        if let Help(_) = *self { 2 }
+                          else { 3 }
+    }
+}
+
+impl fmt::Display for Misfire {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match *self {
+            InvalidOptions(ref e) => write!(f, "{}", e),
+            Help(ref text)        => write!(f, "{}", text),
+            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),
+        }
+    }
+}
+
+/// Turns the Getopts results object into a View object.
+fn view(matches: &getopts::Matches) -> Result<View, Misfire> {
     if matches.opt_present("long") {
         if matches.opt_present("across") {
-            Err(Error::Useless("across", true, "long"))
+            Err(Misfire::Useless("across", true, "long"))
         }
         else if matches.opt_present("oneline") {
-            Err(Error::Useless("across", true, "long"))
+            Err(Misfire::Useless("across", true, "long"))
         }
         else {
             Ok(View::Details(try!(columns(matches)), matches.opt_present("header")))
         }
     }
     else if matches.opt_present("binary") {
-        Err(Error::Useless("binary", false, "long"))
+        Err(Misfire::Useless("binary", false, "long"))
     }
     else if matches.opt_present("bytes") {
-        Err(Error::Useless("bytes", false, "long"))
+        Err(Misfire::Useless("bytes", false, "long"))
     }
     else if matches.opt_present("oneline") {
         if matches.opt_present("across") {
-            Err(Error::Useless("across", true, "oneline"))
+            Err(Misfire::Useless("across", true, "oneline"))
         }
         else {
             Ok(View::Lines)
@@ -190,19 +212,22 @@ fn view(matches: &getopts::Matches) -> Result<View, Error> {
     }
 }
 
-fn file_size(matches: &getopts::Matches) -> Result<SizeFormat, Error> {
+/// Finds out which file size the user has asked for.
+fn file_size(matches: &getopts::Matches) -> Result<SizeFormat, Misfire> {
     let binary = matches.opt_present("binary");
     let bytes = matches.opt_present("bytes");
 
     match (binary, bytes) {
-        (true,  true ) => Err(Error::Conflict("binary", "bytes")),
+        (true,  true ) => Err(Misfire::Conflict("binary", "bytes")),
         (true,  false) => Ok(SizeFormat::BinaryBytes),
         (false, true ) => Ok(SizeFormat::JustBytes),
         (false, false) => Ok(SizeFormat::DecimalBytes),
     }
 }
 
-fn columns(matches: &getopts::Matches) -> Result<Vec<Column>, Error> {
+/// Turns the Getopts results object into a list of columns for the columns
+/// view, depending on the passed-in command-line arguments.
+fn columns(matches: &getopts::Matches) -> Result<Vec<Column>, Misfire> {
     let mut columns = vec![];
 
     if matches.opt_present("inode") {
@@ -215,6 +240,7 @@ fn columns(matches: &getopts::Matches) -> Result<Vec<Column>, Error> {
         columns.push(HardLinks);
     }
 
+    // Fail early here if two file size flags are given
     columns.push(FileSize(try!(file_size(matches))));
 
     if matches.opt_present("blocks") {
@@ -234,13 +260,13 @@ fn columns(matches: &getopts::Matches) -> Result<Vec<Column>, Error> {
 #[cfg(test)]
 mod test {
     use super::Options;
-    use super::Error;
-    use super::Error::*;
+    use super::Misfire;
+    use super::Misfire::*;
 
     use std::fmt;
 
-    fn is_helpful(error: Result<Options, Error>) -> bool {
-        match error {
+    fn is_helpful(misfire: Result<Options, Misfire>) -> bool {
+        match misfire {
             Err(Help(_)) => true,
             _            => false,
         }
@@ -279,30 +305,30 @@ mod test {
     #[test]
     fn file_sizes() {
         let opts = Options::getopts(&[ "--long".to_string(), "--binary".to_string(), "--bytes".to_string() ]);
-        assert_eq!(opts.unwrap_err(), Error::Conflict("binary", "bytes"))
+        assert_eq!(opts.unwrap_err(), Misfire::Conflict("binary", "bytes"))
     }
 
     #[test]
     fn just_binary() {
         let opts = Options::getopts(&[ "--binary".to_string() ]);
-        assert_eq!(opts.unwrap_err(), Error::Useless("binary", false, "long"))
+        assert_eq!(opts.unwrap_err(), Misfire::Useless("binary", false, "long"))
     }
 
     #[test]
     fn just_bytes() {
         let opts = Options::getopts(&[ "--bytes".to_string() ]);
-        assert_eq!(opts.unwrap_err(), Error::Useless("bytes", false, "long"))
+        assert_eq!(opts.unwrap_err(), Misfire::Useless("bytes", false, "long"))
     }
 
     #[test]
     fn long_across() {
         let opts = Options::getopts(&[ "--long".to_string(), "--across".to_string() ]);
-        assert_eq!(opts.unwrap_err(), Error::Useless("across", true, "long"))
+        assert_eq!(opts.unwrap_err(), Misfire::Useless("across", true, "long"))
     }
 
     #[test]
     fn oneline_across() {
         let opts = Options::getopts(&[ "--oneline".to_string(), "--across".to_string() ]);
-        assert_eq!(opts.unwrap_err(), Error::Useless("across", true, "oneline"))
+        assert_eq!(opts.unwrap_err(), Misfire::Useless("across", true, "oneline"))
     }
 }

+ 2 - 1
src/output.rs

@@ -26,9 +26,10 @@ impl View {
     }
 }
 
+/// The lines view literally just displays each file, line-by-line.
 fn lines_view(files: Vec<File>) {
     for file in files.iter() {
-        println!("{}", file.file_name());
+        println!("{}", file.file_name_view());
     }
 }
 

+ 2 - 3
src/term.rs

@@ -37,12 +37,11 @@ mod c {
     }
 }
 
+/// Query the current processes's output, returning its width and height as a
+/// number of characters. Returns None if the output isn't to a terminal.
 pub fn dimensions() -> Option<(usize, usize)> {
     let w = unsafe { c::dimensions() };
 
-    // If either of the dimensions is 0 then the command failed,
-    // usually because output isn't to a terminal (instead to a file
-    // or pipe or something)
     if w.ws_col == 0 || w.ws_row == 0 {
         None
     }