mirror of
https://github.com/Llewellynvdm/exa.git
synced 2024-09-28 21:29:02 +00:00
Merge branch 'restructuring'
This commit is contained in:
commit
c938c699ec
@ -28,19 +28,19 @@ pub struct Colours {
|
|||||||
pub struct FileTypes {
|
pub struct FileTypes {
|
||||||
pub normal: Style,
|
pub normal: Style,
|
||||||
pub directory: Style,
|
pub directory: Style,
|
||||||
pub symlink: Style,
|
pub symlink: Style,
|
||||||
pub special: Style,
|
pub special: Style,
|
||||||
pub executable: Style,
|
pub executable: Style,
|
||||||
pub image: Style,
|
pub image: Style,
|
||||||
pub video: Style,
|
pub video: Style,
|
||||||
pub music: Style,
|
pub music: Style,
|
||||||
pub lossless: Style,
|
pub lossless: Style,
|
||||||
pub crypto: Style,
|
pub crypto: Style,
|
||||||
pub document: Style,
|
pub document: Style,
|
||||||
pub compressed: Style,
|
pub compressed: Style,
|
||||||
pub temp: Style,
|
pub temp: Style,
|
||||||
pub immediate: Style,
|
pub immediate: Style,
|
||||||
pub compiled: Style,
|
pub compiled: Style,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||||
|
@ -11,7 +11,7 @@ use unicode_width::UnicodeWidthStr;
|
|||||||
pub enum Column {
|
pub enum Column {
|
||||||
Permissions,
|
Permissions,
|
||||||
FileSize(SizeFormat),
|
FileSize(SizeFormat),
|
||||||
Timestamp(TimeType, i64),
|
Timestamp(TimeType),
|
||||||
Blocks,
|
Blocks,
|
||||||
User,
|
User,
|
||||||
Group,
|
Group,
|
||||||
@ -46,15 +46,15 @@ impl Column {
|
|||||||
/// to have a header row printed.
|
/// to have a header row printed.
|
||||||
pub fn header(&self) -> &'static str {
|
pub fn header(&self) -> &'static str {
|
||||||
match *self {
|
match *self {
|
||||||
Column::Permissions => "Permissions",
|
Column::Permissions => "Permissions",
|
||||||
Column::FileSize(_) => "Size",
|
Column::FileSize(_) => "Size",
|
||||||
Column::Timestamp(t, _) => t.header(),
|
Column::Timestamp(t) => t.header(),
|
||||||
Column::Blocks => "Blocks",
|
Column::Blocks => "Blocks",
|
||||||
Column::User => "User",
|
Column::User => "User",
|
||||||
Column::Group => "Group",
|
Column::Group => "Group",
|
||||||
Column::HardLinks => "Links",
|
Column::HardLinks => "Links",
|
||||||
Column::Inode => "inode",
|
Column::Inode => "inode",
|
||||||
Column::GitStatus => "Git",
|
Column::GitStatus => "Git",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
11
src/dir.rs
11
src/dir.rs
@ -1,6 +1,5 @@
|
|||||||
use colours::Colours;
|
|
||||||
use feature::Git;
|
use feature::Git;
|
||||||
use file::File;
|
use file::{File, fields};
|
||||||
|
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@ -65,11 +64,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) -> fields::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, _) => fields::Git::empty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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::fields;
|
||||||
|
|
||||||
/// 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) -> fields::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)) => fields::Git { staged: index_status(s), unstaged: working_tree_status(s) },
|
||||||
None => c.punctuation.paint("--").to_string(),
|
None => fields::Git { staged: fields::GitStatus::NotModified, unstaged: fields::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) -> fields::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()
|
fields::Git { staged: index_status(s), unstaged: working_tree_status(s) }
|
||||||
}
|
|
||||||
|
|
||||||
/// The character to display if the file has been modified, but not staged.
|
|
||||||
fn working_tree_status(colours: &Colours, status: git2::Status) -> ANSIString<'static> {
|
|
||||||
match status {
|
|
||||||
s if s.contains(git2::STATUS_WT_NEW) => colours.git.new.paint("A"),
|
|
||||||
s if s.contains(git2::STATUS_WT_MODIFIED) => colours.git.modified.paint("M"),
|
|
||||||
s if s.contains(git2::STATUS_WT_DELETED) => colours.git.deleted.paint("D"),
|
|
||||||
s if s.contains(git2::STATUS_WT_RENAMED) => colours.git.renamed.paint("R"),
|
|
||||||
s if s.contains(git2::STATUS_WT_TYPECHANGE) => colours.git.typechange.paint("T"),
|
|
||||||
_ => colours.punctuation.paint("-"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The character to display if the file has been modified, and the change
|
|
||||||
/// has been staged.
|
|
||||||
fn index_status(colours: &Colours, status: git2::Status) -> ANSIString<'static> {
|
|
||||||
match status {
|
|
||||||
s if s.contains(git2::STATUS_INDEX_NEW) => colours.git.new.paint("A"),
|
|
||||||
s if s.contains(git2::STATUS_INDEX_MODIFIED) => colours.git.modified.paint("M"),
|
|
||||||
s if s.contains(git2::STATUS_INDEX_DELETED) => colours.git.deleted.paint("D"),
|
|
||||||
s if s.contains(git2::STATUS_INDEX_RENAMED) => colours.git.renamed.paint("R"),
|
|
||||||
s if s.contains(git2::STATUS_INDEX_TYPECHANGE) => colours.git.typechange.paint("T"),
|
|
||||||
_ => colours.punctuation.paint("-"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The character to display if the file has been modified, 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
390
src/file.rs
390
src/file.rs
@ -3,34 +3,17 @@ 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::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;
|
||||||
|
|
||||||
|
use self::fields as f;
|
||||||
|
|
||||||
/// 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,91 +92,24 @@ 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 {
|
let mut path_prefix = String::new();
|
||||||
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
|
if !path_bytes.is_empty() {
|
||||||
/// views, but *not* in the grid view.
|
// Use init() to add all but the last component of the
|
||||||
///
|
// path to the prefix. init() panics when given an
|
||||||
/// It consists of the file name coloured in the appropriate style,
|
// empty list, hence the check.
|
||||||
/// with special formatting for a symlink.
|
for component in path_bytes.init().iter() {
|
||||||
pub fn file_name_view(&self, colours: &Colours) -> String {
|
path_prefix.push_str(&*component.as_os_str().to_string_lossy());
|
||||||
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,
|
if component != &Component::RootDir {
|
||||||
/// and an arrow pointing to the file it links to, which is also
|
path_prefix.push_str("/");
|
||||||
/// coloured in the appropriate style.
|
}
|
||||||
///
|
|
||||||
/// If the symlink target doesn't exist, then instead of displaying
|
|
||||||
/// an error, highlight the target and arrow in red. The error would
|
|
||||||
/// be shown out of context, and it's almost always because the
|
|
||||||
/// target doesn't exist.
|
|
||||||
fn symlink_file_name_view(&self, colours: &Colours) -> String {
|
|
||||||
let name = &*self.name;
|
|
||||||
let style = file_colour(colours, self);
|
|
||||||
|
|
||||||
if let Ok(path) = fs::read_link(&self.path) {
|
|
||||||
let target_path = match self.dir {
|
|
||||||
Some(dir) => dir.join(&*path),
|
|
||||||
None => path,
|
|
||||||
};
|
|
||||||
|
|
||||||
match self.target_file(&target_path) {
|
|
||||||
Ok(file) => {
|
|
||||||
|
|
||||||
// Generate a preview for the path this symlink links to.
|
|
||||||
// The preview should consist of the directory of the file
|
|
||||||
// (if present) in cyan, an extra slash if necessary, then
|
|
||||||
// the target file, colourised in the appropriate style.
|
|
||||||
let mut path_prefix = String::new();
|
|
||||||
|
|
||||||
let path_bytes: Vec<Component> = file.path.components().collect();
|
|
||||||
if !path_bytes.is_empty() {
|
|
||||||
// Use init() to add all but the last component of the
|
|
||||||
// path to the prefix. init() panics when given an
|
|
||||||
// empty list, hence the check.
|
|
||||||
for component in path_bytes.init().iter() {
|
|
||||||
path_prefix.push_str(&*component.as_os_str().to_string_lossy());
|
|
||||||
|
|
||||||
if component != &Component::RootDir {
|
|
||||||
path_prefix.push_str("/");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
format!("{} {} {}",
|
|
||||||
style.paint(name),
|
|
||||||
colours.punctuation.paint("=>"),
|
|
||||||
ANSIStrings(&[ colours.symlink_path.paint(&path_prefix),
|
|
||||||
file_colour(colours, &file).paint(&file.name) ]))
|
|
||||||
},
|
|
||||||
Err(filename) => format!("{} {} {}",
|
|
||||||
style.paint(name),
|
|
||||||
colours.broken_arrow.paint("=>"),
|
|
||||||
colours.broken_filename.paint(&filename)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
style.paint(name).to_string()
|
path_prefix
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The Unicode 'display width' of the filename.
|
/// 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
|
/// 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 +158,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) -> f::Links {
|
||||||
self.is_file() && self.stat.as_raw().nlink() > 1
|
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.
|
pub fn inode(&self) -> f::Inode {
|
||||||
fn inode(&self, colours: &Colours) -> Cell {
|
f::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) -> f::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())[..])
|
f::Blocks::Some(self.stat.as_raw().blocks())
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Cell { text: colours.punctuation.paint("-").to_string(), length: 1 }
|
f::Blocks::None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This file's owner's username as a coloured string.
|
pub fn user(&self) -> f::User {
|
||||||
///
|
f::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) -> f::Group {
|
||||||
///
|
f::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(¤t_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 +198,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) -> f::Size {
|
||||||
if self.is_directory() {
|
if self.is_directory() {
|
||||||
Cell { text: colours.punctuation.paint("-").to_string(), length: 1 }
|
f::Size::None
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
let result = match size_format {
|
f::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) -> f::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);
|
f::Time(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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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) -> f::Type {
|
||||||
if self.is_file() {
|
if self.is_file() {
|
||||||
colours.filetypes.normal.paint(".")
|
f::Type::File
|
||||||
}
|
}
|
||||||
else if self.is_directory() {
|
else if self.is_directory() {
|
||||||
colours.filetypes.directory.paint("d")
|
f::Type::Directory
|
||||||
}
|
}
|
||||||
else if self.is_pipe() {
|
else if self.is_pipe() {
|
||||||
colours.filetypes.special.paint("|")
|
f::Type::Pipe
|
||||||
}
|
}
|
||||||
else if self.is_link() {
|
else if self.is_link() {
|
||||||
colours.filetypes.symlink.paint("l")
|
f::Type::Link
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
colours.filetypes.special.paint("?")
|
f::Type::Special
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Marker indicating that the file contains extended attributes
|
pub fn permissions(&self) -> f::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(&[
|
f::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 +304,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) -> f::Git {
|
||||||
let status = match self.dir {
|
match self.dir {
|
||||||
None => colours.punctuation.paint("--").to_string(),
|
None => f::Git { staged: f::GitStatus::NotModified, unstaged: f::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 }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -513,6 +344,71 @@ fn ext<'a>(name: &'a str) -> Option<String> {
|
|||||||
name.rfind('.').map(|p| name[p+1..].to_ascii_lowercase())
|
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)]
|
#[cfg(broken_test)]
|
||||||
pub mod test {
|
pub mod test {
|
||||||
pub use super::*;
|
pub use super::*;
|
||||||
|
@ -43,7 +43,7 @@ impl<'_> FileTypes for File<'_> {
|
|||||||
"build.gradle", "Rakefile", "Gruntfile.js",
|
"build.gradle", "Rakefile", "Gruntfile.js",
|
||||||
"Gruntfile.coffee",
|
"Gruntfile.coffee",
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_image(&self) -> bool {
|
fn is_image(&self) -> bool {
|
||||||
self.extension_is_one_of( &[
|
self.extension_is_one_of( &[
|
||||||
@ -52,33 +52,33 @@ impl<'_> FileTypes for File<'_> {
|
|||||||
"svg", "stl", "eps", "dvi", "ps", "cbr",
|
"svg", "stl", "eps", "dvi", "ps", "cbr",
|
||||||
"cbz", "xpm", "ico",
|
"cbz", "xpm", "ico",
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_video(&self) -> bool {
|
fn is_video(&self) -> bool {
|
||||||
self.extension_is_one_of( &[
|
self.extension_is_one_of( &[
|
||||||
"avi", "flv", "m2v", "mkv", "mov", "mp4", "mpeg",
|
"avi", "flv", "m2v", "mkv", "mov", "mp4", "mpeg",
|
||||||
"mpg", "ogm", "ogv", "vob", "wmv",
|
"mpg", "ogm", "ogv", "vob", "wmv",
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_music(&self) -> bool {
|
fn is_music(&self) -> bool {
|
||||||
self.extension_is_one_of( &[
|
self.extension_is_one_of( &[
|
||||||
"aac", "m4a", "mp3", "ogg", "wma",
|
"aac", "m4a", "mp3", "ogg", "wma",
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_lossless(&self) -> bool {
|
fn is_lossless(&self) -> bool {
|
||||||
self.extension_is_one_of( &[
|
self.extension_is_one_of( &[
|
||||||
"alac", "ape", "flac", "wav",
|
"alac", "ape", "flac", "wav",
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_crypto(&self) -> bool {
|
fn is_crypto(&self) -> bool {
|
||||||
self.extension_is_one_of( &[
|
self.extension_is_one_of( &[
|
||||||
"zip", "tar", "Z", "gz", "bz2", "a", "ar", "7z",
|
"zip", "tar", "Z", "gz", "bz2", "a", "ar", "7z",
|
||||||
"iso", "dmg", "tc", "rar", "par",
|
"iso", "dmg", "tc", "rar", "par",
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_document(&self) -> bool {
|
fn is_document(&self) -> bool {
|
||||||
self.extension_is_one_of( &[
|
self.extension_is_one_of( &[
|
||||||
@ -86,20 +86,20 @@ impl<'_> FileTypes for File<'_> {
|
|||||||
"odp", "odt", "pdf", "ppt", "pptx", "rtf",
|
"odp", "odt", "pdf", "ppt", "pptx", "rtf",
|
||||||
"xls", "xlsx",
|
"xls", "xlsx",
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_compressed(&self) -> bool {
|
fn is_compressed(&self) -> bool {
|
||||||
self.extension_is_one_of( &[
|
self.extension_is_one_of( &[
|
||||||
"zip", "tar", "Z", "gz", "bz2", "a", "ar", "7z",
|
"zip", "tar", "Z", "gz", "bz2", "a", "ar", "7z",
|
||||||
"iso", "dmg", "tc", "rar", "par"
|
"iso", "dmg", "tc", "rar", "par"
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_temp(&self) -> bool {
|
fn is_temp(&self) -> bool {
|
||||||
self.name.ends_with("~")
|
self.name.ends_with("~")
|
||||||
|| (self.name.starts_with("#") && self.name.ends_with("#"))
|
|| (self.name.starts_with("#") && self.name.ends_with("#"))
|
||||||
|| self.extension_is_one_of( &[ "tmp", "swp", "swo", "swn", "bak" ])
|
|| self.extension_is_one_of( &[ "tmp", "swp", "swo", "swn", "bak" ])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_compiled(&self) -> bool {
|
fn is_compiled(&self) -> bool {
|
||||||
if self.extension_is_one_of( &[ "class", "elc", "hi", "o", "pyc" ]) {
|
if self.extension_is_one_of( &[ "class", "elc", "hi", "o", "pyc" ]) {
|
||||||
@ -111,7 +111,7 @@ impl<'_> FileTypes for File<'_> {
|
|||||||
else {
|
else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(broken_test)]
|
#[cfg(broken_test)]
|
||||||
|
@ -15,8 +15,6 @@ use std::os::unix::fs::MetadataExt;
|
|||||||
use getopts;
|
use getopts;
|
||||||
use natord;
|
use natord;
|
||||||
|
|
||||||
use datetime::local::{LocalDateTime, DatePiece};
|
|
||||||
|
|
||||||
use self::Misfire::*;
|
use self::Misfire::*;
|
||||||
|
|
||||||
/// The *Options* struct represents a parsed version of the user's
|
/// The *Options* struct represents a parsed version of the user's
|
||||||
@ -137,17 +135,17 @@ impl FileFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match self.sort_field {
|
match self.sort_field {
|
||||||
SortField::Unsorted => {},
|
SortField::Unsorted => {},
|
||||||
SortField::Name => files.sort_by(|a, b| natord::compare(&*a.name, &*b.name)),
|
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::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::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) {
|
SortField::ModifiedDate => files.sort_by(|a, b| a.stat.as_raw().mtime().cmp(&b.stat.as_raw().mtime())),
|
||||||
Ordering::Equal => natord::compare(&*a.name, &*b.name),
|
SortField::AccessedDate => files.sort_by(|a, b| a.stat.as_raw().atime().cmp(&b.stat.as_raw().atime())),
|
||||||
order => order
|
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 {
|
if self.reverse {
|
||||||
@ -173,15 +171,15 @@ impl SortField {
|
|||||||
/// Find which field to use based on a user-supplied word.
|
/// Find which field to use based on a user-supplied word.
|
||||||
fn from_word(word: String) -> Result<SortField, Misfire> {
|
fn from_word(word: String) -> Result<SortField, Misfire> {
|
||||||
match &word[..] {
|
match &word[..] {
|
||||||
"name" | "filename" => Ok(SortField::Name),
|
"name" | "filename" => Ok(SortField::Name),
|
||||||
"size" | "filesize" => Ok(SortField::Size),
|
"size" | "filesize" => Ok(SortField::Size),
|
||||||
"ext" | "extension" => Ok(SortField::Extension),
|
"ext" | "extension" => Ok(SortField::Extension),
|
||||||
"mod" | "modified" => Ok(SortField::ModifiedDate),
|
"mod" | "modified" => Ok(SortField::ModifiedDate),
|
||||||
"acc" | "accessed" => Ok(SortField::AccessedDate),
|
"acc" | "accessed" => Ok(SortField::AccessedDate),
|
||||||
"cr" | "created" => Ok(SortField::CreatedDate),
|
"cr" | "created" => Ok(SortField::CreatedDate),
|
||||||
"none" => Ok(SortField::Unsorted),
|
"none" => Ok(SortField::Unsorted),
|
||||||
"inode" => Ok(SortField::FileInode),
|
"inode" => Ok(SortField::FileInode),
|
||||||
field => Err(SortField::none(field))
|
field => Err(SortField::none(field))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -231,14 +229,14 @@ impl Misfire {
|
|||||||
impl fmt::Display for Misfire {
|
impl fmt::Display for Misfire {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
match *self {
|
match *self {
|
||||||
InvalidOptions(ref e) => write!(f, "{}", e),
|
InvalidOptions(ref e) => write!(f, "{}", e),
|
||||||
Help(ref text) => write!(f, "{}", text),
|
Help(ref text) => write!(f, "{}", text),
|
||||||
Version => write!(f, "exa {}", env!("CARGO_PKG_VERSION")),
|
Version => write!(f, "exa {}", env!("CARGO_PKG_VERSION")),
|
||||||
Conflict(a, b) => write!(f, "Option --{} conflicts with option {}.", a, b),
|
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, false, b) => write!(f, "Option --{} is useless without option --{}.", a, b),
|
||||||
Useless(a, true, b) => write!(f, "Option --{} is useless given 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),
|
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),
|
FailedParse(ref e) => write!(f, "Failed to parse number: {}", e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -351,10 +349,10 @@ impl SizeFormat {
|
|||||||
let bytes = matches.opt_present("bytes");
|
let bytes = matches.opt_present("bytes");
|
||||||
|
|
||||||
match (binary, bytes) {
|
match (binary, bytes) {
|
||||||
(true, true ) => Err(Misfire::Conflict("binary", "bytes")),
|
(true, true ) => Err(Misfire::Conflict("binary", "bytes")),
|
||||||
(true, false) => Ok(SizeFormat::BinaryBytes),
|
(true, false) => Ok(SizeFormat::BinaryBytes),
|
||||||
(false, true ) => Ok(SizeFormat::JustBytes),
|
(false, true ) => Ok(SizeFormat::JustBytes),
|
||||||
(false, false) => Ok(SizeFormat::DecimalBytes),
|
(false, false) => Ok(SizeFormat::DecimalBytes),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -369,9 +367,9 @@ pub enum TimeType {
|
|||||||
impl TimeType {
|
impl TimeType {
|
||||||
pub fn header(&self) -> &'static str {
|
pub fn header(&self) -> &'static str {
|
||||||
match *self {
|
match *self {
|
||||||
TimeType::FileAccessed => "Date Accessed",
|
TimeType::FileAccessed => "Date Accessed",
|
||||||
TimeType::FileModified => "Date Modified",
|
TimeType::FileModified => "Date Modified",
|
||||||
TimeType::FileCreated => "Date Created",
|
TimeType::FileCreated => "Date Created",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -441,12 +439,12 @@ impl DirAction {
|
|||||||
let tree = matches.opt_present("tree");
|
let tree = matches.opt_present("tree");
|
||||||
|
|
||||||
match (recurse, list, tree) {
|
match (recurse, list, tree) {
|
||||||
(true, true, _ ) => Err(Misfire::Conflict("recurse", "list-dirs")),
|
(true, true, _ ) => Err(Misfire::Conflict("recurse", "list-dirs")),
|
||||||
(_, true, true ) => Err(Misfire::Conflict("tree", "list-dirs")),
|
(_, true, true ) => Err(Misfire::Conflict("tree", "list-dirs")),
|
||||||
(true, false, false) => Ok(DirAction::Recurse(try!(RecurseOptions::deduce(matches, false)))),
|
(true, false, false) => Ok(DirAction::Recurse(try!(RecurseOptions::deduce(matches, false)))),
|
||||||
(_ , _, true ) => Ok(DirAction::Recurse(try!(RecurseOptions::deduce(matches, true)))),
|
(_ , _, true ) => Ok(DirAction::Recurse(try!(RecurseOptions::deduce(matches, true)))),
|
||||||
(false, true, _ ) => Ok(DirAction::AsFile),
|
(false, true, _ ) => Ok(DirAction::AsFile),
|
||||||
(false, false, _ ) => Ok(DirAction::List),
|
(false, false, _ ) => Ok(DirAction::List),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -555,18 +553,16 @@ impl Columns {
|
|||||||
columns.push(Group);
|
columns.push(Group);
|
||||||
}
|
}
|
||||||
|
|
||||||
let current_year = LocalDateTime::now().year();
|
|
||||||
|
|
||||||
if self.time_types.modified {
|
if self.time_types.modified {
|
||||||
columns.push(Timestamp(TimeType::FileModified, current_year));
|
columns.push(Timestamp(TimeType::FileModified));
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.time_types.created {
|
if self.time_types.created {
|
||||||
columns.push(Timestamp(TimeType::FileCreated, current_year));
|
columns.push(Timestamp(TimeType::FileCreated));
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.time_types.accessed {
|
if self.time_types.accessed {
|
||||||
columns.push(Timestamp(TimeType::FileAccessed, current_year));
|
columns.push(Timestamp(TimeType::FileAccessed));
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg!(feature="git") {
|
if cfg!(feature="git") {
|
||||||
|
@ -3,11 +3,22 @@ use column::{Alignment, Column, Cell};
|
|||||||
use feature::Attribute;
|
use feature::Attribute;
|
||||||
use dir::Dir;
|
use dir::Dir;
|
||||||
use file::File;
|
use file::File;
|
||||||
use options::{Columns, FileFilter, RecurseOptions};
|
use file::fields as f;
|
||||||
use users::OSUsers;
|
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 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.
|
||||||
@ -109,11 +120,14 @@ struct Row {
|
|||||||
/// A **Table** object gets built up by the view as it lists files and
|
/// A **Table** object gets built up by the view as it lists files and
|
||||||
/// directories.
|
/// directories.
|
||||||
struct Table {
|
struct Table {
|
||||||
columns: Vec<Column>,
|
columns: Vec<Column>,
|
||||||
users: OSUsers,
|
rows: Vec<Row>,
|
||||||
locale: UserLocale,
|
|
||||||
rows: Vec<Row>,
|
time: locale::Time,
|
||||||
colours: Colours,
|
numeric: locale::Numeric,
|
||||||
|
users: OSUsers,
|
||||||
|
colours: Colours,
|
||||||
|
current_year: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Table {
|
impl Table {
|
||||||
@ -122,10 +136,13 @@ impl Table {
|
|||||||
fn with_options(colours: Colours, columns: Vec<Column>) -> Table {
|
fn with_options(colours: Colours, columns: Vec<Column>) -> Table {
|
||||||
Table {
|
Table {
|
||||||
columns: columns,
|
columns: columns,
|
||||||
users: OSUsers::empty_cache(),
|
rows: Vec::new(),
|
||||||
locale: UserLocale::new(),
|
|
||||||
rows: Vec::new(),
|
time: locale::Time::load_user_locale().unwrap_or_else(|_| locale::Time::english()),
|
||||||
colours: colours,
|
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);
|
self.rows.push(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Use the list of columns to find which cells should be produced for
|
|
||||||
/// this file, per-column.
|
|
||||||
fn cells_for_file(&mut self, file: &File) -> Vec<Cell> {
|
|
||||||
self.columns.clone().iter()
|
|
||||||
.map(|c| file.display(c, &self.colours, &mut self.users, &self.locale))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the cells for the given file, and add the result to the table.
|
/// 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(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.rows.push(row)
|
self.rows.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Use the list of columns to find which cells should be produced for
|
||||||
|
/// this file, per-column.
|
||||||
|
fn cells_for_file(&mut self, file: &File) -> Vec<Cell> {
|
||||||
|
self.columns.clone().iter()
|
||||||
|
.map(|c| self.display(file, c))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display(&mut self, file: &File, column: &Column) -> Cell {
|
||||||
|
match *column {
|
||||||
|
Column::Permissions => self.render_permissions(file.permissions()),
|
||||||
|
Column::FileSize(fmt) => self.render_size(file.size(), fmt),
|
||||||
|
Column::Timestamp(t) => self.render_time(file.timestamp(t)),
|
||||||
|
Column::HardLinks => self.render_links(file.links()),
|
||||||
|
Column::Inode => self.render_inode(file.inode()),
|
||||||
|
Column::Blocks => self.render_blocks(file.blocks()),
|
||||||
|
Column::User => self.render_user(file.user()),
|
||||||
|
Column::Group => self.render_group(file.group()),
|
||||||
|
Column::GitStatus => self.render_git_status(file.git_status()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_permissions(&self, permissions: f::Permissions) -> Cell {
|
||||||
|
let c = self.colours.perms;
|
||||||
|
let bit = |bit, chr: &'static str, style: Style| {
|
||||||
|
if bit { style.paint(chr) } else { self.colours.punctuation.paint("-") }
|
||||||
|
};
|
||||||
|
|
||||||
|
let file_type = match permissions.file_type {
|
||||||
|
f::Type::File => self.colours.filetypes.normal.paint("."),
|
||||||
|
f::Type::Directory => self.colours.filetypes.directory.paint("d"),
|
||||||
|
f::Type::Pipe => self.colours.filetypes.special.paint("|"),
|
||||||
|
f::Type::Link => self.colours.filetypes.symlink.paint("l"),
|
||||||
|
f::Type::Special => self.colours.filetypes.special.paint("?"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let x_colour = if let f::Type::File = permissions.file_type { c.user_execute_file }
|
||||||
|
else { c.user_execute_other };
|
||||||
|
|
||||||
|
let string = ANSIStrings( &[
|
||||||
|
file_type,
|
||||||
|
bit(permissions.user_read, "r", c.user_read),
|
||||||
|
bit(permissions.user_write, "w", c.user_write),
|
||||||
|
bit(permissions.user_execute, "x", x_colour),
|
||||||
|
bit(permissions.group_read, "r", c.group_read),
|
||||||
|
bit(permissions.group_write, "w", c.group_write),
|
||||||
|
bit(permissions.group_execute, "x", c.group_execute),
|
||||||
|
bit(permissions.other_read, "r", c.other_read),
|
||||||
|
bit(permissions.other_write, "w", c.other_write),
|
||||||
|
bit(permissions.other_execute, "x", c.other_execute),
|
||||||
|
if permissions.attribute { c.attribute.paint("@") } else { Plain.paint(" ") },
|
||||||
|
]).to_string();
|
||||||
|
|
||||||
|
Cell {
|
||||||
|
text: string,
|
||||||
|
length: 11,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_links(&self, links: f::Links) -> Cell {
|
||||||
|
let style = if links.multiple { self.colours.links.multi_link_file }
|
||||||
|
else { self.colours.links.normal };
|
||||||
|
|
||||||
|
Cell::paint(style, &self.numeric.format_int(links.count))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_blocks(&self, blocks: f::Blocks) -> Cell {
|
||||||
|
match blocks {
|
||||||
|
f::Blocks::Some(blocks) => Cell::paint(self.colours.blocks, &blocks.to_string()),
|
||||||
|
f::Blocks::None => Cell::paint(self.colours.punctuation, "-"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_inode(&self, inode: f::Inode) -> Cell {
|
||||||
|
Cell::paint(self.colours.inode, &inode.0.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_size(&self, size: f::Size, size_format: SizeFormat) -> Cell {
|
||||||
|
if let f::Size::Some(offset) = size {
|
||||||
|
let result = match size_format {
|
||||||
|
SizeFormat::DecimalBytes => decimal_prefix(offset as f64),
|
||||||
|
SizeFormat::BinaryBytes => binary_prefix(offset as f64),
|
||||||
|
SizeFormat::JustBytes => return Cell::paint(self.colours.size.numbers, &self.numeric.format_int(offset)),
|
||||||
|
};
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Standalone(bytes) => Cell::paint(self.colours.size.numbers, &*bytes.to_string()),
|
||||||
|
Prefixed(prefix, n) => {
|
||||||
|
let number = if n < 10f64 { self.numeric.format_float(n, 1) } else { self.numeric.format_int(n as isize) };
|
||||||
|
let symbol = prefix.symbol();
|
||||||
|
|
||||||
|
Cell {
|
||||||
|
text: ANSIStrings( &[ self.colours.size.numbers.paint(&number[..]), self.colours.size.unit.paint(symbol) ]).to_string(),
|
||||||
|
length: number.len() + symbol.len(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Cell::paint(self.colours.punctuation, "-")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_time(&self, timestamp: f::Time) -> Cell {
|
||||||
|
let date = LocalDateTime::at(timestamp.0);
|
||||||
|
|
||||||
|
let format = if date.year() == self.current_year {
|
||||||
|
DateFormat::parse("{2>:D} {:M} {2>:h}:{02>:m}").unwrap()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
DateFormat::parse("{2>:D} {:M} {5>:Y}").unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
Cell::paint(self.colours.date, &format.format(date, &self.time))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_git_status(&self, git: f::Git) -> Cell {
|
||||||
|
Cell {
|
||||||
|
text: ANSIStrings(&[ self.render_git_char(git.staged),
|
||||||
|
self.render_git_char(git.unstaged) ]).to_string(),
|
||||||
|
length: 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_git_char(&self, status: f::GitStatus) -> ANSIString {
|
||||||
|
match status {
|
||||||
|
f::GitStatus::NotModified => self.colours.punctuation.paint("-"),
|
||||||
|
f::GitStatus::New => self.colours.git.new.paint("N"),
|
||||||
|
f::GitStatus::Modified => self.colours.git.modified.paint("M"),
|
||||||
|
f::GitStatus::Deleted => self.colours.git.deleted.paint("D"),
|
||||||
|
f::GitStatus::Renamed => self.colours.git.renamed.paint("R"),
|
||||||
|
f::GitStatus::TypeChange => self.colours.git.typechange.paint("T"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_user(&mut self, user: f::User) -> Cell {
|
||||||
|
let user_name = match self.users.get_user_by_uid(user.0) {
|
||||||
|
Some(user) => user.name,
|
||||||
|
None => user.0.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let style = if self.users.get_current_uid() == user.0 { self.colours.users.user_you }
|
||||||
|
else { self.colours.users.user_someone_else };
|
||||||
|
Cell::paint(style, &*user_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_group(&mut self, group: f::Group) -> Cell {
|
||||||
|
let mut style = self.colours.users.group_not_yours;
|
||||||
|
|
||||||
|
let group_name = match self.users.get_group_by_gid(group.0) {
|
||||||
|
Some(group) => {
|
||||||
|
let current_uid = self.users.get_current_uid();
|
||||||
|
if let Some(current_user) = self.users.get_user_by_uid(current_uid) {
|
||||||
|
if current_user.primary_group == group.gid || group.members.contains(¤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.
|
/// Print the table to standard output, consuming it in the process.
|
||||||
@ -243,24 +417,10 @@ enum TreePart {
|
|||||||
impl TreePart {
|
impl TreePart {
|
||||||
fn ascii_art(&self) -> &'static str {
|
fn ascii_art(&self) -> &'static str {
|
||||||
match *self {
|
match *self {
|
||||||
TreePart::Edge => "├──",
|
TreePart::Edge => "├──",
|
||||||
TreePart::Line => "│ ",
|
TreePart::Line => "│ ",
|
||||||
TreePart::Corner => "└──",
|
TreePart::Corner => "└──",
|
||||||
TreePart::Blank => " ",
|
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()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user