diff --git a/src/dir.rs b/src/dir.rs index 9433829..438bef7 100644 --- a/src/dir.rs +++ b/src/dir.rs @@ -1,6 +1,7 @@ use colours::Colours; use feature::Git; use file::File; +use file; use std::io; use std::fs; @@ -65,11 +66,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) -> file::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, _) => file::Git::empty() } } } diff --git a/src/feature/git.rs b/src/feature/git.rs index db9cc0e..17ef7d8 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; /// 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) -> file::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)) => file::Git { staged: index_status(s), unstaged: working_tree_status(s) }, + None => file::Git { staged: file::GitStatus::NotModified, unstaged: file::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) -> file::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("-"), - } + file::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) -> file::GitStatus { + match status { + s if s.contains(git2::STATUS_WT_NEW) => file::GitStatus::New, + s if s.contains(git2::STATUS_WT_MODIFIED) => file::GitStatus::Modified, + s if s.contains(git2::STATUS_WT_DELETED) => file::GitStatus::Deleted, + s if s.contains(git2::STATUS_WT_RENAMED) => file::GitStatus::Renamed, + s if s.contains(git2::STATUS_WT_TYPECHANGE) => file::GitStatus::TypeChange, + _ => file::GitStatus::NotModified, + } +} + +/// The character to display if the file has been modified, and the change +/// has been staged. +fn index_status(status: git2::Status) -> file::GitStatus { + match status { + s if s.contains(git2::STATUS_INDEX_NEW) => file::GitStatus::New, + s if s.contains(git2::STATUS_INDEX_MODIFIED) => file::GitStatus::Modified, + s if s.contains(git2::STATUS_INDEX_DELETED) => file::GitStatus::Deleted, + s if s.contains(git2::STATUS_INDEX_RENAMED) => file::GitStatus::Renamed, + s if s.contains(git2::STATUS_INDEX_TYPECHANGE) => file::GitStatus::TypeChange, + _ => file::GitStatus::NotModified, + } +} diff --git a/src/file.rs b/src/file.rs index b179e76..e728834 100644 --- a/src/file.rs +++ b/src/file.rs @@ -3,34 +3,77 @@ 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::raw::{blkcnt_t, gid_t, ino_t, mode_t, nlink_t, time_t, uid_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; +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 } + } +} + /// A **File** is a wrapper around one of Rust's Path objects, along with /// associated data about the file. /// @@ -109,91 +152,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 +187,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 +218,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) -> Links { + let count = self.stat.as_raw().nlink(); + + 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) -> Inode { + 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) -> Blocks { if self.is_file() || self.is_link() { - Cell::paint(colours.blocks, &locale.format_int(self.stat.as_raw().blocks())[..]) + Blocks::Some(self.stat.as_raw().blocks()) } else { - Cell { text: colours.punctuation.paint("-").to_string(), length: 1 } + 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) -> User { + 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) -> Group { + Group(self.stat.as_raw().gid()) } /// This file's size, formatted using the given way, as a coloured string. @@ -310,118 +258,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) -> Size { if self.is_directory() { - Cell { text: colours.punctuation.paint("-").to_string(), length: 1 } + 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(), - } - } - } + 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) -> 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)) + 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) -> Type { if self.is_file() { - colours.filetypes.normal.paint(".") + Type::File } else if self.is_directory() { - colours.filetypes.directory.paint("d") + Type::Directory } else if self.is_pipe() { - colours.filetypes.special.paint("|") + Type::Pipe } else if self.is_link() { - colours.filetypes.symlink.paint("l") + Type::Link } else { - colours.filetypes.special.paint("?") + 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) -> 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("-") + 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 +364,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) -> Git { + match self.dir { + None => Git { staged: GitStatus::NotModified, unstaged: 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 } + } } } diff --git a/src/options.rs b/src/options.rs index bff0d12..7163201 100644 --- a/src/options.rs +++ b/src/options.rs @@ -508,7 +508,7 @@ impl RecurseOptions { #[derive(PartialEq, Copy, Clone, Debug)] pub struct Columns { - size_format: SizeFormat, + pub size_format: SizeFormat, time_types: TimeTypes, inode: bool, links: bool, diff --git a/src/output/details.rs b/src/output/details.rs index ebe43ae..979c99d 100644 --- a/src/output/details.rs +++ b/src/output/details.rs @@ -2,12 +2,23 @@ use colours::Colours; use column::{Alignment, Column, Cell}; use feature::Attribute; use dir::Dir; -use file::File; -use options::{Columns, FileFilter, RecurseOptions}; -use users::OSUsers; +use file::{Blocks, File, Git, GitStatus, Group, Inode, Links, Permissions, Size, Time, User}; +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 ansi_term::Colour::Fixed; 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. @@ -45,7 +56,7 @@ impl Details { pub fn view(&self, dir: Option<&Dir>, files: &[File]) { // First, transform the Columns object into a vector of columns for // the current directory. - let mut table = Table::with_options(self.colours, self.columns.for_dir(dir)); + let mut table = Table::with_options(self.colours, self.columns.for_dir(dir), self.columns.size_format); if self.header { table.add_header() } // Then add files to the table and print it out. @@ -109,21 +120,26 @@ 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, + + local: Locals, + colours: Colours, } impl Table { /// Create a new, empty Table object, setting the caching fields to their /// empty states. - fn with_options(colours: Colours, columns: Vec) -> Table { + fn with_options(colours: Colours, columns: Vec, size: SizeFormat) -> Table { Table { columns: columns, - users: OSUsers::empty_cache(), - locale: UserLocale::new(), + local: Locals { + 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(), + current_year: LocalDateTime::now().year(), + size_format: size, + }, rows: Vec::new(), colours: colours, } @@ -149,16 +165,30 @@ impl Table { /// 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)) + .map(|c| self.display(file, c)) .collect() } + fn display(&mut self, file: &File, column: &Column) -> Cell { + match *column { + Column::Permissions => file.permissions().render(&self.colours, &mut self.local), + Column::FileSize(f) => file.size().render(&self.colours, &mut self.local), + Column::Timestamp(t, y) => file.timestamp(t).render(&self.colours, &mut self.local), + Column::HardLinks => file.links().render(&self.colours, &mut self.local), + Column::Inode => file.inode().render(&self.colours, &mut self.local), + Column::Blocks => file.blocks().render(&self.colours, &mut self.local), + Column::User => file.user().render(&self.colours, &mut self.local), + Column::Group => file.group().render(&self.colours, &mut self.local), + Column::GitStatus => file.git_status().render(&self.colours, &mut self.local), + } + } + /// 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(), @@ -251,16 +281,162 @@ impl TreePart { } } -pub struct UserLocale { - pub time: locale::Time, - pub numeric: locale::Numeric, +pub trait Render { + fn render(self, colours: &Colours, local: &mut Locals) -> Cell; } -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()), +impl Render for Permissions { + fn render(self, colours: &Colours, local: &mut Locals) -> Cell { + let c = colours.perms; + let bit = |bit, chr: &'static str, style: Style| { + if bit { style.paint(chr) } else { colours.punctuation.paint("-") } + }; + + let string = ANSIStrings( &[ + //self.file_type.render(colours, local), + bit(self.user_read, "r", c.user_read), + bit(self.user_write, "w", c.user_write), + bit(self.user_execute, "x", c.user_execute_file), + bit(self.group_read, "r", c.group_read), + bit(self.group_write, "w", c.group_write), + bit(self.group_execute, "x", c.group_execute), + bit(self.other_read, "r", c.other_read), + bit(self.other_write, "w", c.other_write), + bit(self.other_execute, "x", c.other_execute), + if self.attribute { c.attribute.paint("@") } else { Plain.paint(" ") }, + ]).to_string(); + + Cell { + text: string, + length: 11, } } } + +impl Render for Links { + fn render(self, colours: &Colours, local: &mut Locals) -> Cell { + let style = if self.multiple { colours.links.multi_link_file } + else { colours.links.normal }; + Cell::paint(style, &local.numeric.format_int(self.count)) + } +} + +impl Render for Blocks { + fn render(self, colours: &Colours, local: &mut Locals) -> Cell { + match self { + Blocks::Some(blocks) => Cell::paint(colours.blocks, &blocks.to_string()), + Blocks::None => Cell::paint(colours.punctuation, "-"), + } + } +} + +impl Render for Inode { + fn render(self, colours: &Colours, local: &mut Locals) -> Cell { + Cell::paint(colours.inode, &self.0.to_string()) + } +} + +impl Render for Size { + fn render(self, colours: &Colours, local: &mut Locals) -> Cell { + if let Size::Some(offset) = self { + let result = match local.size_format { + SizeFormat::DecimalBytes => decimal_prefix(offset as f64), + SizeFormat::BinaryBytes => binary_prefix(offset as f64), + SizeFormat::JustBytes => return Cell::paint(colours.size.numbers, &local.numeric.format_int(offset)), + }; + + match result { + Standalone(bytes) => Cell::paint(colours.size.numbers, &*bytes.to_string()), + Prefixed(prefix, n) => { + let number = if n < 10f64 { local.numeric.format_float(n, 1) } else { local.numeric.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(), + } + } + } + } + else { + Cell::paint(colours.punctuation, "-") + } + } +} + +impl Render for Time { + fn render(self, colours: &Colours, local: &mut Locals) -> Cell { + let date = LocalDateTime::at(self.0); + + let format = if date.year() == local.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, &local.time)) + } +} + +impl Render for Git { + fn render(self, colours: &Colours, local: &mut Locals) -> Cell { + let render_char = |chr| { + match chr { + GitStatus::NotModified => colours.punctuation.paint("-"), + GitStatus::New => colours.git.renamed.paint("N"), + GitStatus::Modified => colours.git.renamed.paint("M"), + GitStatus::Deleted => colours.git.renamed.paint("D"), + GitStatus::Renamed => colours.git.renamed.paint("R"), + GitStatus::TypeChange => colours.git.renamed.paint("T"), + } + }; + + Cell { + text: ANSIStrings(&[ render_char(self.staged), render_char(self.unstaged) ]).to_string(), + length: 2, + } + } +} + +impl Render for User { + fn render(self, colours: &Colours, local: &mut Locals) -> Cell { + let user_name = match local.users.get_user_by_uid(self.0) { + Some(user) => user.name, + None => self.0.to_string(), + }; + + let style = if local.users.get_current_uid() == self.0 { colours.users.user_you } + else { colours.users.user_someone_else }; + Cell::paint(style, &*user_name) + } +} + +impl Render for Group { + fn render(self, colours: &Colours, local: &mut Locals) -> Cell { + let mut style = colours.users.group_not_yours; + + let group_name = match local.users.get_group_by_gid(self.0) { + Some(group) => { + let current_uid = local.users.get_current_uid(); + if let Some(current_user) = local.users.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 => self.0.to_string(), + }; + + Cell::paint(style, &*group_name) + } +} + +pub struct Locals { + pub time: locale::Time, + pub numeric: locale::Numeric, + pub users: OSUsers, + pub size_format: SizeFormat, + pub current_year: i64, +} 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)), + } +}