diff options
-rw-r--r-- | Cargo.lock | 79 | ||||
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | src/dir.rs | 12 | ||||
-rw-r--r-- | src/file.rs | 37 | ||||
-rw-r--r-- | src/main.rs | 186 | ||||
-rw-r--r-- | src/options.rs | 67 | ||||
-rw-r--r-- | src/output/details.rs | 312 |
7 files changed, 415 insertions, 280 deletions
@@ -5,7 +5,7 @@ dependencies = [ "ansi_term 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", "bitflags 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "datetime 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "getopts 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", + "getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", "git2 0.3.0 (git+https://github.com/alexcrichton/git2-rs.git)", "libc 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "locale 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", @@ -13,8 +13,8 @@ dependencies = [ "num_cpus 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "number_prefix 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", "pad 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "scoped_threadpool 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "term_grid 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "threadpool 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-width 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "users 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -30,10 +30,10 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "memchr 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -48,12 +48,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "byteorder" -version = "0.3.11" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "cmake" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "gcc 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)", @@ -69,7 +69,7 @@ dependencies = [ "pad 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)", "regex_macros 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", - "tz 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tz 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -83,11 +83,8 @@ dependencies = [ [[package]] name = "getopts" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "log 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", -] [[package]] name = "git2" @@ -110,11 +107,11 @@ name = "libgit2-sys" version = "0.3.2" source = "git+https://github.com/alexcrichton/git2-rs.git#cbe8e1a65ac9b16bc05137f80673e74c4d36f6e5" dependencies = [ - "cmake 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "cmake 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "libssh2-sys 0.1.30 (registry+https://github.com/rust-lang/crates.io-index)", "libz-sys 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", - "openssl-sys 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-sys 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", "pkg-config 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -131,10 +128,10 @@ name = "libssh2-sys" version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "cmake 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "cmake 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "libz-sys 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", - "openssl-sys 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-sys 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", "pkg-config 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -158,21 +155,13 @@ dependencies = [ ] [[package]] -name = "log" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] name = "matches" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "memchr" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "libc 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", @@ -188,7 +177,7 @@ name = "num" version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "rand 0.3.10 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -210,7 +199,7 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "gcc 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)", @@ -242,7 +231,7 @@ dependencies = [ [[package]] name = "rand" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "advapi32-sys 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -255,8 +244,8 @@ name = "regex" version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "aho-corasick 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "memchr 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "aho-corasick 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "regex-syntax 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -279,11 +268,32 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] +name = "rustc_version" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "semver 0.1.20 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "scoped_threadpool" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rustc_version 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "semver" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] name = "tempdir" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "rand 0.3.10 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -295,16 +305,11 @@ dependencies = [ ] [[package]] -name = "threadpool" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] name = "tz" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "byteorder 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -17,8 +17,8 @@ natord = "1.0.7" num_cpus = "*" number_prefix = "0.2.3" pad = "0.1.1" +scoped_threadpool = "*" term_grid = "*" -threadpool = "*" unicode-width = "*" users = "0.4.0" @@ -14,8 +14,15 @@ use file::{File, fields}; /// check the existence of surrounding files, then highlight themselves /// accordingly. (See `File#get_source_files`) pub struct Dir { + + /// A vector of the files that have been read from this directory. contents: Vec<PathBuf>, - path: PathBuf, + + /// The path that was read. + pub path: PathBuf, + + /// Holds a `Git` object if scanning for Git repositories is switched on, + /// and this directory happens to contain one. git: Option<Git>, } @@ -25,7 +32,7 @@ impl Dir { /// pointed to by the given path. Fails if the directory can't be read, or /// isn't actually a directory, or if there's an IO error that occurs /// while scanning. - pub fn readdir(path: &Path, git: bool) -> io::Result<Dir> { + pub fn read_dir(path: &Path, git: bool) -> io::Result<Dir> { let reader = try!(fs::read_dir(path)); let contents = try!(reader.map(|e| e.map(|e| e.path())).collect()); @@ -71,6 +78,7 @@ impl Dir { } +/// Iterator over reading the contents of a directory as `File` objects. pub struct Files<'dir> { inner: SliceIter<'dir, PathBuf>, dir: &'dir Dir, diff --git a/src/file.rs b/src/file.rs index be8f960..4e2617d 100644 --- a/src/file.rs +++ b/src/file.rs @@ -56,6 +56,7 @@ pub struct File<'dir> { } impl<'dir> File<'dir> { + /// Create a new `File` object from the given `Path`, inside the given /// `Dir`, if appropriate. /// @@ -70,11 +71,11 @@ impl<'dir> File<'dir> { let filename = path_filename(path); File { - path: path.to_path_buf(), - dir: parent, - metadata: metadata, - ext: ext(&filename), - name: filename.to_string(), + path: path.to_path_buf(), + dir: parent, + metadata: metadata, + ext: ext(&filename), + name: filename.to_string(), } } @@ -83,8 +84,14 @@ impl<'dir> File<'dir> { self.metadata.is_dir() } - pub fn to_dir(&self) -> io::Result<Dir> { - Dir::readdir(&*self.path, false) + /// If this file is a directory on the filesystem, then clone its + /// `PathBuf` for use in one of our own `Dir` objects, and read a list of + /// its contents. + /// + /// Returns an IO error upon failure, but this shouldn't be used to check + /// if a `File` is a directory or not! For that, just use `is_directory()`. + pub fn to_dir(&self, scan_for_git: bool) -> io::Result<Dir> { + Dir::read_dir(&*self.path, scan_for_git) } /// Whether this file is a regular file on the filesystem - that is, not a @@ -178,11 +185,11 @@ impl<'dir> File<'dir> { // Use plain `metadata` instead of `symlink_metadata` - we *want* to follow links. if let Ok(metadata) = fs::metadata(&target_path) { Ok(File { - path: target_path.to_path_buf(), - dir: self.dir, - metadata: metadata, - ext: ext(&filename), - name: filename.to_string(), + path: target_path.to_path_buf(), + dir: self.dir, + metadata: metadata, + ext: ext(&filename), + name: filename.to_string(), }) } else { @@ -282,6 +289,10 @@ impl<'dir> File<'dir> { } /// This file's permissions, with flags for each bit. + /// + /// The extended-attribute '@' character that you see in here is in fact + /// added in later, to avoid querying the extended attributes more than + /// once. (Yes, it's a little hacky.) pub fn permissions(&self) -> f::Permissions { let bits = self.metadata.permissions().mode(); let has_bit = |bit| { bits & bit == bit }; @@ -297,7 +308,6 @@ impl<'dir> File<'dir> { other_read: has_bit(unix::fs::OTHER_READ), other_write: has_bit(unix::fs::OTHER_WRITE), other_execute: has_bit(unix::fs::OTHER_EXECUTE), - attribute: false, // !self.xattrs.is_empty() } } @@ -423,7 +433,6 @@ pub mod fields { pub other_read: bool, pub other_write: bool, pub other_execute: bool, - pub attribute: bool, } pub struct Links { diff --git a/src/main.rs b/src/main.rs index e64f3fb..7e540de 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,8 +11,8 @@ extern crate natord; extern crate num_cpus; extern crate number_prefix; extern crate pad; +extern crate scoped_threadpool; extern crate term_grid; -extern crate threadpool; extern crate unicode_width; extern crate users; @@ -21,12 +21,8 @@ extern crate git2; use std::env; -use std::fs; -use std::path::{Component, Path, PathBuf}; +use std::path::{Component, Path}; use std::process; -use std::sync::mpsc::channel; - -use threadpool::ThreadPool; use dir::Dir; use file::File; @@ -43,95 +39,43 @@ mod output; mod term; -#[cfg(not(test))] -struct Exa<'dir> { - count: usize, +struct Exa { options: Options, - dirs: Vec<PathBuf>, - files: Vec<File<'dir>>, } -#[cfg(not(test))] -impl<'dir> Exa<'dir> { - fn new(options: Options) -> Exa<'dir> { - Exa { - count: 0, - options: options, - dirs: Vec::new(), - files: Vec::new(), - } - } - - fn load(&mut self, files: &[String]) { - - // Separate the user-supplied paths into directories and files. - // Files are shown first, and then each directory is expanded - // and listed second. - let is_tree = self.options.dir_action.is_tree() || self.options.dir_action.is_as_file(); - let total_files = files.len(); - - - // Communication between consumer thread and producer threads - enum StatResult<'dir> { - File(File<'dir>), - Dir(PathBuf), - Error - } - - let pool = ThreadPool::new(8 * num_cpus::get()); - let (tx, rx) = channel(); +impl Exa { + fn run(&mut self, args_file_names: &[String]) { + let mut files = Vec::new(); + let mut dirs = Vec::new(); - for file in files.iter() { - let tx = tx.clone(); - let file = file.clone(); - - // Spawn producer thread - pool.execute(move || { - let path = Path::new(&*file); - let _ = tx.send(match fs::metadata(&path) { - Ok(metadata) => { - if is_tree || !metadata.is_dir() { - StatResult::File(File::with_metadata(metadata, &path, None)) - } - else { - StatResult::Dir(path.to_path_buf()) + for file_name in args_file_names.iter() { + match File::from_path(Path::new(&file_name), None) { + Err(e) => { + println!("{}: {}", file_name, e); + }, + Ok(f) => { + if f.is_directory() && !self.options.dir_action.treat_dirs_as_files() { + match f.to_dir(self.options.should_scan_for_git()) { + Ok(d) => dirs.push(d), + Err(e) => println!("{}: {}", file_name, e), } } - Err(e) => { - println!("{}: {}", file, e); - StatResult::Error + else { + files.push(f); } - }); - }); - } - - // Spawn consumer thread - for result in rx.iter().take(total_files) { - match result { - StatResult::File(file) => self.files.push(file), - StatResult::Dir(path) => self.dirs.push(path), - StatResult::Error => () + }, } - self.count += 1; } - } - fn print_files(&self) { - if !self.files.is_empty() { - self.print(None, &self.files[..]); - } - } + let any_files = files.is_empty(); + self.print_files(None, files); - fn print_dirs(&mut self) { - let mut first = self.files.is_empty(); + let is_only_dir = dirs.len() == 1; + self.print_dirs(dirs, any_files, is_only_dir); + } - // Directories are put on a stack rather than just being iterated through, - // as the vector can change as more directories are added. - loop { - let dir_path = match self.dirs.pop() { - None => break, - Some(f) => f, - }; + fn print_dirs(&self, dir_files: Vec<Dir>, mut first: bool, is_only_dir: bool) { + for dir in dir_files { // Put a gap between directories, or between the list of files and the // first directory. @@ -142,68 +86,66 @@ impl<'dir> Exa<'dir> { print!("\n"); } - match Dir::readdir(&dir_path, self.options.should_scan_for_git()) { - Ok(ref dir) => { - let mut files = Vec::new(); + if !is_only_dir { + println!("{}:", dir.path.display()); + } - for file in dir.files() { - match file { - Ok(file) => files.push(file), - Err((path, e)) => println!("[{}: {}]", path.display(), e), - } - } + let mut children = Vec::new(); + for file in dir.files() { + match file { + Ok(file) => children.push(file), + Err((path, e)) => println!("[{}: {}]", path.display(), e), + } + }; + + self.options.filter_files(&mut children); + self.options.sort_files(&mut children); - self.options.transform_files(&mut files); - - // When recursing, add any directories to the dirs stack - // backwards: the *last* element of the stack is used each - // time, so by inserting them backwards, they get displayed in - // the correct sort order. - if let Some(recurse_opts) = self.options.dir_action.recurse_options() { - let depth = dir_path.components().filter(|&c| c != Component::CurDir).count() + 1; - if !recurse_opts.tree && !recurse_opts.is_too_deep(depth) { - for dir in files.iter().filter(|f| f.is_directory()).rev() { - self.dirs.push(dir.path.clone()); - } + if let Some(recurse_opts) = self.options.dir_action.recurse_options() { + let depth = dir.path.components().filter(|&c| c != Component::CurDir).count() + 1; + if !recurse_opts.tree && !recurse_opts.is_too_deep(depth) { + + let mut child_dirs = Vec::new(); + for child_dir in children.iter().filter(|f| f.is_directory()) { + match child_dir.to_dir(false) { + Ok(d) => child_dirs.push(d), + Err(e) => println!("{}: {}", child_dir.path.display(), e), } } - if self.count > 1 { - println!("{}:", dir_path.display()); + self.print_files(Some(&dir), children); + + if !child_dirs.is_empty() { + self.print_dirs(child_dirs, false, false); } - self.count += 1; - self.print(Some(dir), &files[..]); - } - Err(e) => { - println!("{}: {}", dir_path.display(), e); - return; + continue; } - }; + } + + self.print_files(Some(&dir), children); + } } - fn print(&self, dir: Option<&Dir>, files: &[File]) { + fn print_files(&self, dir: Option<&Dir>, files: Vec<File>) { match self.options.view { - View::Grid(g) => g.view(files), + View::Grid(g) => g.view(&files), View::Details(d) => d.view(dir, files), - View::GridDetails(gd) => gd.view(dir, files), - View::Lines(l) => l.view(files), + View::GridDetails(gd) => gd.view(dir, &files), + View::Lines(l) => l.view(&files), } } } -#[cfg(not(test))] fn main() { let args: Vec<String> = env::args().skip(1).collect(); match Options::getopts(&args) { Ok((options, paths)) => { - let mut exa = Exa::new(options); - exa.load(&paths); - exa.print_files(); - exa.print_dirs(); + let mut exa = Exa { options: options }; + exa.run(&paths); }, Err(e) => { println!("{}", e); diff --git a/src/options.rs b/src/options.rs index d88eee5..dd76218 100644 --- a/src/options.rs +++ b/src/options.rs @@ -107,8 +107,12 @@ impl Options { }, path_strs)) } - pub fn transform_files(&self, files: &mut Vec<File>) { - self.filter.transform_files(files) + 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 @@ -124,7 +128,7 @@ impl Options { } -#[derive(PartialEq, Debug, Copy, Clone)] +#[derive(Default, PartialEq, Debug, Copy, Clone)] pub struct FileFilter { list_dirs_first: bool, reverse: bool, @@ -133,26 +137,14 @@ pub struct FileFilter { } impl FileFilter { - /// Transform the files (sorting, reversing, filtering) before listing them. - pub fn transform_files(&self, files: &mut Vec<File>) { - + pub fn filter_files(&self, files: &mut Vec<File>) { if !self.show_invisibles { files.retain(|f| !f.is_dotfile()); } + } - match self.sort_field { - SortField::Unsorted => {}, - SortField::Name => files.sort_by(|a, b| natord::compare(&*a.name, &*b.name)), - SortField::Size => files.sort_by(|a, b| a.metadata.len().cmp(&b.metadata.len())), - SortField::FileInode => files.sort_by(|a, b| a.metadata.ino().cmp(&b.metadata.ino())), - SortField::ModifiedDate => files.sort_by(|a, b| a.metadata.mtime().cmp(&b.metadata.mtime())), - SortField::AccessedDate => files.sort_by(|a, b| a.metadata.atime().cmp(&b.metadata.atime())), - SortField::CreatedDate => files.sort_by(|a, b| a.metadata.ctime().cmp(&b.metadata.ctime())), - SortField::Extension => files.sort_by(|a, b| match a.ext.cmp(&b.ext) { - cmp::Ordering::Equal => natord::compare(&*a.name, &*b.name), - order => order, - }), - } + pub fn sort_files(&self, files: &mut Vec<File>) { + files.sort_by(|a, b| self.compare_files(a, b)); if self.reverse { files.reverse(); @@ -163,6 +155,22 @@ impl FileFilter { 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. @@ -280,7 +288,8 @@ impl View { let details = Details { columns: Some(try!(Columns::deduce(matches))), header: matches.opt_present("header"), - recurse: dir_action.recurse_options().map(|o| (o, filter)), + recurse: dir_action.recurse_options(), + filter: filter, xattr: xattr::ENABLED && matches.opt_present("extended"), colours: if dimensions().is_some() { Colours::colourful() } else { Colours::plain() }, }; @@ -328,7 +337,8 @@ impl View { let details = Details { columns: None, header: false, - recurse: dir_action.recurse_options().map(|o| (o, filter)), + recurse: dir_action.recurse_options(), + filter: filter, xattr: false, colours: if dimensions().is_some() { Colours::colourful() } else { Colours::plain() }, }; @@ -406,6 +416,7 @@ impl SizeFormat { } } + #[derive(PartialEq, Debug, Copy, Clone)] pub enum TimeType { FileAccessed, @@ -423,6 +434,7 @@ impl TimeType { } } + #[derive(PartialEq, Debug, Copy, Clone)] pub struct TimeTypes { accessed: bool, @@ -479,6 +491,7 @@ impl TimeTypes { } } + /// What to do when encountering a directory? #[derive(PartialEq, Debug, Copy, Clone)] pub enum DirAction { @@ -510,21 +523,16 @@ impl DirAction { } } - pub fn is_as_file(&self) -> bool { + pub fn treat_dirs_as_files(&self) -> bool { match *self { DirAction::AsFile => true, - _ => false, - } - } - - pub fn is_tree(&self) -> bool { - match *self { DirAction::Recurse(RecurseOptions { tree, .. }) => tree, _ => false, - } + } } } + #[derive(PartialEq, Debug, Copy, Clone)] pub struct RecurseOptions { pub tree: bool, @@ -559,6 +567,7 @@ impl RecurseOptions { } } + #[derive(PartialEq, Copy, Clone, Debug, Default)] pub struct Columns { size_format: SizeFormat, diff --git a/src/output/details.rs b/src/output/details.rs index 3052448..f87d0ad 100644 --- a/src/output/details.rs +++ b/src/output/details.rs @@ -1,3 +1,116 @@ +//! The **Details** output view displays each file as a row in a table. +//! +//! It's used in the following situations: +//! +//! - Most commonly, when using the `--long` command-line argument to display the +//! details of each file, which requires using a table view to hold all the data; +//! - When using the `--tree` argument, which uses the same table view to display +//! each file on its own line, with the table providing the tree characters; +//! - When using both the `--long` and `--grid` arguments, which constructs a +//! series of tables to fit all the data on the screen. +//! +//! You will probably recognise it from the `ls --long` command. It looks like +//! this: +//! +//! .rw-r--r-- 9.6k ben 29 Jun 16:16 Cargo.lock +//! .rw-r--r-- 547 ben 23 Jun 10:54 Cargo.toml +//! .rw-r--r-- 1.1k ben 23 Nov 2014 LICENCE +//! .rw-r--r-- 2.5k ben 21 May 14:38 README.md +//! .rw-r--r-- 382k ben 8 Jun 21:00 screenshot.png +//! drwxr-xr-x - ben 29 Jun 14:50 src +//! drwxr-xr-x - ben 28 Jun 19:53 target +//! +//! The table is constructed by creating a `Table` value, which produces a `Row` +//! value for each file. These rows can contain a vector of `Cell`s, or they can +//! contain depth information for the tree view, or both. These are described +//! below. +//! +//! +//! ## Constructing Detail Views +//! +//! When using the `--long` command-line argument, the details of each file are +//! displayed next to its name. +//! +//! The table holds a vector of all the column types. For each file and column, a +//! `Cell` value containing the ANSI-coloured text and Unicode width of each cell +//! is generated, with the row and column determined by indexing into both arrays. +//! +//! The column types vector does not actually include the filename. This is +//! because the filename is always the rightmost field, and as such, it does not +//! need to have its width queried or be padded with spaces. +//! +//! To illustrate the above: +//! +//! ┌─────────────────────────────────────────────────────────────────────────┐ +//! │ columns: [ Permissions, Size, User, Date(Modified) ] │ +//! ├─────────────────────────────────────────────────────────────────────────┤ +//! │ rows: cells: filename: │ +//! │ row 1: [ ".rw-r--r--", "9.6k", "ben", "29 Jun 16:16" ] Cargo.lock │ +//! │ row 2: [ ".rw-r--r--", "547", "ben", "23 Jun 10:54" ] Cargo.toml │ +//! │ row 3: [ "drwxr-xr-x", "-", "ben", "29 Jun 14:50" ] src │ +//! │ row 4: [ "drwxr-xr-x", "-", "ben", "28 Jun 19:53" ] target │ +//! └─────────────────────────────────────────────────────────────────────────┘ +//! +//! Each column in the table needs to be resized to fit its widest argument. This +//! means that we must wait until every row has been added to the table before it +//! can be displayed, in order to make sure that every column is wide enough. +//! +//! +//! ## Constructing Tree Views +//! +//! When using the `--tree` argument, instead of a vector of cells, each row has a +//! `depth` field that indicates how far deep in the tree it is: the top level has +//! depth 0, its children have depth 1, and *their* children have depth 2, and so +//! on. +//! +//! On top of this, it also has a `last` field that specifies whether this is the +//! last row of this particular consecutive set of rows. This doesn't affect the +//! file's information; it's just used to display a different set of Unicode tree +//! characters! The resulting table looks like this: +//! +//! ┌───────┬───────┬───────────────────────┐ +//! │ Depth │ Last │ Output │ +//! ├───────┼───────┼───────────────────────┤ +//! │ 0 │ │ documents │ +//! │ 1 │ false │ ├── this_file.txt │ +//! │ 1 │ false │ ├── that_file.txt │ +//! │ 1 │ false │ ├── features │ +//! │ 2 │ false │ │ ├── feature_1.rs │ +//! │ 2 │ false │ │ ├── feature_2.rs │ +//! │ 2 │ true │ │ └── feature_3.rs │ +//! │ 1 │ true │ └── pictures │ +//! │ 2 │ false │ ├── garden.jpg │ +//! │ 2 │ false │ ├── flowers.jpg │ +//! │ 2 │ false │ ├── library.png │ +//! │ 2 │ true │ └── space.tiff │ +//! └───────┴───────┴───────────────────────┘ +//! +//! Creating the table like this means that each file has to be tested to see if +//! it's the last one in the group. This is usually done by putting all the files +//! in a vector beforehand, getting its length, then comparing the index of each +//! file to see if it's the last one. (As some files may not be successfully +//! `stat`ted, we don't know how many files are going to exist in each directory) +//! +//! These rows have a `None` value for their vector of cells, instead of a `Some` +//! vector containing any. It's possible to have *both* a vector of cells and +//! depth and last flags when the user specifies `--tree` *and* `--long`. +//! +//! +//! ## Extended Attributes and Errors +//! +//! Finally, files' extended attributes and any errors that occur while statting +//! them can also be displayed as their children. It looks like this: +//! +//! .rw-r--r-- 0 ben 3 Sep 13:26 forbidden +//! └── <Permission denied (os error 13)> +//! .rw-r--r--@ 0 ben 3 Sep 13:26 file_with_xattrs +//! ├── another_greeting (len 2) +//! └── greeting (len 5) +//! +//! These lines also have `None` cells, and the error string or attribute details +//! are used in place of the filename. + + use std::error::Error; use std::io; use std::path::PathBuf; @@ -49,7 +162,10 @@ pub struct Details { /// Whether to recurse through directories with a tree view, and if so, /// which options to use. This field is only relevant here if the `tree` /// field of the RecurseOptions is `true`. - pub recurse: Option<(RecurseOptions, FileFilter)>, + pub recurse: Option<RecurseOptions>, + + /// How to sort and filter the files after getting their details. + pub filter: FileFilter, /// Whether to show a header line or not. pub header: bool, @@ -63,15 +179,19 @@ pub struct Details { } impl Details { - pub fn view(&self, dir: Option<&Dir>, files: &[File]) { + + /// Print the details of the given vector of files -- all of which will + /// have been read from the given directory, if present -- to stdout. + pub fn view(&self, dir: Option<&Dir>, files: Vec<File>) { + // First, transform the Columns object into a vector of columns for // the current directory. - let columns_for_dir = match self.columns { Some(cols) => cols.for_dir(dir), None => Vec::new(), }; + // Next, add a header if the user requests it. let mut table = Table::with_options(self.colours, columns_for_dir); if self.header { table.add_header() } @@ -82,78 +202,126 @@ impl Details { } } - /// Adds files to the table - recursively, if the `recurse` option - /// is present. - fn add_files_to_table<U: Users>(&self, table: &mut Table<U>, src: &[File], depth: usize) { - for (index, file) in src.iter().enumerate() { - let mut xattrs = Vec::new(); - let mut errors = Vec::new(); - - let has_xattrs = match fil |