Turn File into simply a data source

And move all the rendering, ansi_term, colourful stuff into the output modules, which is the only place they should be used!
This commit is contained in:
Ben S 2015-05-11 23:28:01 +01:00
parent bc844a3843
commit dc6599b1b3
7 changed files with 404 additions and 307 deletions

View File

@ -1,6 +1,7 @@
use colours::Colours; use colours::Colours;
use feature::Git; use feature::Git;
use file::File; use file::File;
use file;
use std::io; use std::io;
use std::fs; use std::fs;
@ -65,11 +66,11 @@ impl Dir {
} }
/// Get a string describing the Git status of the given file. /// 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) { match (&self.git, prefix_lookup) {
(&Some(ref git), false) => git.status(colours, path), (&Some(ref git), false) => git.status(path),
(&Some(ref git), true) => git.dir_status(colours, path), (&Some(ref git), true) => git.dir_status(path),
(&None, _) => colours.punctuation.paint("--").to_string(), (&None, _) => file::Git::empty()
} }
} }
} }

View File

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

View File

@ -3,34 +3,77 @@ use std::env::current_dir;
use std::fs; use std::fs;
use std::io; use std::io;
use std::os::unix; 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::os::unix::fs::{MetadataExt, PermissionsExt};
use std::path::{Component, Path, PathBuf}; 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 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 dir::Dir;
use filetype::file_colour; use options::TimeType;
use options::{SizeFormat, TimeType};
use output::details::UserLocale;
use feature::Attribute; 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 /// A **File** is a wrapper around one of Rust's Path objects, along with
/// associated data about the file. /// associated data about the file.
/// ///
@ -109,63 +152,10 @@ impl<'a> File<'a> {
self.name.starts_with(".") self.name.starts_with(".")
} }
/// Get the data for a column, formatted as a coloured string. pub fn path_prefix(&self) -> String {
pub fn display<U: Users>(&self, column: &Column, colours: &Colours, users_cache: &mut U, locale: &UserLocale) -> Cell { let path_bytes: Vec<Component> = self.path.components().collect();
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),
}
}
/// 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 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 mut path_prefix = String::new();
let path_bytes: Vec<Component> = file.path.components().collect();
if !path_bytes.is_empty() { if !path_bytes.is_empty() {
// Use init() to add all but the last component of the // Use init() to add all but the last component of the
// path to the prefix. init() panics when given an // path to the prefix. init() panics when given an
@ -179,21 +169,7 @@ impl<'a> File<'a> {
} }
} }
format!("{} {} {}", path_prefix
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)),
}
}
else {
style.paint(name).to_string()
}
} }
/// The Unicode 'display width' of the filename. /// 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 /// If statting the file fails (usually because the file on the
/// other end doesn't exist), returns the *filename* of the file /// other end doesn't exist), returns the *filename* of the file
/// that should be there. /// that should be there.
fn target_file(&self, target_path: &Path) -> Result<File, String> { pub fn link_target(&self) -> Result<File, String> {
let filename = path_filename(target_path); 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. // 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 { Ok(File {
path: target_path.to_path_buf(), path: target_path.to_path_buf(),
dir: self.dir, dir: self.dir,
stat: stat, stat: stat,
ext: ext(&filename), 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(), name: filename.to_string(),
this: None, this: None,
}) })
@ -232,76 +218,38 @@ impl<'a> File<'a> {
} }
/// This file's number of hard links as a coloured string. /// 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, /// This is important, because a file with multiple links is uncommon,
/// while you can come across directories and other types with multiple /// while you can come across directories and other types with multiple
/// links much more often. /// links much more often.
fn has_multiple_links(&self) -> bool { pub fn links(&self) -> Links {
self.is_file() && self.stat.as_raw().nlink() > 1 let count = self.stat.as_raw().nlink();
Links {
count: count,
multiple: self.is_file() && count > 1,
}
} }
/// This file's inode as a coloured string. pub fn inode(&self) -> Inode {
fn inode(&self, colours: &Colours) -> Cell { Inode(self.stat.as_raw().ino())
let inode = self.stat.as_raw().ino();
Cell::paint(colours.inode, &inode.to_string()[..])
} }
/// This file's number of filesystem blocks (if available) as a coloured string. pub fn blocks(&self) -> Blocks {
fn blocks(&self, colours: &Colours, locale: &locale::Numeric) -> Cell {
if self.is_file() || self.is_link() { 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 { else {
Cell { text: colours.punctuation.paint("-").to_string(), length: 1 } Blocks::None
} }
} }
/// This file's owner's username as a coloured string. pub fn user(&self) -> User {
/// User(self.stat.as_raw().uid())
/// 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)
} }
/// This file's group name as a coloured string. pub fn group(&self) -> Group {
/// Group(self.stat.as_raw().gid())
/// 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)
} }
/// This file's size, formatted using the given way, as a coloured string. /// 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 /// 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 /// any information from it, so by emitting "-" instead, the table is less
/// cluttered with numbers. /// 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() { if self.is_directory() {
Cell { text: colours.punctuation.paint("-").to_string(), length: 1 } Size::None
} }
else { else {
let result = match size_format { Size::Some(self.stat.len())
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(),
}
}
}
} }
} }
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 { let time_in_seconds = match time_type {
TimeType::FileAccessed => self.stat.as_raw().atime(), TimeType::FileAccessed => self.stat.as_raw().atime(),
TimeType::FileModified => self.stat.as_raw().mtime(), TimeType::FileModified => self.stat.as_raw().mtime(),
TimeType::FileCreated => self.stat.as_raw().ctime(), 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. /// This file's type, represented by a coloured character.
/// ///
/// Although the file type can usually be guessed from the colour of the /// Although the file type can usually be guessed from the colour of the
/// file, `ls` puts this character there, so people will expect it. /// 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() { if self.is_file() {
colours.filetypes.normal.paint(".") Type::File
} }
else if self.is_directory() { else if self.is_directory() {
colours.filetypes.directory.paint("d") Type::Directory
} }
else if self.is_pipe() { else if self.is_pipe() {
colours.filetypes.special.paint("|") Type::Pipe
} }
else if self.is_link() { else if self.is_link() {
colours.filetypes.symlink.paint("l") Type::Link
} }
else { else {
colours.filetypes.special.paint("?") Type::Special
} }
} }
/// Marker indicating that the file contains extended attributes pub fn permissions(&self) -> Permissions {
///
/// 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 {
let bits = self.stat.permissions().mode(); let bits = self.stat.permissions().mode();
let executable_colour = if self.is_file() { colours.perms.user_execute_file } let has_bit = |bit| { bits & bit == bit };
else { colours.perms.user_execute_other };
let string = ANSIStrings(&[ Permissions {
self.type_char(colours), file_type: self.type_char(),
File::permission_bit(bits, unix::fs::USER_READ, "r", colours.perms.user_read), user_read: has_bit(unix::fs::USER_READ),
File::permission_bit(bits, unix::fs::USER_WRITE, "w", colours.perms.user_write), user_write: has_bit(unix::fs::USER_WRITE),
File::permission_bit(bits, unix::fs::USER_EXECUTE, "x", executable_colour), user_execute: has_bit(unix::fs::USER_EXECUTE),
File::permission_bit(bits, unix::fs::GROUP_READ, "r", colours.perms.group_read), group_read: has_bit(unix::fs::GROUP_READ),
File::permission_bit(bits, unix::fs::GROUP_WRITE, "w", colours.perms.group_write), group_write: has_bit(unix::fs::GROUP_WRITE),
File::permission_bit(bits, unix::fs::GROUP_EXECUTE, "x", colours.perms.group_execute), group_execute: has_bit(unix::fs::GROUP_EXECUTE),
File::permission_bit(bits, unix::fs::OTHER_READ, "r", colours.perms.other_read), other_read: has_bit(unix::fs::OTHER_READ),
File::permission_bit(bits, unix::fs::OTHER_WRITE, "w", colours.perms.other_write), other_write: has_bit(unix::fs::OTHER_WRITE),
File::permission_bit(bits, unix::fs::OTHER_EXECUTE, "x", colours.perms.other_execute), other_execute: has_bit(unix::fs::OTHER_EXECUTE),
self.attribute_marker(colours) attribute: !self.xattrs.is_empty()
]).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("-")
} }
} }
@ -471,20 +364,18 @@ impl<'a> File<'a> {
choices.contains(&&self.name[..]) choices.contains(&&self.name[..])
} }
fn git_status(&self, colours: &Colours) -> Cell { pub fn git_status(&self) -> Git {
let status = match self.dir { match self.dir {
None => colours.punctuation.paint("--").to_string(), None => Git { staged: GitStatus::NotModified, unstaged: GitStatus::NotModified },
Some(d) => { Some(d) => {
let cwd = match current_dir() { let cwd = match current_dir() {
Err(_) => Path::new(".").join(&self.path), Err(_) => Path::new(".").join(&self.path),
Ok(dir) => dir.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 }
} }
} }

View File

@ -508,7 +508,7 @@ impl RecurseOptions {
#[derive(PartialEq, Copy, Clone, Debug)] #[derive(PartialEq, Copy, Clone, Debug)]
pub struct Columns { pub struct Columns {
size_format: SizeFormat, pub size_format: SizeFormat,
time_types: TimeTypes, time_types: TimeTypes,
inode: bool, inode: bool,
links: bool, links: bool,

View File

@ -2,12 +2,23 @@ use colours::Colours;
use column::{Alignment, Column, Cell}; use column::{Alignment, Column, Cell};
use feature::Attribute; use feature::Attribute;
use dir::Dir; use dir::Dir;
use file::File; use file::{Blocks, File, Git, GitStatus, Group, Inode, Links, Permissions, Size, Time, User};
use options::{Columns, FileFilter, RecurseOptions}; use options::{Columns, FileFilter, RecurseOptions, SizeFormat};
use users::OSUsers; 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 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 /// With the **Details** view, the output gets formatted into columns, with
/// each `Column` object showing some piece of information about the file, /// each `Column` object showing some piece of information about the file,
/// such as its size, or its permissions. /// such as its size, or its permissions.
@ -45,7 +56,7 @@ impl Details {
pub fn view(&self, dir: Option<&Dir>, files: &[File]) { pub fn view(&self, dir: Option<&Dir>, files: &[File]) {
// First, transform the Columns object into a vector of columns for // First, transform the Columns object into a vector of columns for
// the current directory. // 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() } if self.header { table.add_header() }
// Then add files to the table and print it out. // Then add files to the table and print it out.
@ -110,20 +121,25 @@ struct Row {
/// directories. /// directories.
struct Table { struct Table {
columns: Vec<Column>, columns: Vec<Column>,
users: OSUsers,
locale: UserLocale,
rows: Vec<Row>, rows: Vec<Row>,
local: Locals,
colours: Colours, colours: Colours,
} }
impl Table { impl Table {
/// Create a new, empty Table object, setting the caching fields to their /// Create a new, empty Table object, setting the caching fields to their
/// empty states. /// empty states.
fn with_options(colours: Colours, columns: Vec<Column>) -> Table { fn with_options(colours: Colours, columns: Vec<Column>, size: SizeFormat) -> Table {
Table { Table {
columns: columns, columns: columns,
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(), users: OSUsers::empty_cache(),
locale: UserLocale::new(), current_year: LocalDateTime::now().year(),
size_format: size,
},
rows: Vec::new(), rows: Vec::new(),
colours: colours, colours: colours,
} }
@ -149,16 +165,30 @@ impl Table {
/// this file, per-column. /// this file, per-column.
fn cells_for_file(&mut self, file: &File) -> Vec<Cell> { fn cells_for_file(&mut self, file: &File) -> Vec<Cell> {
self.columns.clone().iter() self.columns.clone().iter()
.map(|c| file.display(c, &self.colours, &mut self.users, &self.locale)) .map(|c| self.display(file, c))
.collect() .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. /// 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) { fn add_file(&mut self, file: &File, depth: usize, last: bool) {
let row = Row { let row = Row {
depth: depth, depth: depth,
cells: self.cells_for_file(file), cells: self.cells_for_file(file),
name: file.file_name_view(&self.colours), name: filename(file, &self.colours),
last: last, last: last,
attrs: file.xattrs.clone(), attrs: file.xattrs.clone(),
children: file.this.is_some(), children: file.this.is_some(),
@ -251,16 +281,162 @@ impl TreePart {
} }
} }
pub struct UserLocale { pub trait Render {
pub time: locale::Time, fn render(self, colours: &Colours, local: &mut Locals) -> Cell;
pub numeric: locale::Numeric,
} }
impl UserLocale { impl Render for Permissions {
pub fn new() -> UserLocale { fn render(self, colours: &Colours, local: &mut Locals) -> Cell {
UserLocale { let c = colours.perms;
time: locale::Time::load_user_locale().unwrap_or_else(|_| locale::Time::english()), let bit = |bit, chr: &'static str, style: Style| {
numeric: locale::Numeric::load_user_locale().unwrap_or_else(|_| locale::Numeric::english()), 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(&current_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,
}

View File

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

View File

@ -2,6 +2,36 @@ mod grid;
pub mod details; pub mod details;
mod lines; mod lines;
use colours::Colours;
use file::File;
use filetype::file_colour;
use ansi_term::ANSIStrings;
pub use self::grid::Grid; pub use self::grid::Grid;
pub use self::details::Details; pub use self::details::Details;
pub use self::lines::Lines; 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)),
}
}