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:
Ben S 2015-11-14 23:32:57 +00:00
parent cc04d0452f
commit 10468797bb
8 changed files with 446 additions and 381 deletions

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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 couldnt be matched for some reason, such
// as the programs stdout being connected to a file, then
// fallback to the lines view.
let lines = Lines {
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 users 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 SizeFormat {
DecimalBytes,
BinaryBytes,
JustBytes,
pub enum SortField {
Unsorted, Name, Extension, Size, FileInode,
ModifiedDate, AccessedDate, CreatedDate,
}
impl Default for SizeFormat {
fn default() -> SizeFormat {
SizeFormat::DecimalBytes
impl Default for SortField {
fn default() -> SortField {
SortField::Name
}
}
impl SizeFormat {
pub fn deduce(matches: &getopts::Matches) -> Result<SizeFormat, Misfire> {
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 users options.
///
/// The default mode is to use the decimal prefixes, as they are the
/// most commonly-understood, and dont 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 files time fields should be displayed for it
/// based on the users 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.
///
/// Its 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[..] {
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)),
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,82 +515,60 @@ 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"),
})
impl Misfire {
/// The OS return code this misfire should signify.
pub fn error_code(&self) -> i32 {
if let Misfire::Help(_) = *self { 2 }
else { 3 }
}
}
pub fn should_scan_for_git(&self) -> bool {
self.git
}
impl fmt::Display for Misfire {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use self::Misfire::*;
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);
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),
}
}
}
columns
}
}
#[cfg(test)]
mod test {

231
src/output/column.rs Normal file
View 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 files time fields. These three fields are standard
/// across most (all?) operating systems.
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum TimeType {
/// The files accessed time (`st_atime`).
FileAccessed,
/// The files modified time (`st_mtime`).
FileModified,
/// The files creation time (`st_ctime`).
FileCreated,
}
impl TimeType {
/// Returns the text to use for a columns 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 files 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);
}
}

View File

@ -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
@ -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;

View File

@ -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;

View File

@ -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() {