Ben S 10 лет назад
Родитель
Сommit
c938c699ec
10 измененных файлов с 483 добавлено и 403 удалено
  1. 13 13
      src/colours.rs
  2. 10 10
      src/column.rs
  3. 5 6
      src/dir.rs
  4. 27 29
      src/feature/git.rs
  5. 144 248
      src/file.rs
  6. 10 10
      src/filetype.rs
  7. 43 47
      src/options.rs
  8. 199 39
      src/output/details.rs
  9. 2 1
      src/output/lines.rs
  10. 30 0
      src/output/mod.rs

+ 13 - 13
src/colours.rs

@@ -28,19 +28,19 @@ pub struct Colours {
 pub struct FileTypes {
     pub normal: Style,
     pub directory: Style,
-	pub symlink: Style,
-	pub special: Style,
-	pub executable: Style,
-	pub image: Style,
-	pub video: Style,
-	pub music: Style,
-	pub lossless: Style,
-	pub crypto: Style,
-	pub document: Style,
-	pub compressed: Style,
-	pub temp: Style,
-	pub immediate: Style,
-	pub compiled: Style,
+    pub symlink: Style,
+    pub special: Style,
+    pub executable: Style,
+    pub image: Style,
+    pub video: Style,
+    pub music: Style,
+    pub lossless: Style,
+    pub crypto: Style,
+    pub document: Style,
+    pub compressed: Style,
+    pub temp: Style,
+    pub immediate: Style,
+    pub compiled: Style,
 }
 
 #[derive(Clone, Copy, Debug, Default, PartialEq)]

+ 10 - 10
src/column.rs

@@ -11,7 +11,7 @@ use unicode_width::UnicodeWidthStr;
 pub enum Column {
     Permissions,
     FileSize(SizeFormat),
-    Timestamp(TimeType, i64),
+    Timestamp(TimeType),
     Blocks,
     User,
     Group,
@@ -46,15 +46,15 @@ impl Column {
     /// 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",
+            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",
         }
     }
 }

+ 5 - 6
src/dir.rs

@@ -1,6 +1,5 @@
-use colours::Colours;
 use feature::Git;
-use file::File;
+use file::{File, fields};
 
 use std::io;
 use std::fs;
@@ -65,11 +64,11 @@ impl Dir {
     }
 
     /// Get a string describing the Git status of the given file.
-    pub fn git_status(&self, path: &Path, colours: &Colours, prefix_lookup: bool) -> String {
+    pub fn git_status(&self, path: &Path, prefix_lookup: bool) -> fields::Git {
         match (&self.git, prefix_lookup) {
-            (&Some(ref git), false)  => git.status(colours, path),
-            (&Some(ref git), true)   => git.dir_status(colours, path),
-            (&None, _)               => colours.punctuation.paint("--").to_string(),
+            (&Some(ref git), false)  => git.status(path),
+            (&Some(ref git), true)   => git.dir_status(path),
+            (&None, _)               => fields::Git::empty()
         }
     }
 }

+ 27 - 29
src/feature/git.rs

@@ -1,9 +1,8 @@
 use std::path::{Path, PathBuf};
 
-use ansi_term::{ANSIString, ANSIStrings};
 use git2;
 
-use colours::Colours;
+use file::fields;
 
 /// Container of Git statuses for all the files in this folder's Git repository.
 pub struct Git {
@@ -29,49 +28,48 @@ impl Git {
     }
 
     /// Get the status for the file at the given path, if present.
-    pub fn status(&self, c: &Colours, path: &Path) -> String {
+    pub fn status(&self, path: &Path) -> fields::Git {
         let status = self.statuses.iter()
                                   .find(|p| p.0.as_path() == path);
         match status {
-            Some(&(_, s)) => ANSIStrings( &[Git::index_status(c, s), Git::working_tree_status(c, s) ]).to_string(),
-            None => c.punctuation.paint("--").to_string(),
+            Some(&(_, s)) =>  fields::Git { staged: index_status(s),        unstaged: working_tree_status(s) },
+            None          =>  fields::Git { staged: fields::GitStatus::NotModified, unstaged: fields::GitStatus::NotModified }
         }
     }
 
     /// Get the combined status for all the files whose paths begin with the
     /// path that gets passed in. This is used for getting the status of
     /// directories, which don't really have an 'official' status.
-    pub fn dir_status(&self, c: &Colours, dir: &Path) -> String {
+    pub fn dir_status(&self, dir: &Path) -> fields::Git {
         let s = self.statuses.iter()
                              .filter(|p| p.0.starts_with(dir))
                              .fold(git2::Status::empty(), |a, b| a | b.1);
 
-        ANSIStrings( &[Git::index_status(c, s), Git::working_tree_status(c, s)] ).to_string()
+        fields::Git { staged: index_status(s), unstaged: working_tree_status(s) }
     }
+}
 
-    /// The character to display if the file has been modified, but not staged.
-    fn working_tree_status(colours: &Colours, status: git2::Status) -> ANSIString<'static> {
-        match status {
-            s if s.contains(git2::STATUS_WT_NEW) => colours.git.new.paint("A"),
-            s if s.contains(git2::STATUS_WT_MODIFIED) => colours.git.modified.paint("M"),
-            s if s.contains(git2::STATUS_WT_DELETED) => colours.git.deleted.paint("D"),
-            s if s.contains(git2::STATUS_WT_RENAMED) => colours.git.renamed.paint("R"),
-            s if s.contains(git2::STATUS_WT_TYPECHANGE) => colours.git.typechange.paint("T"),
-            _ => colours.punctuation.paint("-"),
-        }
+/// The character to display if the file has been modified, but not staged.
+fn working_tree_status(status: git2::Status) -> fields::GitStatus {
+    match status {
+        s if s.contains(git2::STATUS_WT_NEW)         => fields::GitStatus::New,
+        s if s.contains(git2::STATUS_WT_MODIFIED)    => fields::GitStatus::Modified,
+        s if s.contains(git2::STATUS_WT_DELETED)     => fields::GitStatus::Deleted,
+        s if s.contains(git2::STATUS_WT_RENAMED)     => fields::GitStatus::Renamed,
+        s if s.contains(git2::STATUS_WT_TYPECHANGE)  => fields::GitStatus::TypeChange,
+        _                                            => fields::GitStatus::NotModified,
     }
+}
 
-    /// The character to display if the file has been modified, and the change
-    /// has been staged.
-    fn index_status(colours: &Colours, status: git2::Status) -> ANSIString<'static> {
-        match status {
-            s if s.contains(git2::STATUS_INDEX_NEW) => colours.git.new.paint("A"),
-            s if s.contains(git2::STATUS_INDEX_MODIFIED) => colours.git.modified.paint("M"),
-            s if s.contains(git2::STATUS_INDEX_DELETED) => colours.git.deleted.paint("D"),
-            s if s.contains(git2::STATUS_INDEX_RENAMED) => colours.git.renamed.paint("R"),
-            s if s.contains(git2::STATUS_INDEX_TYPECHANGE) => colours.git.typechange.paint("T"),
-            _ => colours.punctuation.paint("-"),
-        }
+/// The character to display if the file has been modified, and the change
+/// has been staged.
+fn index_status(status: git2::Status) -> fields::GitStatus {
+    match status {
+        s if s.contains(git2::STATUS_INDEX_NEW)         => fields::GitStatus::New,
+        s if s.contains(git2::STATUS_INDEX_MODIFIED)    => fields::GitStatus::Modified,
+        s if s.contains(git2::STATUS_INDEX_DELETED)     => fields::GitStatus::Deleted,
+        s if s.contains(git2::STATUS_INDEX_RENAMED)     => fields::GitStatus::Renamed,
+        s if s.contains(git2::STATUS_INDEX_TYPECHANGE)  => fields::GitStatus::TypeChange,
+        _                                               => fields::GitStatus::NotModified,
     }
 }
-

+ 144 - 248
src/file.rs

@@ -3,34 +3,17 @@ use std::env::current_dir;
 use std::fs;
 use std::io;
 use std::os::unix;
-use std::os::unix::raw::mode_t;
 use std::os::unix::fs::{MetadataExt, PermissionsExt};
 use std::path::{Component, Path, PathBuf};
 
-use ansi_term::{ANSIString, ANSIStrings, Style};
-use ansi_term::Style::Plain;
-use ansi_term::Colour::Fixed;
-
-use users::Users;
-
-use locale;
-
 use unicode_width::UnicodeWidthStr;
 
-use number_prefix::{binary_prefix, decimal_prefix, Prefixed, Standalone, PrefixNames};
-
-use datetime::local::{LocalDateTime, DatePiece};
-use datetime::format::{DateFormat};
-
-use colours::Colours;
-use column::{Column, Cell};
-use column::Column::*;
 use dir::Dir;
-use filetype::file_colour;
-use options::{SizeFormat, TimeType};
-use output::details::UserLocale;
+use options::TimeType;
 use feature::Attribute;
 
+use self::fields as f;
+
 /// A **File** is a wrapper around one of Rust's Path objects, along with
 /// associated data about the file.
 ///
@@ -109,91 +92,24 @@ impl<'a> File<'a> {
         self.name.starts_with(".")
     }
 
-    /// Get the data for a column, formatted as a coloured string.
-    pub fn display<U: Users>(&self, column: &Column, colours: &Colours, users_cache: &mut U, locale: &UserLocale) -> Cell {
-        match *column {
-            Permissions     => self.permissions_string(colours),
-            FileSize(f)     => self.file_size(colours, f, &locale.numeric),
-            Timestamp(t, y) => self.timestamp(colours, t, y, &locale.time),
-            HardLinks       => self.hard_links(colours, &locale.numeric),
-            Inode           => self.inode(colours),
-            Blocks          => self.blocks(colours, &locale.numeric),
-            User            => self.user(colours, users_cache),
-            Group           => self.group(colours, users_cache),
-            GitStatus       => self.git_status(colours),
-        }
-    }
+    pub fn path_prefix(&self) -> String {
+        let path_bytes: Vec<Component> = self.path.components().collect();
+        let mut path_prefix = String::new();
 
-    /// 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,
-    /// with special formatting for a symlink.
-    pub fn file_name_view(&self, colours: &Colours) -> String {
-        if self.is_link() {
-            self.symlink_file_name_view(colours)
-        }
-        else {
-            file_colour(colours, self).paint(&*self.name).to_string()
-        }
-    }
+        if !path_bytes.is_empty() {
+            // Use init() to add all but the last component of the
+            // path to the prefix. init() panics when given an
+            // empty list, hence the check.
+            for component in path_bytes.init().iter() {
+                path_prefix.push_str(&*component.as_os_str().to_string_lossy());
 
-    /// If this file is a symlink, returns a string displaying its name,
-    /// and an arrow pointing to the file it links to, which is also
-    /// coloured in the appropriate style.
-    ///
-    /// If the symlink target doesn't exist, then instead of displaying
-    /// an error, highlight the target and arrow in red. The error would
-    /// be shown out of context, and it's almost always because the
-    /// target doesn't exist.
-    fn symlink_file_name_view(&self, colours: &Colours) -> String {
-        let name = &*self.name;
-        let style = file_colour(colours, self);
-
-        if let Ok(path) = fs::read_link(&self.path) {
-            let target_path = match self.dir {
-                Some(dir) => dir.join(&*path),
-                None => path,
-            };
-
-            match self.target_file(&target_path) {
-                Ok(file) => {
-
-                    // Generate a preview for the path this symlink links to.
-                    // The preview should consist of the directory of the file
-                    // (if present) in cyan, an extra slash if necessary, then
-                    // the target file, colourised in the appropriate style.
-                    let mut path_prefix = String::new();
-
-                    let path_bytes: Vec<Component> = file.path.components().collect();
-                    if !path_bytes.is_empty() {
-                        // Use init() to add all but the last component of the
-                        // path to the prefix. init() panics when given an
-                        // empty list, hence the check.
-                        for component in path_bytes.init().iter() {
-                            path_prefix.push_str(&*component.as_os_str().to_string_lossy());
-
-                            if component != &Component::RootDir {
-                                path_prefix.push_str("/");
-                            }
-                        }
-                    }
-
-                    format!("{} {} {}",
-                            style.paint(name),
-                            colours.punctuation.paint("=>"),
-                            ANSIStrings(&[ colours.symlink_path.paint(&path_prefix),
-                                           file_colour(colours, &file).paint(&file.name) ]))
-                },
-                Err(filename) => format!("{} {} {}",
-                                         style.paint(name),
-                                         colours.broken_arrow.paint("=>"),
-                                         colours.broken_filename.paint(&filename)),
+                if component != &Component::RootDir {
+                    path_prefix.push_str("/");
+                }
             }
         }
-        else {
-            style.paint(name).to_string()
-        }
+
+        path_prefix
     }
 
     /// The Unicode 'display width' of the filename.
@@ -211,17 +127,27 @@ impl<'a> File<'a> {
     /// 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.
-    fn target_file(&self, target_path: &Path) -> Result<File, String> {
-        let filename = path_filename(target_path);
+    pub fn link_target(&self) -> Result<File, String> {
+        let path = match fs::read_link(&self.path) {
+            Ok(path)  => path,
+            Err(_)    => return Err(self.name.clone()),
+        };
+
+        let target_path = match self.dir {
+            Some(dir)  => dir.join(&*path),
+            None       => path
+        };
+
+        let filename = path_filename(&target_path);
 
         // Use plain `metadata` instead of `symlink_metadata` - we *want* to follow links.
-        if let Ok(stat) = fs::metadata(target_path) {
+        if let Ok(stat) = fs::metadata(&target_path) {
             Ok(File {
                 path:   target_path.to_path_buf(),
                 dir:    self.dir,
                 stat:   stat,
                 ext:    ext(&filename),
-                xattrs: Attribute::list(target_path).unwrap_or(Vec::new()),
+                xattrs: Attribute::list(&target_path).unwrap_or(Vec::new()),
                 name:   filename.to_string(),
                 this:   None,
             })
@@ -232,76 +158,38 @@ impl<'a> File<'a> {
     }
 
     /// This file's number of hard links as a coloured string.
-    fn hard_links(&self, colours: &Colours, locale: &locale::Numeric) -> Cell {
-        let style = if self.has_multiple_links() { colours.links.multi_link_file }
-                                            else { colours.links.normal };
-
-        Cell::paint(style, &locale.format_int(self.stat.as_raw().nlink())[..])
-    }
-
-    /// 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.is_file() && self.stat.as_raw().nlink() > 1
+    pub fn links(&self) -> f::Links {
+        let count = self.stat.as_raw().nlink();
+
+        f::Links {
+            count: count,
+            multiple: self.is_file() && count > 1,
+        }
     }
 
-    /// This file's inode as a coloured string.
-    fn inode(&self, colours: &Colours) -> Cell {
-        let inode = self.stat.as_raw().ino();
-        Cell::paint(colours.inode, &inode.to_string()[..])
+    pub fn inode(&self) -> f::Inode {
+        f::Inode(self.stat.as_raw().ino())
     }
 
-    /// This file's number of filesystem blocks (if available) as a coloured string.
-    fn blocks(&self, colours: &Colours, locale: &locale::Numeric) -> Cell {
+    pub fn blocks(&self) -> f::Blocks {
         if self.is_file() || self.is_link() {
-            Cell::paint(colours.blocks, &locale.format_int(self.stat.as_raw().blocks())[..])
+            f::Blocks::Some(self.stat.as_raw().blocks())
         }
         else {
-            Cell { text: colours.punctuation.paint("-").to_string(), length: 1 }
+            f::Blocks::None
         }
     }
 
-    /// 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, colours: &Colours, users_cache: &mut U) -> Cell {
-        let uid = self.stat.as_raw().uid();
-
-        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 { colours.users.user_you } else { colours.users.user_someone_else };
-        Cell::paint(style, &*user_name)
+    pub fn user(&self) -> f::User {
+        f::User(self.stat.as_raw().uid())
     }
 
-    /// 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, colours: &Colours, users_cache: &mut U) -> Cell {
-        let gid = self.stat.as_raw().gid();
-        let mut style = colours.users.group_not_yours;
-
-        let group_name = match users_cache.get_group_by_gid(gid as u32) {
-            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 = colours.users.group_yours;
-                    }
-                }
-                group.name
-            },
-            None => gid.to_string(),
-        };
-
-        Cell::paint(style, &*group_name)
+    pub fn group(&self) -> f::Group {
+        f::Group(self.stat.as_raw().gid())
     }
 
     /// This file's size, formatted using the given way, as a coloured string.
@@ -310,118 +198,63 @@ impl<'a> File<'a> {
     /// 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, colours: &Colours, size_format: SizeFormat, locale: &locale::Numeric) -> Cell {
+    pub fn size(&self) -> f::Size {
         if self.is_directory() {
-            Cell { text: colours.punctuation.paint("-").to_string(), length: 1 }
+            f::Size::None
         }
         else {
-            let result = match size_format {
-                SizeFormat::DecimalBytes => decimal_prefix(self.stat.len() as f64),
-                SizeFormat::BinaryBytes  => binary_prefix(self.stat.len() as f64),
-                SizeFormat::JustBytes    => return Cell::paint(colours.size.numbers, &locale.format_int(self.stat.len())[..]),
-            };
-
-            match result {
-                Standalone(bytes) => Cell::paint(colours.size.numbers, &*bytes.to_string()),
-                Prefixed(prefix, n) => {
-                    let number = if n < 10f64 { locale.format_float(n, 1) } else { locale.format_int(n as isize) };
-                    let symbol = prefix.symbol();
-
-                    Cell {
-                        text: ANSIStrings( &[ colours.size.unit.paint(&number[..]), colours.size.unit.paint(symbol) ]).to_string(),
-                        length: number.len() + symbol.len(),
-                    }
-                }
-            }
+            f::Size::Some(self.stat.len())
         }
     }
 
-    fn timestamp(&self, colours: &Colours, time_type: TimeType, current_year: i64, locale: &locale::Time) -> Cell {
-
+    pub fn timestamp(&self, time_type: TimeType) -> f::Time {
         let time_in_seconds = match time_type {
             TimeType::FileAccessed => self.stat.as_raw().atime(),
             TimeType::FileModified => self.stat.as_raw().mtime(),
             TimeType::FileCreated  => self.stat.as_raw().ctime(),
-        } as i64;
-
-        let date = LocalDateTime::at(time_in_seconds);
-
-        let format = if date.year() == current_year {
-                DateFormat::parse("{2>:D} {:M} {2>:h}:{02>:m}").unwrap()
-            }
-            else {
-                DateFormat::parse("{2>:D} {:M} {5>:Y}").unwrap()
-            };
+        };
 
-        Cell::paint(colours.date, &format.format(date, locale))
+        f::Time(time_in_seconds)
     }
 
     /// 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, colours: &Colours) -> ANSIString {
+    fn type_char(&self) -> f::Type {
         if self.is_file() {
-            colours.filetypes.normal.paint(".")
+            f::Type::File
         }
         else if self.is_directory() {
-            colours.filetypes.directory.paint("d")
+            f::Type::Directory
         }
         else if self.is_pipe() {
-            colours.filetypes.special.paint("|")
+            f::Type::Pipe
         }
         else if self.is_link() {
-            colours.filetypes.symlink.paint("l")
+            f::Type::Link
         }
         else {
-            colours.filetypes.special.paint("?")
+            f::Type::Special
         }
     }
 
-    /// Marker indicating that the file contains extended attributes
-    ///
-    /// Returns "@" or  " " depending on wheter the file contains an extented
-    /// attribute or not. Also returns " " in case the attributes cannot be read
-    /// for some reason.
-    fn attribute_marker(&self, colours: &Colours) -> ANSIString {
-        if self.xattrs.len() > 0 { colours.perms.attribute.paint("@") } else { Plain.paint(" ") }
-    }
-
-    /// 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, colours: &Colours) -> Cell {
-
+    pub fn permissions(&self) -> f::Permissions {
         let bits = self.stat.permissions().mode();
-        let executable_colour = if self.is_file() { colours.perms.user_execute_file }
-                                             else { colours.perms.user_execute_other };
-
-        let string = ANSIStrings(&[
-            self.type_char(colours),
-            File::permission_bit(bits, unix::fs::USER_READ,     "r", colours.perms.user_read),
-            File::permission_bit(bits, unix::fs::USER_WRITE,    "w", colours.perms.user_write),
-            File::permission_bit(bits, unix::fs::USER_EXECUTE,  "x", executable_colour),
-            File::permission_bit(bits, unix::fs::GROUP_READ,    "r", colours.perms.group_read),
-            File::permission_bit(bits, unix::fs::GROUP_WRITE,   "w", colours.perms.group_write),
-            File::permission_bit(bits, unix::fs::GROUP_EXECUTE, "x", colours.perms.group_execute),
-            File::permission_bit(bits, unix::fs::OTHER_READ,    "r", colours.perms.other_read),
-            File::permission_bit(bits, unix::fs::OTHER_WRITE,   "w", colours.perms.other_write),
-            File::permission_bit(bits, unix::fs::OTHER_EXECUTE, "x", colours.perms.other_execute),
-            self.attribute_marker(colours)
-        ]).to_string();
-
-        Cell { text: string, length: 11 }
-    }
-
-    /// Helper method for the permissions string.
-    fn permission_bit(bits: mode_t, bit: mode_t, character: &'static str, style: Style) -> ANSIString<'static> {
-        if bits & bit == bit {
-            style.paint(character)
-        }
-        else {
-            Fixed(244).paint("-")
+        let has_bit = |bit| { bits & bit == bit };
+
+        f::Permissions {
+            file_type:      self.type_char(),
+            user_read:      has_bit(unix::fs::USER_READ),
+            user_write:     has_bit(unix::fs::USER_WRITE),
+            user_execute:   has_bit(unix::fs::USER_EXECUTE),
+            group_read:     has_bit(unix::fs::GROUP_READ),
+            group_write:    has_bit(unix::fs::GROUP_WRITE),
+            group_execute:  has_bit(unix::fs::GROUP_EXECUTE),
+            other_read:     has_bit(unix::fs::OTHER_READ),
+            other_write:    has_bit(unix::fs::OTHER_WRITE),
+            other_execute:  has_bit(unix::fs::OTHER_EXECUTE),
+            attribute:      !self.xattrs.is_empty()
         }
     }
 
@@ -471,20 +304,18 @@ impl<'a> File<'a> {
         choices.contains(&&self.name[..])
     }
 
-    fn git_status(&self, colours: &Colours) -> Cell {
-        let status = match self.dir {
-            None    => colours.punctuation.paint("--").to_string(),
+    pub fn git_status(&self) -> f::Git {
+        match self.dir {
+            None    => f::Git { staged: f::GitStatus::NotModified, unstaged: f::GitStatus::NotModified },
             Some(d) => {
                 let cwd = match current_dir() {
                     Err(_)  => Path::new(".").join(&self.path),
                     Ok(dir) => dir.join(&self.path),
                 };
 
-                d.git_status(&cwd, colours, self.is_directory())
+                d.git_status(&cwd, self.is_directory())
             },
-        };
-
-        Cell { text: status, length: 2 }
+        }
     }
 }
 
@@ -513,6 +344,71 @@ fn ext<'a>(name: &'a str) -> Option<String> {
     name.rfind('.').map(|p| name[p+1..].to_ascii_lowercase())
 }
 
+pub mod fields {
+    use std::os::unix::raw::{blkcnt_t, gid_t, ino_t, nlink_t, time_t, uid_t};
+
+    pub enum Type {
+        File, Directory, Pipe, Link, Special,
+    }
+
+    pub struct Permissions {
+        pub file_type:      Type,
+        pub user_read:      bool,
+        pub user_write:     bool,
+        pub user_execute:   bool,
+        pub group_read:     bool,
+        pub group_write:    bool,
+        pub group_execute:  bool,
+        pub other_read:     bool,
+        pub other_write:    bool,
+        pub other_execute:  bool,
+        pub attribute:      bool,
+    }
+
+    pub struct Links {
+        pub count: nlink_t,
+        pub multiple: bool,
+    }
+
+    pub struct Inode(pub ino_t);
+
+    pub enum Blocks {
+        Some(blkcnt_t),
+        None,
+    }
+
+    pub struct User(pub uid_t);
+
+    pub struct Group(pub gid_t);
+
+    pub enum Size {
+        Some(u64),
+        None,
+    }
+
+    pub struct Time(pub time_t);
+
+    pub enum GitStatus {
+        NotModified,
+        New,
+        Modified,
+        Deleted,
+        Renamed,
+        TypeChange,
+    }
+
+    pub struct Git {
+        pub staged:   GitStatus,
+        pub unstaged: GitStatus,
+    }
+
+    impl Git {
+        pub fn empty() -> Git {
+            Git { staged: GitStatus::NotModified, unstaged: GitStatus::NotModified }
+        }
+    }
+}
+
 #[cfg(broken_test)]
 pub mod test {
     pub use super::*;

+ 10 - 10
src/filetype.rs

@@ -43,7 +43,7 @@ impl<'_> FileTypes for File<'_> {
             "build.gradle", "Rakefile", "Gruntfile.js",
             "Gruntfile.coffee",
         ])
-	}
+    }
 
     fn is_image(&self) -> bool {
         self.extension_is_one_of( &[
@@ -52,33 +52,33 @@ impl<'_> FileTypes for File<'_> {
             "svg", "stl", "eps", "dvi", "ps", "cbr",
             "cbz", "xpm", "ico",
         ])
-	}
+    }
 
     fn is_video(&self) -> bool {
         self.extension_is_one_of( &[
             "avi", "flv", "m2v", "mkv", "mov", "mp4", "mpeg",
             "mpg", "ogm", "ogv", "vob", "wmv",
         ])
-	}
+    }
 
     fn is_music(&self) -> bool {
         self.extension_is_one_of( &[
             "aac", "m4a", "mp3", "ogg", "wma",
         ])
-	}
+    }
 
     fn is_lossless(&self) -> bool {
         self.extension_is_one_of( &[
             "alac", "ape", "flac", "wav",
         ])
-	}
+    }
 
     fn is_crypto(&self) -> bool {
         self.extension_is_one_of( &[
             "zip", "tar", "Z", "gz", "bz2", "a", "ar", "7z",
             "iso", "dmg", "tc", "rar", "par",
         ])
-	}
+    }
 
     fn is_document(&self) -> bool {
         self.extension_is_one_of( &[
@@ -86,20 +86,20 @@ impl<'_> FileTypes for File<'_> {
             "odp", "odt", "pdf", "ppt", "pptx", "rtf",
             "xls", "xlsx",
         ])
-	}
+    }
 
     fn is_compressed(&self) -> bool {
         self.extension_is_one_of( &[
             "zip", "tar", "Z", "gz", "bz2", "a", "ar", "7z",
             "iso", "dmg", "tc", "rar", "par"
         ])
-	}
+    }
 
     fn is_temp(&self) -> bool {
         self.name.ends_with("~")
             || (self.name.starts_with("#") && self.name.ends_with("#"))
             || self.extension_is_one_of( &[ "tmp", "swp", "swo", "swn", "bak" ])
-	}
+    }
 
     fn is_compiled(&self) -> bool {
         if self.extension_is_one_of( &[ "class", "elc", "hi", "o", "pyc" ]) {
@@ -111,7 +111,7 @@ impl<'_> FileTypes for File<'_> {
         else {
             false
         }
-	}
+    }
 }
 
 #[cfg(broken_test)]

+ 43 - 47
src/options.rs

@@ -15,8 +15,6 @@ use std::os::unix::fs::MetadataExt;
 use getopts;
 use natord;
 
-use datetime::local::{LocalDateTime, DatePiece};
-
 use self::Misfire::*;
 
 /// The *Options* struct represents a parsed version of the user's
@@ -137,17 +135,17 @@ impl FileFilter {
         }
 
         match self.sort_field {
-            SortField::Unsorted => {},
-            SortField::Name => files.sort_by(|a, b| natord::compare(&*a.name, &*b.name)),
-            SortField::Size => files.sort_by(|a, b| a.stat.len().cmp(&b.stat.len())),
-            SortField::FileInode => files.sort_by(|a, b| a.stat.as_raw().ino().cmp(&b.stat.as_raw().ino())),
-            SortField::Extension => files.sort_by(|a, b| match a.ext.cmp(&b.ext) {
-                Ordering::Equal => natord::compare(&*a.name, &*b.name),
-                order => order
+            SortField::Unsorted      => {},
+            SortField::Name          => files.sort_by(|a, b| natord::compare(&*a.name, &*b.name)),
+            SortField::Size          => files.sort_by(|a, b| a.stat.len().cmp(&b.stat.len())),
+            SortField::FileInode     => files.sort_by(|a, b| a.stat.as_raw().ino().cmp(&b.stat.as_raw().ino())),
+            SortField::ModifiedDate  => files.sort_by(|a, b| a.stat.as_raw().mtime().cmp(&b.stat.as_raw().mtime())),
+            SortField::AccessedDate  => files.sort_by(|a, b| a.stat.as_raw().atime().cmp(&b.stat.as_raw().atime())),
+            SortField::CreatedDate   => files.sort_by(|a, b| a.stat.as_raw().ctime().cmp(&b.stat.as_raw().ctime())),
+            SortField::Extension     => files.sort_by(|a, b| match a.ext.cmp(&b.ext) {
+                Ordering::Equal  => natord::compare(&*a.name, &*b.name),
+                order            => order,
             }),
-            SortField::ModifiedDate => files.sort_by(|a, b| a.stat.as_raw().mtime().cmp(&b.stat.as_raw().mtime())),
-            SortField::AccessedDate => files.sort_by(|a, b| a.stat.as_raw().atime().cmp(&b.stat.as_raw().atime())),
-            SortField::CreatedDate  => files.sort_by(|a, b| a.stat.as_raw().ctime().cmp(&b.stat.as_raw().ctime())),
         }
 
         if self.reverse {
@@ -173,15 +171,15 @@ 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))
+            "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))
         }
     }
 
@@ -231,14 +229,14 @@ impl Misfire {
 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),
-            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),
+            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),
         }
     }
 }
@@ -351,10 +349,10 @@ impl SizeFormat {
         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),
+            (true,  true )  => Err(Misfire::Conflict("binary", "bytes")),
+            (true,  false)  => Ok(SizeFormat::BinaryBytes),
+            (false, true )  => Ok(SizeFormat::JustBytes),
+            (false, false)  => Ok(SizeFormat::DecimalBytes),
         }
     }
 }
@@ -369,9 +367,9 @@ pub enum TimeType {
 impl TimeType {
     pub fn header(&self) -> &'static str {
         match *self {
-            TimeType::FileAccessed => "Date Accessed",
-            TimeType::FileModified => "Date Modified",
-            TimeType::FileCreated  => "Date Created",
+            TimeType::FileAccessed  => "Date Accessed",
+            TimeType::FileModified  => "Date Modified",
+            TimeType::FileCreated   => "Date Created",
         }
     }
 }
@@ -441,12 +439,12 @@ impl DirAction {
         let tree    = matches.opt_present("tree");
 
         match (recurse, list, tree) {
-            (true,  true,  _    ) => Err(Misfire::Conflict("recurse", "list-dirs")),
-            (_,     true,  true ) => Err(Misfire::Conflict("tree", "list-dirs")),
-            (true,  false, false) => Ok(DirAction::Recurse(try!(RecurseOptions::deduce(matches, false)))),
-            (_   ,  _,     true ) => Ok(DirAction::Recurse(try!(RecurseOptions::deduce(matches, true)))),
-            (false, true,  _    ) => Ok(DirAction::AsFile),
-            (false, false, _    ) => Ok(DirAction::List),
+            (true,  true,  _    )  => Err(Misfire::Conflict("recurse", "list-dirs")),
+            (_,     true,  true )  => Err(Misfire::Conflict("tree", "list-dirs")),
+            (true,  false, false)  => Ok(DirAction::Recurse(try!(RecurseOptions::deduce(matches, false)))),
+            (_   ,  _,     true )  => Ok(DirAction::Recurse(try!(RecurseOptions::deduce(matches, true)))),
+            (false, true,  _    )  => Ok(DirAction::AsFile),
+            (false, false, _    )  => Ok(DirAction::List),
         }
     }
 
@@ -555,18 +553,16 @@ impl Columns {
             columns.push(Group);
         }
 
-        let current_year = LocalDateTime::now().year();
-
         if self.time_types.modified {
-            columns.push(Timestamp(TimeType::FileModified, current_year));
+            columns.push(Timestamp(TimeType::FileModified));
         }
 
         if self.time_types.created {
-            columns.push(Timestamp(TimeType::FileCreated, current_year));
+            columns.push(Timestamp(TimeType::FileCreated));
         }
 
         if self.time_types.accessed {
-            columns.push(Timestamp(TimeType::FileAccessed, current_year));
+            columns.push(Timestamp(TimeType::FileAccessed));
         }
 
         if cfg!(feature="git") {

+ 199 - 39
src/output/details.rs

@@ -3,11 +3,22 @@ use column::{Alignment, Column, Cell};
 use feature::Attribute;
 use dir::Dir;
 use file::File;
-use options::{Columns, FileFilter, RecurseOptions};
-use users::OSUsers;
+use file::fields as f;
+use options::{Columns, FileFilter, RecurseOptions, SizeFormat};
+use users::{OSUsers, Users};
+
+use super::filename;
+
+use ansi_term::{ANSIString, ANSIStrings, Style};
+use ansi_term::Style::Plain;
 
 use locale;
 
+use number_prefix::{binary_prefix, decimal_prefix, Prefixed, Standalone, PrefixNames};
+
+use datetime::local::{LocalDateTime, DatePiece};
+use datetime::format::{DateFormat};
+
 /// With the **Details** view, the output gets formatted into columns, with
 /// each `Column` object showing some piece of information about the file,
 /// such as its size, or its permissions.
@@ -109,11 +120,14 @@ struct Row {
 /// A **Table** object gets built up by the view as it lists files and
 /// directories.
 struct Table {
-    columns: Vec<Column>,
-    users:   OSUsers,
-    locale:  UserLocale,
-    rows:    Vec<Row>,
-    colours: Colours,
+    columns:  Vec<Column>,
+    rows:     Vec<Row>,
+
+    time:         locale::Time,
+    numeric:      locale::Numeric,
+    users:        OSUsers,
+    colours:      Colours,
+    current_year: i64,
 }
 
 impl Table {
@@ -122,10 +136,13 @@ impl Table {
     fn with_options(colours: Colours, columns: Vec<Column>) -> Table {
         Table {
             columns: columns,
-            users: OSUsers::empty_cache(),
-            locale: UserLocale::new(),
-            rows: Vec::new(),
-            colours: colours,
+            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()),
+            users:        OSUsers::empty_cache(),
+            colours:      colours,
+            current_year: LocalDateTime::now().year(),
         }
     }
 
@@ -145,26 +162,183 @@ impl Table {
         self.rows.push(row);
     }
 
-    /// Use the list of columns to find which cells should be produced for
-    /// this file, per-column.
-    fn cells_for_file(&mut self, file: &File) -> Vec<Cell> {
-        self.columns.clone().iter()
-                    .map(|c| file.display(c, &self.colours, &mut self.users, &self.locale))
-                    .collect()
-    }
-
     /// Get the cells for the given file, and add the result to the table.
     fn add_file(&mut self, file: &File, depth: usize, last: bool) {
         let row = Row {
             depth:    depth,
             cells:    self.cells_for_file(file),
-            name:     file.file_name_view(&self.colours),
+            name:     filename(file, &self.colours),
             last:     last,
             attrs:    file.xattrs.clone(),
             children: file.this.is_some(),
         };
 
-        self.rows.push(row)
+        self.rows.push(row);
+    }
+
+    /// Use the list of columns to find which cells should be produced for
+    /// this file, per-column.
+    fn cells_for_file(&mut self, file: &File) -> Vec<Cell> {
+        self.columns.clone().iter()
+                    .map(|c| self.display(file, c))
+                    .collect()
+    }
+
+    fn display(&mut self, file: &File, column: &Column) -> Cell {
+        match *column {
+            Column::Permissions    => self.render_permissions(file.permissions()),
+            Column::FileSize(fmt)  => self.render_size(file.size(), fmt),
+            Column::Timestamp(t)   => self.render_time(file.timestamp(t)),
+            Column::HardLinks      => self.render_links(file.links()),
+            Column::Inode          => self.render_inode(file.inode()),
+            Column::Blocks         => self.render_blocks(file.blocks()),
+            Column::User           => self.render_user(file.user()),
+            Column::Group          => self.render_group(file.group()),
+            Column::GitStatus      => self.render_git_status(file.git_status()),
+        }
+    }
+
+    fn render_permissions(&self, permissions: f::Permissions) -> Cell {
+        let c = self.colours.perms;
+        let bit = |bit, chr: &'static str, style: Style| {
+            if bit { style.paint(chr) } else { self.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("?"),
+        };
+
+        let x_colour = if let f::Type::File = permissions.file_type { c.user_execute_file }
+                                                            else { c.user_execute_other };
+
+        let string = ANSIStrings( &[
+            file_type,
+            bit(permissions.user_read,     "r", c.user_read),
+            bit(permissions.user_write,    "w", c.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),
+            if permissions.attribute { c.attribute.paint("@") } else { Plain.paint(" ") },
+        ]).to_string();
+
+        Cell {
+            text: string,
+            length: 11,
+        }
+    }
+
+    fn render_links(&self, links: f::Links) -> Cell {
+        let style = if links.multiple { self.colours.links.multi_link_file }
+                                 else { self.colours.links.normal };
+
+        Cell::paint(style, &self.numeric.format_int(links.count))
+    }
+
+    fn render_blocks(&self, blocks: f::Blocks) -> Cell {
+        match blocks {
+            f::Blocks::Some(blocks)  => Cell::paint(self.colours.blocks, &blocks.to_string()),
+            f::Blocks::None          => Cell::paint(self.colours.punctuation, "-"),
+        }
+    }
+
+    fn render_inode(&self, inode: f::Inode) -> Cell {
+        Cell::paint(self.colours.inode, &inode.0.to_string())
+    }
+
+    fn render_size(&self, size: f::Size, size_format: SizeFormat) -> Cell {
+        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)),
+            };
+
+            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();
+
+                    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, "-")
+        }
+    }
+
+    fn render_time(&self, timestamp: f::Time) -> Cell {
+        let date = LocalDateTime::at(timestamp.0);
+
+        let format = if date.year() == self.current_year {
+                DateFormat::parse("{2>:D} {:M} {2>:h}:{02>:m}").unwrap()
+            }
+            else {
+                DateFormat::parse("{2>:D} {:M} {5>:Y}").unwrap()
+            };
+
+        Cell::paint(self.colours.date, &format.format(date, &self.time))
+    }
+
+    fn render_git_status(&self, git: f::Git) -> Cell {
+        Cell {
+            text: ANSIStrings(&[ self.render_git_char(git.staged),
+                                 self.render_git_char(git.unstaged) ]).to_string(),
+            length: 2,
+        }
+    }
+
+    fn render_git_char(&self, status: f::GitStatus) -> ANSIString {
+        match status {
+            f::GitStatus::NotModified  => self.colours.punctuation.paint("-"),
+            f::GitStatus::New          => self.colours.git.new.paint("N"),
+            f::GitStatus::Modified     => self.colours.git.modified.paint("M"),
+            f::GitStatus::Deleted      => self.colours.git.deleted.paint("D"),
+            f::GitStatus::Renamed      => self.colours.git.renamed.paint("R"),
+            f::GitStatus::TypeChange   => self.colours.git.typechange.paint("T"),
+        }
+    }
+
+    fn render_user(&mut self, user: f::User) -> Cell {
+        let user_name = match self.users.get_user_by_uid(user.0) {
+            Some(user)  => user.name,
+            None        => user.0.to_string(),
+        };
+
+        let style = if self.users.get_current_uid() == user.0 { self.colours.users.user_you }
+                                                               else { self.colours.users.user_someone_else };
+        Cell::paint(style, &*user_name)
+    }
+
+    fn render_group(&mut self, group: f::Group) -> Cell {
+        let mut style = self.colours.users.group_not_yours;
+
+        let group_name = match self.users.get_group_by_gid(group.0) {
+            Some(group) => {
+                let current_uid = self.users.get_current_uid();
+                if let Some(current_user) = self.users.get_user_by_uid(current_uid) {
+                    if current_user.primary_group == group.gid || group.members.contains(&current_user.name) {
+                        style = self.colours.users.group_yours;
+                    }
+                }
+                group.name
+            },
+            None => group.0.to_string(),
+        };
+
+        Cell::paint(style, &*group_name)
     }
 
     /// Print the table to standard output, consuming it in the process.
@@ -243,24 +417,10 @@ enum TreePart {
 impl TreePart {
     fn ascii_art(&self) -> &'static str {
         match *self {
-            TreePart::Edge   => "├──",
-            TreePart::Line   => "│  ",
-            TreePart::Corner => "└──",
-            TreePart::Blank  => "   ",
-        }
-    }
-}
-
-pub struct UserLocale {
-    pub time:    locale::Time,
-    pub numeric: locale::Numeric,
-}
-
-impl UserLocale {
-    pub fn new() -> UserLocale {
-        UserLocale {
-            time:    locale::Time::load_user_locale().unwrap_or_else(|_| locale::Time::english()),
-            numeric: locale::Numeric::load_user_locale().unwrap_or_else(|_| locale::Numeric::english()),
+            TreePart::Edge    => "├──",
+            TreePart::Line    => "│  ",
+            TreePart::Corner  => "└──",
+            TreePart::Blank   => "   ",
         }
     }
 }

+ 2 - 1
src/output/lines.rs

@@ -1,5 +1,6 @@
 use colours::Colours;
 use file::File;
+use super::filename;
 
 #[derive(Clone, Copy, Debug, PartialEq)]
 pub struct Lines {
@@ -10,7 +11,7 @@ pub struct Lines {
 impl Lines {
     pub fn view(&self, files: &[File]) {
         for file in files {
-            println!("{}", file.file_name_view(&self.colours));
+            println!("{}", filename(file, &self.colours));
         }
     }
 }

+ 30 - 0
src/output/mod.rs

@@ -2,6 +2,36 @@ mod grid;
 pub mod details;
 mod lines;
 
+use colours::Colours;
+use file::File;
+use filetype::file_colour;
+use ansi_term::ANSIStrings;
+
 pub use self::grid::Grid;
 pub use self::details::Details;
 pub use self::lines::Lines;
+
+pub fn filename(file: &File, colours: &Colours) -> String {
+    if file.is_link() {
+        symlink_filename(file, colours)
+    }
+    else {
+        let style = file_colour(colours, file);
+        style.paint(&file.name).to_string()
+    }
+}
+
+fn symlink_filename(file: &File, colours: &Colours) -> String {
+    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)),
+    }
+}