mirror of
https://github.com/Llewellynvdm/exa.git
synced 2024-11-23 12:32:00 +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 unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
use dir::Dir;
|
use dir::Dir;
|
||||||
use options::TimeType;
|
use output::column::TimeType;
|
||||||
|
|
||||||
use self::fields as f;
|
use self::fields as f;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
@ -99,8 +98,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;
|
||||||
|
463
src/options.rs
463
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,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,14 +112,6 @@ impl Options {
|
|||||||
}, path_strs))
|
}, 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
|
||||||
/// status column. It's only worth trying to discover a repository if the
|
/// status column. It's only worth trying to discover a repository if the
|
||||||
/// results will end up being displayed.
|
/// 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)]
|
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||||
pub enum View {
|
pub enum View {
|
||||||
Details(Details),
|
Details(Details),
|
||||||
@ -274,7 +134,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 +216,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 +249,135 @@ 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 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> {
|
||||||
|
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 binary = matches.opt_present("binary");
|
||||||
let bytes = matches.opt_present("bytes");
|
let bytes = matches.opt_present("bytes");
|
||||||
|
|
||||||
@ -417,40 +391,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 +420,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::InvalidOptions(getopts::Fail::UnrecognizedOption(format!("--time {}", otherwise)))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
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,82 +515,60 @@ 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> {
|
/// The OS return code this misfire should signify.
|
||||||
Ok(Columns {
|
pub fn error_code(&self) -> i32 {
|
||||||
size_format: try!(SizeFormat::deduce(matches)),
|
if let Misfire::Help(_) = *self { 2 }
|
||||||
time_types: try!(TimeTypes::deduce(matches)),
|
else { 3 }
|
||||||
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 {
|
impl fmt::Display for Misfire {
|
||||||
self.git
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
}
|
use self::Misfire::*;
|
||||||
|
|
||||||
pub fn for_dir(&self, dir: Option<&Dir>) -> Vec<Column> {
|
match *self {
|
||||||
let mut columns = vec![];
|
InvalidOptions(ref e) => write!(f, "{}", e),
|
||||||
|
Help(ref text) => write!(f, "{}", text),
|
||||||
if self.inode {
|
Version => write!(f, "exa {}", env!("CARGO_PKG_VERSION")),
|
||||||
columns.push(Inode);
|
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),
|
||||||
columns.push(Permissions);
|
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),
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod 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::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 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
|
||||||
@ -754,8 +754,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