diff --git a/src/colours.rs b/src/colours.rs index 352b13c..2e51e61 100644 --- a/src/colours.rs +++ b/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)] diff --git a/src/column.rs b/src/column.rs index 73692e3..4bacba1 100644 --- a/src/column.rs +++ b/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", } } } diff --git a/src/dir.rs b/src/dir.rs index 9433829..7bbeda7 100644 --- a/src/dir.rs +++ b/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() } } } diff --git a/src/feature/git.rs b/src/feature/git.rs index db9cc0e..59c3a8b 100644 --- a/src/feature/git.rs +++ b/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() - } - - /// 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, 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("-"), - } + 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(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(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, + } +} diff --git a/src/file.rs b/src/file.rs index b179e76..cd1589e 100644 --- a/src/file.rs +++ b/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(&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 = 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 = 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 { - let filename = path_filename(target_path); + pub fn link_target(&self) -> Result { + 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(&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(&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(¤t_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 has_bit = |bit| { bits & bit == bit }; - 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("-") + 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 { 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::*; diff --git a/src/filetype.rs b/src/filetype.rs index a75f842..d9ea071 100644 --- a/src/filetype.rs +++ b/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)] diff --git a/src/options.rs b/src/options.rs index bff0d12..1fb843f 100644 --- a/src/options.rs +++ b/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 { 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") { diff --git a/src/output/details.rs b/src/output/details.rs index ebe43ae..28c8228 100644 --- a/src/output/details.rs +++ b/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, - users: OSUsers, - locale: UserLocale, - rows: Vec, - colours: Colours, + columns: Vec, + rows: Vec, + + 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) -> 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 { - 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 { + 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(¤t_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 => " ", } } } diff --git a/src/output/lines.rs b/src/output/lines.rs index b61494f..bd1fcdc 100644 --- a/src/output/lines.rs +++ b/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)); } } } diff --git a/src/output/mod.rs b/src/output/mod.rs index 8606d23..ebb11ce 100644 --- a/src/output/mod.rs +++ b/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)), + } +}