mirror of
https://github.com/Llewellynvdm/exa.git
synced 2024-12-27 10:22:40 +00:00
Move many Options structs to the output module
This cleans up the options module, moving the structs that were *only* in use for the columns view out of it. The new OptionSet trait is used to add the ‘deduce’ methods that used to be present on the values.
This commit is contained in:
parent
cc04d0452f
commit
10468797bb
@ -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);
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@ use std::path::{Component, Path, PathBuf};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use dir::Dir;
|
||||
use options::TimeType;
|
||||
use output::column::TimeType;
|
||||
|
||||
use self::fields as f;
|
||||
|
||||
|
@ -28,7 +28,6 @@ use file::File;
|
||||
use options::{Options, View};
|
||||
|
||||
mod colours;
|
||||
mod column;
|
||||
mod dir;
|
||||
mod feature;
|
||||
mod file;
|
||||
@ -99,8 +98,8 @@ impl Exa {
|
||||
}
|
||||
};
|
||||
|
||||
self.options.filter_files(&mut children);
|
||||
self.options.sort_files(&mut children);
|
||||
self.options.filter.filter_files(&mut children);
|
||||
self.options.filter.sort_files(&mut children);
|
||||
|
||||
if let Some(recurse_opts) = self.options.dir_action.recurse_options() {
|
||||
let depth = dir.path.components().filter(|&c| c != Component::CurDir).count() + 1;
|
||||
|
481
src/options.rs
481
src/options.rs
@ -7,21 +7,26 @@ use getopts;
|
||||
use natord;
|
||||
|
||||
use colours::Colours;
|
||||
use column::Column;
|
||||
use column::Column::*;
|
||||
use dir::Dir;
|
||||
use feature::xattr;
|
||||
use file::File;
|
||||
use output::{Grid, Details, GridDetails, Lines};
|
||||
use output::column::{Columns, TimeTypes, SizeFormat};
|
||||
use term::dimensions;
|
||||
|
||||
|
||||
/// The *Options* struct represents a parsed version of the user's
|
||||
/// command-line options.
|
||||
/// These **options** represent a parsed, error-checked versions of the
|
||||
/// user's command-line options.
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
pub struct Options {
|
||||
|
||||
/// The action to perform when encountering a directory rather than a
|
||||
/// regular file.
|
||||
pub dir_action: DirAction,
|
||||
|
||||
/// How to sort and filter files before outputting them.
|
||||
pub filter: FileFilter,
|
||||
|
||||
/// The type of output to use (lines, grid, or details).
|
||||
pub view: View,
|
||||
}
|
||||
|
||||
@ -107,14 +112,6 @@ impl Options {
|
||||
}, 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
|
||||
/// status column. It's only worth trying to discover a repository if the
|
||||
/// results will end up being displayed.
|
||||
@ -128,143 +125,6 @@ impl Options {
|
||||
}
|
||||
|
||||
|
||||
#[derive(Default, PartialEq, Debug, Copy, Clone)]
|
||||
pub struct FileFilter {
|
||||
list_dirs_first: bool,
|
||||
reverse: bool,
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
pub enum View {
|
||||
Details(Details),
|
||||
@ -274,7 +134,7 @@ pub enum 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::*;
|
||||
|
||||
let long = || {
|
||||
@ -356,8 +216,8 @@ impl View {
|
||||
}
|
||||
}
|
||||
else {
|
||||
// If the terminal width couldn't be matched for some reason, such
|
||||
// as the program's stdout being connected to a file, then
|
||||
// If the terminal width couldn’t be matched for some reason, such
|
||||
// as the program’s stdout being connected to a file, then
|
||||
// fallback to the lines view.
|
||||
let lines = Lines {
|
||||
colours: Colours::plain(),
|
||||
@ -389,21 +249,135 @@ impl View {
|
||||
}
|
||||
|
||||
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
pub enum SizeFormat {
|
||||
DecimalBytes,
|
||||
BinaryBytes,
|
||||
JustBytes,
|
||||
trait OptionSet: Sized {
|
||||
fn deduce(matches: &getopts::Matches) -> Result<Self, Misfire>;
|
||||
}
|
||||
|
||||
impl Default for SizeFormat {
|
||||
fn default() -> SizeFormat {
|
||||
SizeFormat::DecimalBytes
|
||||
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"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl SizeFormat {
|
||||
pub fn deduce(matches: &getopts::Matches) -> Result<SizeFormat, Misfire> {
|
||||
|
||||
/// 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 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)]
|
||||
pub enum SortField {
|
||||
Unsorted, Name, Extension, Size, FileInode,
|
||||
ModifiedDate, AccessedDate, CreatedDate,
|
||||
}
|
||||
|
||||
impl Default for SortField {
|
||||
fn default() -> SortField {
|
||||
SortField::Name
|
||||
}
|
||||
}
|
||||
|
||||
impl OptionSet for SortField {
|
||||
fn deduce(matches: &getopts::Matches) -> Result<SortField, Misfire> {
|
||||
match matches.opt_str("sort") {
|
||||
Some(word) => SortField::from_word(word),
|
||||
None => Ok(SortField::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 bytes = matches.opt_present("bytes");
|
||||
|
||||
@ -417,40 +391,18 @@ impl SizeFormat {
|
||||
}
|
||||
|
||||
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
pub enum TimeType {
|
||||
FileAccessed,
|
||||
FileModified,
|
||||
FileCreated,
|
||||
}
|
||||
impl OptionSet for TimeTypes {
|
||||
|
||||
impl TimeType {
|
||||
pub fn header(&self) -> &'static str {
|
||||
match *self {
|
||||
TimeType::FileAccessed => "Date Accessed",
|
||||
TimeType::FileModified => "Date Modified",
|
||||
TimeType::FileCreated => "Date Created",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[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.
|
||||
/// Determine which of a file’s time fields should be displayed for it
|
||||
/// based on the user’s options.
|
||||
///
|
||||
/// There are two separate ways to pick which fields to show: with a
|
||||
/// flag (such as `--modified`) or with a parameter (such as
|
||||
/// `--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.
|
||||
fn deduce(matches: &getopts::Matches) -> Result<TimeTypes, Misfire> {
|
||||
let possible_word = matches.opt_str("time");
|
||||
let modified = matches.opt_present("modified");
|
||||
@ -468,11 +420,11 @@ impl TimeTypes {
|
||||
return Err(Misfire::Useless("accessed", true, "time"));
|
||||
}
|
||||
|
||||
match &word[..] {
|
||||
"mod" | "modified" => Ok(TimeTypes { accessed: false, modified: true, created: false }),
|
||||
"acc" | "accessed" => Ok(TimeTypes { accessed: true, modified: false, created: false }),
|
||||
"cr" | "created" => Ok(TimeTypes { accessed: false, modified: false, created: true }),
|
||||
field => Err(TimeTypes::none(field)),
|
||||
match &*word {
|
||||
"mod" | "modified" => Ok(TimeTypes { accessed: false, modified: true, created: false }),
|
||||
"acc" | "accessed" => Ok(TimeTypes { accessed: true, modified: false, created: false }),
|
||||
"cr" | "created" => Ok(TimeTypes { accessed: false, modified: false, created: true }),
|
||||
otherwise => Err(Misfire::InvalidOptions(getopts::Fail::UnrecognizedOption(format!("--time {}", otherwise)))),
|
||||
}
|
||||
}
|
||||
else {
|
||||
@ -484,11 +436,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,83 +515,61 @@ impl RecurseOptions {
|
||||
}
|
||||
|
||||
|
||||
#[derive(PartialEq, Copy, Clone, Debug, Default)]
|
||||
pub struct Columns {
|
||||
size_format: SizeFormat,
|
||||
time_types: TimeTypes,
|
||||
inode: bool,
|
||||
links: bool,
|
||||
blocks: bool,
|
||||
group: bool,
|
||||
git: bool
|
||||
/// 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 Columns {
|
||||
pub 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"),
|
||||
})
|
||||
}
|
||||
|
||||
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(Inode);
|
||||
}
|
||||
|
||||
columns.push(Permissions);
|
||||
|
||||
if self.links {
|
||||
columns.push(HardLinks);
|
||||
}
|
||||
|
||||
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
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::Options;
|
||||
|
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::FileModified));
|
||||
}
|
||||
|
||||
if self.time_types.created {
|
||||
columns.push(Column::Timestamp(TimeType::FileCreated));
|
||||
}
|
||||
|
||||
if self.time_types.accessed {
|
||||
columns.push(Column::Timestamp(TimeType::FileAccessed));
|
||||
}
|
||||
|
||||
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`).
|
||||
FileAccessed,
|
||||
|
||||
/// The file’s modified time (`st_mtime`).
|
||||
FileModified,
|
||||
|
||||
/// The file’s creation time (`st_ctime`).
|
||||
FileCreated,
|
||||
}
|
||||
|
||||
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::FileAccessed => "Date Accessed",
|
||||
TimeType::FileModified => "Date Modified",
|
||||
TimeType::FileCreated => "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 colours::Colours;
|
||||
use column::{Alignment, Column, Cell};
|
||||
use dir::Dir;
|
||||
use feature::xattr::{Attribute, FileAttributes};
|
||||
use file::fields as f;
|
||||
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};
|
||||
|
||||
@ -153,7 +153,7 @@ use super::filename;
|
||||
///
|
||||
/// Almost all the heavy lifting is done in a Table object, which handles the
|
||||
/// columns for each row.
|
||||
#[derive(PartialEq, Debug, Copy, Clone, Default)]
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
pub struct Details {
|
||||
|
||||
/// A Columns object that says which columns should be included in the
|
||||
@ -658,7 +658,7 @@ impl<U> Table<U> where U: Users {
|
||||
.map(|n| self.rows.iter().map(|row| row.column_width(n)).max().unwrap_or(0))
|
||||
.collect();
|
||||
|
||||
let total_width: usize = self.columns.len() + column_widths.iter().fold(0,Add::add);
|
||||
let total_width: usize = self.columns.len() + column_widths.iter().fold(0, Add::add);
|
||||
|
||||
for row in self.rows.iter() {
|
||||
let mut cell = Cell::empty();
|
||||
@ -754,8 +754,7 @@ pub mod test {
|
||||
pub use super::Table;
|
||||
pub use file::File;
|
||||
pub use file::fields as f;
|
||||
|
||||
pub use column::{Cell, Column};
|
||||
pub use output::column::{Cell, Column};
|
||||
|
||||
pub use users::{User, Group, uid_t, gid_t};
|
||||
pub use users::mock::MockUsers;
|
||||
|
@ -3,10 +3,11 @@ use std::iter::repeat;
|
||||
use users::OSUsers;
|
||||
use term_grid as grid;
|
||||
|
||||
use column::{Column, Cell};
|
||||
use dir::Dir;
|
||||
use feature::xattr::FileAttributes;
|
||||
use file::File;
|
||||
|
||||
use output::column::{Column, Cell};
|
||||
use output::details::{Details, Table};
|
||||
use output::grid::Grid;
|
||||
|
||||
|
@ -13,6 +13,8 @@ mod grid;
|
||||
pub mod details;
|
||||
mod lines;
|
||||
mod grid_details;
|
||||
pub mod column;
|
||||
|
||||
|
||||
pub fn filename(file: &File, colours: &Colours, links: bool) -> String {
|
||||
if links && file.is_link() {
|
||||
|
Loading…
Reference in New Issue
Block a user