mirror of
https://github.com/Llewellynvdm/exa.git
synced 2024-11-27 06:06:28 +00:00
Merge branch 'better-options'
This commit is contained in:
commit
021655faec
@ -1,92 +0,0 @@
|
|||||||
use ansi_term::Style;
|
|
||||||
use unicode_width::UnicodeWidthStr;
|
|
||||||
|
|
||||||
use options::{SizeFormat, TimeType};
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
|
||||||
pub enum Column {
|
|
||||||
Permissions,
|
|
||||||
FileSize(SizeFormat),
|
|
||||||
Timestamp(TimeType),
|
|
||||||
Blocks,
|
|
||||||
User,
|
|
||||||
Group,
|
|
||||||
HardLinks,
|
|
||||||
Inode,
|
|
||||||
|
|
||||||
GitStatus,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Each column can pick its own **Alignment**. Usually, numbers are
|
|
||||||
/// right-aligned, and text is left-aligned.
|
|
||||||
#[derive(Copy, Clone)]
|
|
||||||
pub enum Alignment {
|
|
||||||
Left, Right,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Column {
|
|
||||||
|
|
||||||
/// Get the alignment this column should use.
|
|
||||||
pub fn alignment(&self) -> Alignment {
|
|
||||||
match *self {
|
|
||||||
Column::FileSize(_) => Alignment::Right,
|
|
||||||
Column::HardLinks => Alignment::Right,
|
|
||||||
Column::Inode => Alignment::Right,
|
|
||||||
Column::Blocks => Alignment::Right,
|
|
||||||
Column::GitStatus => Alignment::Right,
|
|
||||||
_ => Alignment::Left,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the text that should be printed at the top, when the user elects
|
|
||||||
/// to have a header row printed.
|
|
||||||
pub fn header(&self) -> &'static str {
|
|
||||||
match *self {
|
|
||||||
Column::Permissions => "Permissions",
|
|
||||||
Column::FileSize(_) => "Size",
|
|
||||||
Column::Timestamp(t) => t.header(),
|
|
||||||
Column::Blocks => "Blocks",
|
|
||||||
Column::User => "User",
|
|
||||||
Column::Group => "Group",
|
|
||||||
Column::HardLinks => "Links",
|
|
||||||
Column::Inode => "inode",
|
|
||||||
Column::GitStatus => "Git",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(PartialEq, Debug, Clone)]
|
|
||||||
pub struct Cell {
|
|
||||||
pub length: usize,
|
|
||||||
pub text: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Cell {
|
|
||||||
pub fn empty() -> Cell {
|
|
||||||
Cell {
|
|
||||||
text: String::new(),
|
|
||||||
length: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn paint(style: Style, string: &str) -> Cell {
|
|
||||||
Cell {
|
|
||||||
text: style.paint(string).to_string(),
|
|
||||||
length: UnicodeWidthStr::width(string),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_spaces(&mut self, count: usize) {
|
|
||||||
self.length += count;
|
|
||||||
for _ in 0 .. count {
|
|
||||||
self.text.push(' ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn append(&mut self, other: &Cell) {
|
|
||||||
self.length += other.length;
|
|
||||||
self.text.push_str(&*other.text);
|
|
||||||
}
|
|
||||||
}
|
|
24
src/file.rs
24
src/file.rs
@ -10,14 +10,13 @@ use std::path::{Component, Path, PathBuf};
|
|||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
use dir::Dir;
|
use dir::Dir;
|
||||||
use options::TimeType;
|
|
||||||
|
|
||||||
use self::fields as f;
|
use self::fields as f;
|
||||||
|
|
||||||
// Constant table copied from https://doc.rust-lang.org/src/std/sys/unix/ext/fs.rs.html#11-259
|
|
||||||
// which is currently unstable and lacks vision for stabilization,
|
|
||||||
// see https://github.com/rust-lang/rust/issues/27712
|
|
||||||
|
|
||||||
|
/// Constant table copied from https://doc.rust-lang.org/src/std/sys/unix/ext/fs.rs.html#11-259
|
||||||
|
/// which is currently unstable and lacks vision for stabilization,
|
||||||
|
/// see https://github.com/rust-lang/rust/issues/27712
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
mod modes {
|
mod modes {
|
||||||
use std::os::unix::raw;
|
use std::os::unix::raw;
|
||||||
@ -281,15 +280,16 @@ impl<'dir> File<'dir> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One of this file's timestamps, as a number in seconds.
|
pub fn modified_time(&self) -> f::Time {
|
||||||
pub fn timestamp(&self, time_type: TimeType) -> f::Time {
|
f::Time(self.metadata.mtime())
|
||||||
let time_in_seconds = match time_type {
|
}
|
||||||
TimeType::FileAccessed => self.metadata.atime(),
|
|
||||||
TimeType::FileModified => self.metadata.mtime(),
|
|
||||||
TimeType::FileCreated => self.metadata.ctime(),
|
|
||||||
};
|
|
||||||
|
|
||||||
f::Time(time_in_seconds)
|
pub fn created_time(&self) -> f::Time {
|
||||||
|
f::Time(self.metadata.ctime())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn accessed_time(&self) -> f::Time {
|
||||||
|
f::Time(self.metadata.mtime())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This file's 'type'.
|
/// This file's 'type'.
|
||||||
|
13
src/main.rs
13
src/main.rs
@ -28,7 +28,6 @@ use file::File;
|
|||||||
use options::{Options, View};
|
use options::{Options, View};
|
||||||
|
|
||||||
mod colours;
|
mod colours;
|
||||||
mod column;
|
|
||||||
mod dir;
|
mod dir;
|
||||||
mod feature;
|
mod feature;
|
||||||
mod file;
|
mod file;
|
||||||
@ -43,10 +42,14 @@ struct Exa {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Exa {
|
impl Exa {
|
||||||
fn run(&mut self, args_file_names: &[String]) {
|
fn run(&mut self, mut args_file_names: Vec<String>) {
|
||||||
let mut files = Vec::new();
|
let mut files = Vec::new();
|
||||||
let mut dirs = Vec::new();
|
let mut dirs = Vec::new();
|
||||||
|
|
||||||
|
if args_file_names.is_empty() {
|
||||||
|
args_file_names.push(".".to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
for file_name in args_file_names.iter() {
|
for file_name in args_file_names.iter() {
|
||||||
match File::from_path(Path::new(&file_name), None) {
|
match File::from_path(Path::new(&file_name), None) {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@ -99,8 +102,8 @@ impl Exa {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
self.options.filter_files(&mut children);
|
self.options.filter.filter_files(&mut children);
|
||||||
self.options.sort_files(&mut children);
|
self.options.filter.sort_files(&mut children);
|
||||||
|
|
||||||
if let Some(recurse_opts) = self.options.dir_action.recurse_options() {
|
if let Some(recurse_opts) = self.options.dir_action.recurse_options() {
|
||||||
let depth = dir.path.components().filter(|&c| c != Component::CurDir).count() + 1;
|
let depth = dir.path.components().filter(|&c| c != Component::CurDir).count() + 1;
|
||||||
@ -146,7 +149,7 @@ fn main() {
|
|||||||
match Options::getopts(&args) {
|
match Options::getopts(&args) {
|
||||||
Ok((options, paths)) => {
|
Ok((options, paths)) => {
|
||||||
let mut exa = Exa { options: options };
|
let mut exa = Exa { options: options };
|
||||||
exa.run(&paths);
|
exa.run(paths);
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("{}", e);
|
println!("{}", e);
|
||||||
|
595
src/options.rs
595
src/options.rs
@ -7,21 +7,26 @@ use getopts;
|
|||||||
use natord;
|
use natord;
|
||||||
|
|
||||||
use colours::Colours;
|
use colours::Colours;
|
||||||
use column::Column;
|
|
||||||
use column::Column::*;
|
|
||||||
use dir::Dir;
|
|
||||||
use feature::xattr;
|
use feature::xattr;
|
||||||
use file::File;
|
use file::File;
|
||||||
use output::{Grid, Details, GridDetails, Lines};
|
use output::{Grid, Details, GridDetails, Lines};
|
||||||
|
use output::column::{Columns, TimeTypes, SizeFormat};
|
||||||
use term::dimensions;
|
use term::dimensions;
|
||||||
|
|
||||||
|
|
||||||
/// The *Options* struct represents a parsed version of the user's
|
/// These **options** represent a parsed, error-checked versions of the
|
||||||
/// command-line options.
|
/// user's command-line options.
|
||||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||||
pub struct Options {
|
pub struct Options {
|
||||||
|
|
||||||
|
/// The action to perform when encountering a directory rather than a
|
||||||
|
/// regular file.
|
||||||
pub dir_action: DirAction,
|
pub dir_action: DirAction,
|
||||||
|
|
||||||
|
/// How to sort and filter files before outputting them.
|
||||||
pub filter: FileFilter,
|
pub filter: FileFilter,
|
||||||
|
|
||||||
|
/// The type of output to use (lines, grid, or details).
|
||||||
pub view: View,
|
pub view: View,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,39 +36,45 @@ impl Options {
|
|||||||
#[allow(unused_results)]
|
#[allow(unused_results)]
|
||||||
pub fn getopts(args: &[String]) -> Result<(Options, Vec<String>), Misfire> {
|
pub fn getopts(args: &[String]) -> Result<(Options, Vec<String>), Misfire> {
|
||||||
let mut opts = getopts::Options::new();
|
let mut opts = getopts::Options::new();
|
||||||
|
|
||||||
|
opts.optflag("v", "version", "display version of exa");
|
||||||
|
opts.optflag("?", "help", "show list of command-line options");
|
||||||
|
|
||||||
|
// Display options
|
||||||
opts.optflag("1", "oneline", "display one entry per line");
|
opts.optflag("1", "oneline", "display one entry per line");
|
||||||
|
opts.optflag("G", "grid", "display entries in a grid view (default)");
|
||||||
|
opts.optflag("l", "long", "display extended details and attributes");
|
||||||
|
opts.optflag("R", "recurse", "recurse into directories");
|
||||||
|
opts.optflag("T", "tree", "recurse into subdirectories in a tree view");
|
||||||
|
opts.optflag("x", "across", "sort multi-column view entries across");
|
||||||
|
|
||||||
|
// Filtering and sorting options
|
||||||
|
opts.optflag("", "group-directories-first", "list directories before other files");
|
||||||
opts.optflag("a", "all", "show dot-files");
|
opts.optflag("a", "all", "show dot-files");
|
||||||
|
opts.optflag("d", "list-dirs", "list directories as regular files");
|
||||||
|
opts.optflag("r", "reverse", "reverse order of files");
|
||||||
|
opts.optopt ("s", "sort", "field to sort by", "WORD");
|
||||||
|
|
||||||
|
// Long view options
|
||||||
opts.optflag("b", "binary", "use binary prefixes in file sizes");
|
opts.optflag("b", "binary", "use binary prefixes in file sizes");
|
||||||
opts.optflag("B", "bytes", "list file sizes in bytes, without prefixes");
|
opts.optflag("B", "bytes", "list file sizes in bytes, without prefixes");
|
||||||
opts.optflag("d", "list-dirs", "list directories as regular files");
|
|
||||||
opts.optflag("g", "group", "show group as well as user");
|
opts.optflag("g", "group", "show group as well as user");
|
||||||
opts.optflag("G", "grid", "display entries in a grid view (default)");
|
|
||||||
opts.optflag("", "group-directories-first", "list directories before other files");
|
|
||||||
opts.optflag("h", "header", "show a header row at the top");
|
opts.optflag("h", "header", "show a header row at the top");
|
||||||
opts.optflag("H", "links", "show number of hard links");
|
opts.optflag("H", "links", "show number of hard links");
|
||||||
opts.optflag("i", "inode", "show each file's inode number");
|
opts.optflag("i", "inode", "show each file's inode number");
|
||||||
opts.optflag("l", "long", "display extended details and attributes");
|
|
||||||
opts.optopt ("L", "level", "maximum depth of recursion", "DEPTH");
|
opts.optopt ("L", "level", "maximum depth of recursion", "DEPTH");
|
||||||
opts.optflag("m", "modified", "display timestamp of most recent modification");
|
opts.optflag("m", "modified", "display timestamp of most recent modification");
|
||||||
opts.optflag("r", "reverse", "reverse order of files");
|
|
||||||
opts.optflag("R", "recurse", "recurse into directories");
|
|
||||||
opts.optopt ("s", "sort", "field to sort by", "WORD");
|
|
||||||
opts.optflag("S", "blocks", "show number of file system blocks");
|
opts.optflag("S", "blocks", "show number of file system blocks");
|
||||||
opts.optopt ("t", "time", "which timestamp to show for a file", "WORD");
|
opts.optopt ("t", "time", "which timestamp to show for a file", "WORD");
|
||||||
opts.optflag("T", "tree", "recurse into subdirectories in a tree view");
|
|
||||||
opts.optflag("u", "accessed", "display timestamp of last access for a file");
|
opts.optflag("u", "accessed", "display timestamp of last access for a file");
|
||||||
opts.optflag("U", "created", "display timestamp of creation for a file");
|
opts.optflag("U", "created", "display timestamp of creation for a file");
|
||||||
opts.optflag("x", "across", "sort multi-column view entries across");
|
|
||||||
|
|
||||||
opts.optflag("", "version", "display version of exa");
|
|
||||||
opts.optflag("?", "help", "show list of command-line options");
|
|
||||||
|
|
||||||
if cfg!(feature="git") {
|
if cfg!(feature="git") {
|
||||||
opts.optflag("", "git", "show git status");
|
opts.optflag("", "git", "show git status");
|
||||||
}
|
}
|
||||||
|
|
||||||
if xattr::ENABLED {
|
if xattr::ENABLED {
|
||||||
opts.optflag("@", "extended", "display extended attribute keys and sizes in long (-l) output");
|
opts.optflag("@", "extended", "display extended attribute keys and sizes");
|
||||||
}
|
}
|
||||||
|
|
||||||
let matches = match opts.parse(args) {
|
let matches = match opts.parse(args) {
|
||||||
@ -72,47 +83,32 @@ impl Options {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if matches.opt_present("help") {
|
if matches.opt_present("help") {
|
||||||
return Err(Misfire::Help(opts.usage("Usage:\n exa [options] [files...]")));
|
let mut help_string = "Usage:\n exa [options] [files...]\n".to_owned();
|
||||||
|
|
||||||
|
if !matches.opt_present("long") {
|
||||||
|
help_string.push_str(OPTIONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
help_string.push_str(LONG_OPTIONS);
|
||||||
|
|
||||||
|
if cfg!(feature="git") {
|
||||||
|
help_string.push_str(GIT_HELP);
|
||||||
|
help_string.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
if xattr::ENABLED {
|
||||||
|
help_string.push_str(EXTENDED_HELP);
|
||||||
|
help_string.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(Misfire::Help(help_string));
|
||||||
}
|
}
|
||||||
else if matches.opt_present("version") {
|
else if matches.opt_present("version") {
|
||||||
return Err(Misfire::Version);
|
return Err(Misfire::Version);
|
||||||
}
|
}
|
||||||
|
|
||||||
let sort_field = match matches.opt_str("sort") {
|
let options = try!(Options::deduce(&matches));
|
||||||
Some(word) => try!(SortField::from_word(word)),
|
Ok((options, matches.free))
|
||||||
None => SortField::default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let filter = FileFilter {
|
|
||||||
list_dirs_first: matches.opt_present("group-directories-first"),
|
|
||||||
reverse: matches.opt_present("reverse"),
|
|
||||||
show_invisibles: matches.opt_present("all"),
|
|
||||||
sort_field: sort_field,
|
|
||||||
};
|
|
||||||
|
|
||||||
let path_strs = if matches.free.is_empty() {
|
|
||||||
vec![ ".".to_string() ]
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
matches.free.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
let dir_action = try!(DirAction::deduce(&matches));
|
|
||||||
let view = try!(View::deduce(&matches, filter, dir_action));
|
|
||||||
|
|
||||||
Ok((Options {
|
|
||||||
dir_action: dir_action,
|
|
||||||
view: view,
|
|
||||||
filter: filter,
|
|
||||||
}, path_strs))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sort_files(&self, files: &mut Vec<File>) {
|
|
||||||
self.filter.sort_files(files)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn filter_files(&self, files: &mut Vec<File>) {
|
|
||||||
self.filter.filter_files(files)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the View specified in this set of options includes a Git
|
/// Whether the View specified in this set of options includes a Git
|
||||||
@ -127,140 +123,17 @@ impl Options {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl OptionSet for Options {
|
||||||
|
fn deduce(matches: &getopts::Matches) -> Result<Options, Misfire> {
|
||||||
|
let dir_action = try!(DirAction::deduce(&matches));
|
||||||
|
let filter = try!(FileFilter::deduce(&matches));
|
||||||
|
let view = try!(View::deduce(&matches, filter, dir_action));
|
||||||
|
|
||||||
#[derive(Default, PartialEq, Debug, Copy, Clone)]
|
Ok(Options {
|
||||||
pub struct FileFilter {
|
dir_action: dir_action,
|
||||||
list_dirs_first: bool,
|
view: view,
|
||||||
reverse: bool,
|
filter: filter,
|
||||||
show_invisibles: bool,
|
})
|
||||||
sort_field: SortField,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FileFilter {
|
|
||||||
pub fn filter_files(&self, files: &mut Vec<File>) {
|
|
||||||
if !self.show_invisibles {
|
|
||||||
files.retain(|f| !f.is_dotfile());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sort_files(&self, files: &mut Vec<File>) {
|
|
||||||
files.sort_by(|a, b| self.compare_files(a, b));
|
|
||||||
|
|
||||||
if self.reverse {
|
|
||||||
files.reverse();
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.list_dirs_first {
|
|
||||||
// This relies on the fact that sort_by is stable.
|
|
||||||
files.sort_by(|a, b| b.is_directory().cmp(&a.is_directory()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn compare_files(&self, a: &File, b: &File) -> cmp::Ordering {
|
|
||||||
match self.sort_field {
|
|
||||||
SortField::Unsorted => cmp::Ordering::Equal,
|
|
||||||
SortField::Name => natord::compare(&*a.name, &*b.name),
|
|
||||||
SortField::Size => a.metadata.len().cmp(&b.metadata.len()),
|
|
||||||
SortField::FileInode => a.metadata.ino().cmp(&b.metadata.ino()),
|
|
||||||
SortField::ModifiedDate => a.metadata.mtime().cmp(&b.metadata.mtime()),
|
|
||||||
SortField::AccessedDate => a.metadata.atime().cmp(&b.metadata.atime()),
|
|
||||||
SortField::CreatedDate => a.metadata.ctime().cmp(&b.metadata.ctime()),
|
|
||||||
SortField::Extension => match a.ext.cmp(&b.ext) {
|
|
||||||
cmp::Ordering::Equal => natord::compare(&*a.name, &*b.name),
|
|
||||||
order => order,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// User-supplied field to sort by.
|
|
||||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
|
||||||
pub enum SortField {
|
|
||||||
Unsorted, Name, Extension, Size, FileInode,
|
|
||||||
ModifiedDate, AccessedDate, CreatedDate,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for SortField {
|
|
||||||
fn default() -> SortField {
|
|
||||||
SortField::Name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SortField {
|
|
||||||
|
|
||||||
/// Find which field to use based on a user-supplied word.
|
|
||||||
fn from_word(word: String) -> Result<SortField, Misfire> {
|
|
||||||
match &word[..] {
|
|
||||||
"name" | "filename" => Ok(SortField::Name),
|
|
||||||
"size" | "filesize" => Ok(SortField::Size),
|
|
||||||
"ext" | "extension" => Ok(SortField::Extension),
|
|
||||||
"mod" | "modified" => Ok(SortField::ModifiedDate),
|
|
||||||
"acc" | "accessed" => Ok(SortField::AccessedDate),
|
|
||||||
"cr" | "created" => Ok(SortField::CreatedDate),
|
|
||||||
"none" => Ok(SortField::Unsorted),
|
|
||||||
"inode" => Ok(SortField::FileInode),
|
|
||||||
field => Err(SortField::none(field))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// How to display an error when the word didn't match with anything.
|
|
||||||
fn none(field: &str) -> Misfire {
|
|
||||||
Misfire::InvalidOptions(getopts::Fail::UnrecognizedOption(format!("--sort {}", field)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// One of these things could happen instead of listing files.
|
|
||||||
#[derive(PartialEq, Debug)]
|
|
||||||
pub enum Misfire {
|
|
||||||
|
|
||||||
/// The getopts crate didn't like these arguments.
|
|
||||||
InvalidOptions(getopts::Fail),
|
|
||||||
|
|
||||||
/// The user asked for help. This isn't strictly an error, which is why
|
|
||||||
/// this enum isn't named Error!
|
|
||||||
Help(String),
|
|
||||||
|
|
||||||
/// The user wanted the version number.
|
|
||||||
Version,
|
|
||||||
|
|
||||||
/// Two options were given that conflict with one another.
|
|
||||||
Conflict(&'static str, &'static str),
|
|
||||||
|
|
||||||
/// An option was given that does nothing when another one either is or
|
|
||||||
/// isn't present.
|
|
||||||
Useless(&'static str, bool, &'static str),
|
|
||||||
|
|
||||||
/// An option was given that does nothing when either of two other options
|
|
||||||
/// are not present.
|
|
||||||
Useless2(&'static str, &'static str, &'static str),
|
|
||||||
|
|
||||||
/// A numeric option was given that failed to be parsed as a number.
|
|
||||||
FailedParse(ParseIntError),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Misfire {
|
|
||||||
/// The OS return code this misfire should signify.
|
|
||||||
pub fn error_code(&self) -> i32 {
|
|
||||||
if let Misfire::Help(_) = *self { 2 }
|
|
||||||
else { 3 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Misfire {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
use self::Misfire::*;
|
|
||||||
|
|
||||||
match *self {
|
|
||||||
InvalidOptions(ref e) => write!(f, "{}", e),
|
|
||||||
Help(ref text) => write!(f, "{}", text),
|
|
||||||
Version => write!(f, "exa {}", env!("CARGO_PKG_VERSION")),
|
|
||||||
Conflict(a, b) => write!(f, "Option --{} conflicts with option {}.", a, b),
|
|
||||||
Useless(a, false, b) => write!(f, "Option --{} is useless without option --{}.", a, b),
|
|
||||||
Useless(a, true, b) => write!(f, "Option --{} is useless given option --{}.", a, b),
|
|
||||||
Useless2(a, b1, b2) => write!(f, "Option --{} is useless without options --{} or --{}.", a, b1, b2),
|
|
||||||
FailedParse(ref e) => write!(f, "Failed to parse number: {}", e),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -274,7 +147,7 @@ pub enum View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl View {
|
impl View {
|
||||||
pub fn deduce(matches: &getopts::Matches, filter: FileFilter, dir_action: DirAction) -> Result<View, Misfire> {
|
fn deduce(matches: &getopts::Matches, filter: FileFilter, dir_action: DirAction) -> Result<View, Misfire> {
|
||||||
use self::Misfire::*;
|
use self::Misfire::*;
|
||||||
|
|
||||||
let long = || {
|
let long = || {
|
||||||
@ -356,8 +229,8 @@ impl View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// If the terminal width couldn't be matched for some reason, such
|
// If the terminal width couldn’t be matched for some reason, such
|
||||||
// as the program's stdout being connected to a file, then
|
// as the program’s stdout being connected to a file, then
|
||||||
// fallback to the lines view.
|
// fallback to the lines view.
|
||||||
let lines = Lines {
|
let lines = Lines {
|
||||||
colours: Colours::plain(),
|
colours: Colours::plain(),
|
||||||
@ -389,21 +262,137 @@ impl View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
trait OptionSet: Sized {
|
||||||
|
fn deduce(matches: &getopts::Matches) -> Result<Self, Misfire>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OptionSet for Columns {
|
||||||
|
fn deduce(matches: &getopts::Matches) -> Result<Columns, Misfire> {
|
||||||
|
Ok(Columns {
|
||||||
|
size_format: try!(SizeFormat::deduce(matches)),
|
||||||
|
time_types: try!(TimeTypes::deduce(matches)),
|
||||||
|
inode: matches.opt_present("inode"),
|
||||||
|
links: matches.opt_present("links"),
|
||||||
|
blocks: matches.opt_present("blocks"),
|
||||||
|
group: matches.opt_present("group"),
|
||||||
|
git: cfg!(feature="git") && matches.opt_present("git"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// The **file filter** processes a vector of files before outputting them,
|
||||||
|
/// filtering and sorting the files depending on the user’s command-line
|
||||||
|
/// flags.
|
||||||
|
#[derive(Default, PartialEq, Debug, Copy, Clone)]
|
||||||
|
pub struct FileFilter {
|
||||||
|
list_dirs_first: bool,
|
||||||
|
reverse: bool,
|
||||||
|
show_invisibles: bool,
|
||||||
|
sort_field: SortField,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OptionSet for FileFilter {
|
||||||
|
fn deduce(matches: &getopts::Matches) -> Result<FileFilter, Misfire> {
|
||||||
|
let sort_field = try!(SortField::deduce(&matches));
|
||||||
|
|
||||||
|
Ok(FileFilter {
|
||||||
|
list_dirs_first: matches.opt_present("group-directories-first"),
|
||||||
|
reverse: matches.opt_present("reverse"),
|
||||||
|
show_invisibles: matches.opt_present("all"),
|
||||||
|
sort_field: sort_field,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileFilter {
|
||||||
|
|
||||||
|
/// Remove every file in the given vector that does *not* pass the
|
||||||
|
/// filter predicate.
|
||||||
|
pub fn filter_files(&self, files: &mut Vec<File>) {
|
||||||
|
if !self.show_invisibles {
|
||||||
|
files.retain(|f| !f.is_dotfile());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sort the files in the given vector based on the sort field option.
|
||||||
|
pub fn sort_files(&self, files: &mut Vec<File>) {
|
||||||
|
files.sort_by(|a, b| self.compare_files(a, b));
|
||||||
|
|
||||||
|
if self.reverse {
|
||||||
|
files.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.list_dirs_first {
|
||||||
|
// This relies on the fact that `sort_by` is stable.
|
||||||
|
files.sort_by(|a, b| b.is_directory().cmp(&a.is_directory()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compare_files(&self, a: &File, b: &File) -> cmp::Ordering {
|
||||||
|
match self.sort_field {
|
||||||
|
SortField::Unsorted => cmp::Ordering::Equal,
|
||||||
|
SortField::Name => natord::compare(&*a.name, &*b.name),
|
||||||
|
SortField::Size => a.metadata.len().cmp(&b.metadata.len()),
|
||||||
|
SortField::FileInode => a.metadata.ino().cmp(&b.metadata.ino()),
|
||||||
|
SortField::ModifiedDate => a.metadata.mtime().cmp(&b.metadata.mtime()),
|
||||||
|
SortField::AccessedDate => a.metadata.atime().cmp(&b.metadata.atime()),
|
||||||
|
SortField::CreatedDate => a.metadata.ctime().cmp(&b.metadata.ctime()),
|
||||||
|
SortField::Extension => match a.ext.cmp(&b.ext) {
|
||||||
|
cmp::Ordering::Equal => natord::compare(&*a.name, &*b.name),
|
||||||
|
order => order,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// User-supplied field to sort by.
|
||||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||||
pub enum SizeFormat {
|
pub enum SortField {
|
||||||
DecimalBytes,
|
Unsorted, Name, Extension, Size, FileInode,
|
||||||
BinaryBytes,
|
ModifiedDate, AccessedDate, CreatedDate,
|
||||||
JustBytes,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for SizeFormat {
|
impl Default for SortField {
|
||||||
fn default() -> SizeFormat {
|
fn default() -> SortField {
|
||||||
SizeFormat::DecimalBytes
|
SortField::Name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SizeFormat {
|
impl OptionSet for SortField {
|
||||||
pub fn deduce(matches: &getopts::Matches) -> Result<SizeFormat, Misfire> {
|
fn deduce(matches: &getopts::Matches) -> Result<SortField, Misfire> {
|
||||||
|
if let Some(word) = matches.opt_str("sort") {
|
||||||
|
match &word[..] {
|
||||||
|
"name" | "filename" => Ok(SortField::Name),
|
||||||
|
"size" | "filesize" => Ok(SortField::Size),
|
||||||
|
"ext" | "extension" => Ok(SortField::Extension),
|
||||||
|
"mod" | "modified" => Ok(SortField::ModifiedDate),
|
||||||
|
"acc" | "accessed" => Ok(SortField::AccessedDate),
|
||||||
|
"cr" | "created" => Ok(SortField::CreatedDate),
|
||||||
|
"none" => Ok(SortField::Unsorted),
|
||||||
|
"inode" => Ok(SortField::FileInode),
|
||||||
|
field => Err(Misfire::bad_argument("sort", field))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Ok(SortField::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl OptionSet for SizeFormat {
|
||||||
|
|
||||||
|
/// Determine which file size to use in the file size column based on
|
||||||
|
/// the user’s options.
|
||||||
|
///
|
||||||
|
/// The default mode is to use the decimal prefixes, as they are the
|
||||||
|
/// most commonly-understood, and don’t involve trying to parse large
|
||||||
|
/// strings of digits in your head. Changing the format to anything else
|
||||||
|
/// involves the `--binary` or `--bytes` flags, and these conflict with
|
||||||
|
/// each other.
|
||||||
|
fn deduce(matches: &getopts::Matches) -> Result<SizeFormat, Misfire> {
|
||||||
let binary = matches.opt_present("binary");
|
let binary = matches.opt_present("binary");
|
||||||
let bytes = matches.opt_present("bytes");
|
let bytes = matches.opt_present("bytes");
|
||||||
|
|
||||||
@ -417,40 +406,18 @@ impl SizeFormat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
impl OptionSet for TimeTypes {
|
||||||
pub enum TimeType {
|
|
||||||
FileAccessed,
|
|
||||||
FileModified,
|
|
||||||
FileCreated,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TimeType {
|
/// Determine which of a file’s time fields should be displayed for it
|
||||||
pub fn header(&self) -> &'static str {
|
/// based on the user’s options.
|
||||||
match *self {
|
///
|
||||||
TimeType::FileAccessed => "Date Accessed",
|
/// There are two separate ways to pick which fields to show: with a
|
||||||
TimeType::FileModified => "Date Modified",
|
/// flag (such as `--modified`) or with a parameter (such as
|
||||||
TimeType::FileCreated => "Date Created",
|
/// `--time=modified`). An error is signaled if both ways are used.
|
||||||
}
|
///
|
||||||
}
|
/// It’s valid to show more than one column by passing in more than one
|
||||||
}
|
/// option, but passing *no* options means that the user just wants to
|
||||||
|
/// see the default set.
|
||||||
|
|
||||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
|
||||||
pub struct TimeTypes {
|
|
||||||
accessed: bool,
|
|
||||||
modified: bool,
|
|
||||||
created: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for TimeTypes {
|
|
||||||
fn default() -> TimeTypes {
|
|
||||||
TimeTypes { accessed: false, modified: true, created: false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TimeTypes {
|
|
||||||
|
|
||||||
/// Find which field to use based on a user-supplied word.
|
|
||||||
fn deduce(matches: &getopts::Matches) -> Result<TimeTypes, Misfire> {
|
fn deduce(matches: &getopts::Matches) -> Result<TimeTypes, Misfire> {
|
||||||
let possible_word = matches.opt_str("time");
|
let possible_word = matches.opt_str("time");
|
||||||
let modified = matches.opt_present("modified");
|
let modified = matches.opt_present("modified");
|
||||||
@ -468,11 +435,11 @@ impl TimeTypes {
|
|||||||
return Err(Misfire::Useless("accessed", true, "time"));
|
return Err(Misfire::Useless("accessed", true, "time"));
|
||||||
}
|
}
|
||||||
|
|
||||||
match &word[..] {
|
match &*word {
|
||||||
"mod" | "modified" => Ok(TimeTypes { accessed: false, modified: true, created: false }),
|
"mod" | "modified" => Ok(TimeTypes { accessed: false, modified: true, created: false }),
|
||||||
"acc" | "accessed" => Ok(TimeTypes { accessed: true, modified: false, created: false }),
|
"acc" | "accessed" => Ok(TimeTypes { accessed: true, modified: false, created: false }),
|
||||||
"cr" | "created" => Ok(TimeTypes { accessed: false, modified: false, created: true }),
|
"cr" | "created" => Ok(TimeTypes { accessed: false, modified: false, created: true }),
|
||||||
field => Err(TimeTypes::none(field)),
|
otherwise => Err(Misfire::bad_argument("time", otherwise)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -484,11 +451,6 @@ impl TimeTypes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// How to display an error when the word didn't match with anything.
|
|
||||||
fn none(field: &str) -> Misfire {
|
|
||||||
Misfire::InvalidOptions(getopts::Fail::UnrecognizedOption(format!("--time {}", field)))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -568,82 +530,105 @@ impl RecurseOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(PartialEq, Copy, Clone, Debug, Default)]
|
/// One of these things could happen instead of listing files.
|
||||||
pub struct Columns {
|
#[derive(PartialEq, Debug)]
|
||||||
size_format: SizeFormat,
|
pub enum Misfire {
|
||||||
time_types: TimeTypes,
|
|
||||||
inode: bool,
|
/// The getopts crate didn't like these arguments.
|
||||||
links: bool,
|
InvalidOptions(getopts::Fail),
|
||||||
blocks: bool,
|
|
||||||
group: bool,
|
/// The user asked for help. This isn't strictly an error, which is why
|
||||||
git: bool
|
/// this enum isn't named Error!
|
||||||
|
Help(String),
|
||||||
|
|
||||||
|
/// The user wanted the version number.
|
||||||
|
Version,
|
||||||
|
|
||||||
|
/// Two options were given that conflict with one another.
|
||||||
|
Conflict(&'static str, &'static str),
|
||||||
|
|
||||||
|
/// An option was given that does nothing when another one either is or
|
||||||
|
/// isn't present.
|
||||||
|
Useless(&'static str, bool, &'static str),
|
||||||
|
|
||||||
|
/// An option was given that does nothing when either of two other options
|
||||||
|
/// are not present.
|
||||||
|
Useless2(&'static str, &'static str, &'static str),
|
||||||
|
|
||||||
|
/// A numeric option was given that failed to be parsed as a number.
|
||||||
|
FailedParse(ParseIntError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Columns {
|
impl Misfire {
|
||||||
pub fn deduce(matches: &getopts::Matches) -> Result<Columns, Misfire> {
|
|
||||||
Ok(Columns {
|
/// The OS return code this misfire should signify.
|
||||||
size_format: try!(SizeFormat::deduce(matches)),
|
pub fn error_code(&self) -> i32 {
|
||||||
time_types: try!(TimeTypes::deduce(matches)),
|
if let Misfire::Help(_) = *self { 2 }
|
||||||
inode: matches.opt_present("inode"),
|
else { 3 }
|
||||||
links: matches.opt_present("links"),
|
|
||||||
blocks: matches.opt_present("blocks"),
|
|
||||||
group: matches.opt_present("group"),
|
|
||||||
git: cfg!(feature="git") && matches.opt_present("git"),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn should_scan_for_git(&self) -> bool {
|
/// The Misfire that happens when an option gets given the wrong
|
||||||
self.git
|
/// argument. This has to use one of the `getopts` failure
|
||||||
|
/// variants--it’s meant to take just an option name, rather than an
|
||||||
|
/// option *and* an argument, but it works just as well.
|
||||||
|
pub fn bad_argument(option: &str, otherwise: &str) -> Misfire {
|
||||||
|
Misfire::InvalidOptions(getopts::Fail::UnrecognizedOption(format!("--{} {}", option, otherwise)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn for_dir(&self, dir: Option<&Dir>) -> Vec<Column> {
|
impl fmt::Display for Misfire {
|
||||||
let mut columns = vec![];
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
use self::Misfire::*;
|
||||||
|
|
||||||
if self.inode {
|
match *self {
|
||||||
columns.push(Inode);
|
InvalidOptions(ref e) => write!(f, "{}", e),
|
||||||
}
|
Help(ref text) => write!(f, "{}", text),
|
||||||
|
Version => write!(f, "exa {}", env!("CARGO_PKG_VERSION")),
|
||||||
columns.push(Permissions);
|
Conflict(a, b) => write!(f, "Option --{} conflicts with option {}.", a, b),
|
||||||
|
Useless(a, false, b) => write!(f, "Option --{} is useless without option --{}.", a, b),
|
||||||
if self.links {
|
Useless(a, true, b) => write!(f, "Option --{} is useless given option --{}.", a, b),
|
||||||
columns.push(HardLinks);
|
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),
|
||||||
|
|
||||||
columns.push(FileSize(self.size_format));
|
|
||||||
|
|
||||||
if self.blocks {
|
|
||||||
columns.push(Blocks);
|
|
||||||
}
|
|
||||||
|
|
||||||
columns.push(User);
|
|
||||||
|
|
||||||
if self.group {
|
|
||||||
columns.push(Group);
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.time_types.modified {
|
|
||||||
columns.push(Timestamp(TimeType::FileModified));
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.time_types.created {
|
|
||||||
columns.push(Timestamp(TimeType::FileCreated));
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.time_types.accessed {
|
|
||||||
columns.push(Timestamp(TimeType::FileAccessed));
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg!(feature="git") {
|
|
||||||
if let Some(d) = dir {
|
|
||||||
if self.should_scan_for_git() && d.has_git_repo() {
|
|
||||||
columns.push(GitStatus);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
columns
|
static OPTIONS: &'static str = r##"
|
||||||
}
|
DISPLAY OPTIONS
|
||||||
}
|
-1, --oneline display one entry per line
|
||||||
|
-G, --grid display entries in a grid view (default)
|
||||||
|
-l, --long display extended details and attributes
|
||||||
|
-R, --recurse recurse into directories
|
||||||
|
-T, --tree recurse into subdirectories in a tree view
|
||||||
|
-x, --across sort multi-column view entries across
|
||||||
|
|
||||||
|
FILTERING AND SORTING OPTIONS
|
||||||
|
-a, --all show dot-files
|
||||||
|
-d, --list-dirs list directories as regular files
|
||||||
|
-r, --reverse reverse order of files
|
||||||
|
-s, --sort WORD field to sort by
|
||||||
|
--group-directories-first list directories before other files
|
||||||
|
"##;
|
||||||
|
|
||||||
|
static LONG_OPTIONS: &'static str = r##"
|
||||||
|
LONG VIEW OPTIONS
|
||||||
|
-b, --binary use binary prefixes in file sizes
|
||||||
|
-B, --bytes list file sizes in bytes, without prefixes
|
||||||
|
-g, --group show group as well as user
|
||||||
|
-h, --header show a header row at the top
|
||||||
|
-H, --links show number of hard links
|
||||||
|
-i, --inode show each file's inode number
|
||||||
|
-L, --level DEPTH maximum depth of recursion
|
||||||
|
-m, --modified display timestamp of most recent modification
|
||||||
|
-S, --blocks show number of file system blocks
|
||||||
|
-t, --time WORD which timestamp to show for a file
|
||||||
|
-u, --accessed display timestamp of last access for a file
|
||||||
|
-U, --created display timestamp of creation for a file
|
||||||
|
"##;
|
||||||
|
|
||||||
|
static GIT_HELP: &'static str = r##" -@, --extended display extended attribute keys and sizes"##;
|
||||||
|
static EXTENDED_HELP: &'static str = r##" --git show git status for files"##;
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
@ -679,7 +664,7 @@ mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
fn no_args() {
|
fn no_args() {
|
||||||
let args = Options::getopts(&[]).unwrap().1;
|
let args = Options::getopts(&[]).unwrap().1;
|
||||||
assert_eq!(args, vec![ ".".to_string() ])
|
assert!(args.is_empty()); // Listing the `.` directory is done in main.rs
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
231
src/output/column.rs
Normal file
231
src/output/column.rs
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
use ansi_term::Style;
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
|
use dir::Dir;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||||
|
pub enum Column {
|
||||||
|
Permissions,
|
||||||
|
FileSize(SizeFormat),
|
||||||
|
Timestamp(TimeType),
|
||||||
|
Blocks,
|
||||||
|
User,
|
||||||
|
Group,
|
||||||
|
HardLinks,
|
||||||
|
Inode,
|
||||||
|
|
||||||
|
GitStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Each column can pick its own **Alignment**. Usually, numbers are
|
||||||
|
/// right-aligned, and text is left-aligned.
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub enum Alignment {
|
||||||
|
Left, Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Column {
|
||||||
|
|
||||||
|
/// Get the alignment this column should use.
|
||||||
|
pub fn alignment(&self) -> Alignment {
|
||||||
|
match *self {
|
||||||
|
Column::FileSize(_) => Alignment::Right,
|
||||||
|
Column::HardLinks => Alignment::Right,
|
||||||
|
Column::Inode => Alignment::Right,
|
||||||
|
Column::Blocks => Alignment::Right,
|
||||||
|
Column::GitStatus => Alignment::Right,
|
||||||
|
_ => Alignment::Left,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the text that should be printed at the top, when the user elects
|
||||||
|
/// to have a header row printed.
|
||||||
|
pub fn header(&self) -> &'static str {
|
||||||
|
match *self {
|
||||||
|
Column::Permissions => "Permissions",
|
||||||
|
Column::FileSize(_) => "Size",
|
||||||
|
Column::Timestamp(t) => t.header(),
|
||||||
|
Column::Blocks => "Blocks",
|
||||||
|
Column::User => "User",
|
||||||
|
Column::Group => "Group",
|
||||||
|
Column::HardLinks => "Links",
|
||||||
|
Column::Inode => "inode",
|
||||||
|
Column::GitStatus => "Git",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(PartialEq, Copy, Clone, Debug, Default)]
|
||||||
|
pub struct Columns {
|
||||||
|
pub size_format: SizeFormat,
|
||||||
|
pub time_types: TimeTypes,
|
||||||
|
pub inode: bool,
|
||||||
|
pub links: bool,
|
||||||
|
pub blocks: bool,
|
||||||
|
pub group: bool,
|
||||||
|
pub git: bool
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Columns {
|
||||||
|
pub fn should_scan_for_git(&self) -> bool {
|
||||||
|
self.git
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn for_dir(&self, dir: Option<&Dir>) -> Vec<Column> {
|
||||||
|
let mut columns = vec![];
|
||||||
|
|
||||||
|
if self.inode {
|
||||||
|
columns.push(Column::Inode);
|
||||||
|
}
|
||||||
|
|
||||||
|
columns.push(Column::Permissions);
|
||||||
|
|
||||||
|
if self.links {
|
||||||
|
columns.push(Column::HardLinks);
|
||||||
|
}
|
||||||
|
|
||||||
|
columns.push(Column::FileSize(self.size_format));
|
||||||
|
|
||||||
|
if self.blocks {
|
||||||
|
columns.push(Column::Blocks);
|
||||||
|
}
|
||||||
|
|
||||||
|
columns.push(Column::User);
|
||||||
|
|
||||||
|
if self.group {
|
||||||
|
columns.push(Column::Group);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.time_types.modified {
|
||||||
|
columns.push(Column::Timestamp(TimeType::Modified));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.time_types.created {
|
||||||
|
columns.push(Column::Timestamp(TimeType::Created));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.time_types.accessed {
|
||||||
|
columns.push(Column::Timestamp(TimeType::Accessed));
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg!(feature="git") {
|
||||||
|
if let Some(d) = dir {
|
||||||
|
if self.should_scan_for_git() && d.has_git_repo() {
|
||||||
|
columns.push(Column::GitStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
columns
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Formatting options for file sizes.
|
||||||
|
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||||
|
pub enum SizeFormat {
|
||||||
|
|
||||||
|
/// Format the file size using **decimal** prefixes, such as “kilo”,
|
||||||
|
/// “mega”, or “giga”.
|
||||||
|
DecimalBytes,
|
||||||
|
|
||||||
|
/// Format the file size using **binary** prefixes, such as “kibi”,
|
||||||
|
/// “mebi”, or “gibi”.
|
||||||
|
BinaryBytes,
|
||||||
|
|
||||||
|
/// Do no formatting and just display the size as a number of bytes.
|
||||||
|
JustBytes,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SizeFormat {
|
||||||
|
fn default() -> SizeFormat {
|
||||||
|
SizeFormat::DecimalBytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// The types of a file’s time fields. These three fields are standard
|
||||||
|
/// across most (all?) operating systems.
|
||||||
|
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||||
|
pub enum TimeType {
|
||||||
|
|
||||||
|
/// The file’s accessed time (`st_atime`).
|
||||||
|
Accessed,
|
||||||
|
|
||||||
|
/// The file’s modified time (`st_mtime`).
|
||||||
|
Modified,
|
||||||
|
|
||||||
|
/// The file’s creation time (`st_ctime`).
|
||||||
|
Created,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimeType {
|
||||||
|
|
||||||
|
/// Returns the text to use for a column’s heading in the columns output.
|
||||||
|
pub fn header(&self) -> &'static str {
|
||||||
|
match *self {
|
||||||
|
TimeType::Accessed => "Date Accessed",
|
||||||
|
TimeType::Modified => "Date Modified",
|
||||||
|
TimeType::Created => "Date Created",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Fields for which of a file’s time fields should be displayed in the
|
||||||
|
/// columns output.
|
||||||
|
///
|
||||||
|
/// There should always be at least one of these--there's no way to disable
|
||||||
|
/// the time columns entirely (yet).
|
||||||
|
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||||
|
pub struct TimeTypes {
|
||||||
|
pub accessed: bool,
|
||||||
|
pub modified: bool,
|
||||||
|
pub created: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TimeTypes {
|
||||||
|
|
||||||
|
/// By default, display just the ‘modified’ time. This is the most
|
||||||
|
/// common option, which is why it has this shorthand.
|
||||||
|
fn default() -> TimeTypes {
|
||||||
|
TimeTypes { accessed: false, modified: true, created: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug, Clone)]
|
||||||
|
pub struct Cell {
|
||||||
|
pub length: usize,
|
||||||
|
pub text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cell {
|
||||||
|
pub fn empty() -> Cell {
|
||||||
|
Cell {
|
||||||
|
text: String::new(),
|
||||||
|
length: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn paint(style: Style, string: &str) -> Cell {
|
||||||
|
Cell {
|
||||||
|
text: style.paint(string).to_string(),
|
||||||
|
length: UnicodeWidthStr::width(string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_spaces(&mut self, count: usize) {
|
||||||
|
self.length += count;
|
||||||
|
for _ in 0 .. count {
|
||||||
|
self.text.push(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn append(&mut self, other: &Cell) {
|
||||||
|
self.length += other.length;
|
||||||
|
self.text.push_str(&*other.text);
|
||||||
|
}
|
||||||
|
}
|
@ -119,12 +119,12 @@ use std::ops::Add;
|
|||||||
use std::iter::repeat;
|
use std::iter::repeat;
|
||||||
|
|
||||||
use colours::Colours;
|
use colours::Colours;
|
||||||
use column::{Alignment, Column, Cell};
|
|
||||||
use dir::Dir;
|
use dir::Dir;
|
||||||
use feature::xattr::{Attribute, FileAttributes};
|
use feature::xattr::{Attribute, FileAttributes};
|
||||||
use file::fields as f;
|
use file::fields as f;
|
||||||
use file::File;
|
use file::File;
|
||||||
use options::{Columns, FileFilter, RecurseOptions, SizeFormat};
|
use options::{FileFilter, RecurseOptions};
|
||||||
|
use output::column::{Alignment, Column, Columns, Cell, SizeFormat};
|
||||||
|
|
||||||
use ansi_term::{ANSIString, ANSIStrings, Style};
|
use ansi_term::{ANSIString, ANSIStrings, Style};
|
||||||
|
|
||||||
@ -153,7 +153,7 @@ use super::filename;
|
|||||||
///
|
///
|
||||||
/// Almost all the heavy lifting is done in a Table object, which handles the
|
/// Almost all the heavy lifting is done in a Table object, which handles the
|
||||||
/// columns for each row.
|
/// columns for each row.
|
||||||
#[derive(PartialEq, Debug, Copy, Clone, Default)]
|
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||||
pub struct Details {
|
pub struct Details {
|
||||||
|
|
||||||
/// A Columns object that says which columns should be included in the
|
/// A Columns object that says which columns should be included in the
|
||||||
@ -486,10 +486,14 @@ impl<U> Table<U> where U: Users {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn display(&mut self, file: &File, column: &Column, xattrs: bool) -> Cell {
|
fn display(&mut self, file: &File, column: &Column, xattrs: bool) -> Cell {
|
||||||
|
use output::column::TimeType::*;
|
||||||
|
|
||||||
match *column {
|
match *column {
|
||||||
Column::Permissions => self.render_permissions(file.permissions(), xattrs),
|
Column::Permissions => self.render_permissions(file.permissions(), xattrs),
|
||||||
Column::FileSize(fmt) => self.render_size(file.size(), fmt),
|
Column::FileSize(fmt) => self.render_size(file.size(), fmt),
|
||||||
Column::Timestamp(t) => self.render_time(file.timestamp(t)),
|
Column::Timestamp(Modified) => self.render_time(file.modified_time()),
|
||||||
|
Column::Timestamp(Created) => self.render_time(file.created_time()),
|
||||||
|
Column::Timestamp(Accessed) => self.render_time(file.accessed_time()),
|
||||||
Column::HardLinks => self.render_links(file.links()),
|
Column::HardLinks => self.render_links(file.links()),
|
||||||
Column::Inode => self.render_inode(file.inode()),
|
Column::Inode => self.render_inode(file.inode()),
|
||||||
Column::Blocks => self.render_blocks(file.blocks()),
|
Column::Blocks => self.render_blocks(file.blocks()),
|
||||||
@ -754,8 +758,7 @@ pub mod test {
|
|||||||
pub use super::Table;
|
pub use super::Table;
|
||||||
pub use file::File;
|
pub use file::File;
|
||||||
pub use file::fields as f;
|
pub use file::fields as f;
|
||||||
|
pub use output::column::{Cell, Column};
|
||||||
pub use column::{Cell, Column};
|
|
||||||
|
|
||||||
pub use users::{User, Group, uid_t, gid_t};
|
pub use users::{User, Group, uid_t, gid_t};
|
||||||
pub use users::mock::MockUsers;
|
pub use users::mock::MockUsers;
|
||||||
|
@ -3,10 +3,11 @@ use std::iter::repeat;
|
|||||||
use users::OSUsers;
|
use users::OSUsers;
|
||||||
use term_grid as grid;
|
use term_grid as grid;
|
||||||
|
|
||||||
use column::{Column, Cell};
|
|
||||||
use dir::Dir;
|
use dir::Dir;
|
||||||
use feature::xattr::FileAttributes;
|
use feature::xattr::FileAttributes;
|
||||||
use file::File;
|
use file::File;
|
||||||
|
|
||||||
|
use output::column::{Column, Cell};
|
||||||
use output::details::{Details, Table};
|
use output::details::{Details, Table};
|
||||||
use output::grid::Grid;
|
use output::grid::Grid;
|
||||||
|
|
||||||
|
@ -13,6 +13,8 @@ mod grid;
|
|||||||
pub mod details;
|
pub mod details;
|
||||||
mod lines;
|
mod lines;
|
||||||
mod grid_details;
|
mod grid_details;
|
||||||
|
pub mod column;
|
||||||
|
|
||||||
|
|
||||||
pub fn filename(file: &File, colours: &Colours, links: bool) -> String {
|
pub fn filename(file: &File, colours: &Colours, links: bool) -> String {
|
||||||
if links && file.is_link() {
|
if links && file.is_link() {
|
||||||
|
Loading…
Reference in New Issue
Block a user