diff --git a/Cargo.lock b/Cargo.lock index 9bbcfeca..c883b53c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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)", ] @@ -157,14 +154,6 @@ dependencies = [ "num 0.1.27 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[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" @@ -172,7 +161,7 @@ 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)", ] @@ -278,12 +267,33 @@ name = "rustc-serialize" 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]] @@ -294,17 +304,12 @@ dependencies = [ "unicode-width 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[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]] diff --git a/Cargo.toml b/Cargo.toml index 71dbac9c..64df4203 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/dir.rs b/src/dir.rs index 414ebd34..78e0ed6f 100644 --- a/src/dir.rs +++ b/src/dir.rs @@ -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, - 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, } @@ -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 { + pub fn read_dir(path: &Path, git: bool) -> io::Result { 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 be8f9606..4e2617d2 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::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::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 e64f3fb3..7e540dec 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, - files: Vec>, } -#[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, 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) { 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 = 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 d88eee5c..dd762182 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) { - self.filter.transform_files(files) + pub fn sort_files(&self, files: &mut Vec) { + self.filter.sort_files(files) + } + + pub fn filter_files(&self, files: &mut Vec) { + 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) { - + pub fn filter_files(&self, files: &mut Vec) { 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) { + 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 3052448d..f87d0ada 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 +//! └── +//! .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, + + /// 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) { + // 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(&self, table: &mut Table, 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 file.path.attributes() { - Ok(xs) => { - let r = !xs.is_empty(); - if self.xattr { - for xattr in xs { - xattrs.push(xattr); - } - } - r - }, - Err(e) => { - if self.xattr { - errors.push((e, None)); - } - true - }, - }; + /// Adds files to the table, possibly recursively. This is easily + /// parallelisable, and uses a pool of threads. + fn add_files_to_table<'dir, U: Users+Send>(&self, mut table: &mut Table, src: Vec>, depth: usize) { + use num_cpus; + use scoped_threadpool::Pool; + use std::sync::{Arc, Mutex}; + + let mut pool = Pool::new(num_cpus::get() as u32); + let mut file_eggs = Vec::new(); + + struct Egg<'_> { + cells: Vec, + name: Cell, + xattrs: Vec, + errors: Vec<(io::Error, Option)>, + dir: Option, + file: Arc>, + } - table.add_file(file, depth, index == src.len() - 1, true, has_xattrs); - - // There are two types of recursion that exa supports: a tree - // view, which is dealt with here, and multiple listings, which is - // dealt with in the main module. So only actually recurse if we - // are in tree mode - the other case will be dealt with elsewhere. - if let Some((r, filter)) = self.recurse { - if file.is_directory() && r.tree && !r.is_too_deep(depth) { - - // Use the filter to remove unwanted files *before* expanding - // them, so we don't examine any directories that wouldn't - // have their contents listed anyway. - match file.to_dir() { - Ok(ref dir) => { - let mut files = Vec::new(); - - for file_to_add in dir.files() { - match file_to_add { - Ok(f) => files.push(f), - Err((path, e)) => errors.push((e, Some(path))) - } - } + pool.scoped(|scoped| { + let file_eggs = Arc::new(Mutex::new(&mut file_eggs)); + let table = Arc::new(Mutex::new(&mut table)); - filter.transform_files(&mut files); + for file in src.into_iter() { + let file: Arc = Arc::new(file); + let file_eggs = file_eggs.clone(); + let table = table.clone(); - if !files.is_empty() { - for xattr in xattrs { - table.add_xattr(xattr, depth + 1, false); - } + scoped.execute(move || { + let mut errors = Vec::new(); - for (error, path) in errors { - table.add_error(&error, depth + 1, false, path); + let mut xattrs = Vec::new(); + match file.path.attributes() { + Ok(xs) => { + if self.xattr { + for xattr in xs { + xattrs.push(xattr); } - - self.add_files_to_table(table, &files, depth + 1); - continue; } }, Err(e) => { - errors.push((e, None)); + if self.xattr { + errors.push((e, None)); + } }, + }; + + let cells = table.lock().unwrap().cells_for_file(&file, !xattrs.is_empty()); + + let name = Cell { + text: filename(&file, &self.colours, true), + length: file.file_name_width() + }; + + let mut dir = None; + + if let Some(r) = self.recurse { + if file.is_directory() && r.tree && !r.is_too_deep(depth) { + if let Ok(d) = file.to_dir(false) { + dir = Some(d); + } + } + }; + + let egg = Egg { + cells: cells, + name: name, + xattrs: xattrs, + errors: errors, + dir: dir, + file: file, + }; + + file_eggs.lock().unwrap().push(egg); + }); + } + }); + + file_eggs.sort_by(|a, b| self.filter.compare_files(&*a.file, &*b.file)); + + let num_eggs = file_eggs.len(); + for (index, egg) in file_eggs.into_iter().enumerate() { + let mut files = Vec::new(); + let mut errors = egg.errors; + + let row = Row { + depth: depth, + cells: Some(egg.cells), + name: egg.name, + last: index == num_eggs - 1, + }; + + table.rows.push(row); + + if let Some(ref dir) = egg.dir { + for file_to_add in dir.files() { + match file_to_add { + Ok(f) => files.push(f), + Err((path, e)) => errors.push((e, Some(path))) } } + + self.filter.filter_files(&mut files); + + if !files.is_empty() { + for xattr in egg.xattrs { + table.add_xattr(xattr, depth + 1, false); + } + + for (error, path) in errors { + table.add_error(&error, depth + 1, false, path); + } + + self.add_files_to_table(table, files, depth + 1); + continue; + } } - let count = xattrs.len(); - for (index, xattr) in xattrs.into_iter().enumerate() { + let count = egg.xattrs.len(); + for (index, xattr) in egg.xattrs.into_iter().enumerate() { table.add_xattr(xattr, depth + 1, errors.is_empty() && index == count - 1); } @@ -161,7 +329,6 @@ impl Details { for (index, (error, path)) in errors.into_iter().enumerate() { table.add_error(&error, depth + 1, index == count - 1, path); } - } } } @@ -171,10 +338,10 @@ struct Row { /// Vector of cells to display. /// - /// Most of the rows will be files that have had their metadata - /// successfully queried and displayed in these cells, so this will almost - /// always be `Some`. It will be `None` for a row that's only displaying - /// an attribute or an error. + /// Most of the rows will be used to display files' metadata, so this will + /// almost always be `Some`, containing a vector of cells. It will only be + /// `None` for a row displaying an attribute or error, neither of which + /// have cells. cells: Option>, // Did You Know? @@ -195,7 +362,8 @@ struct Row { impl Row { - /// Gets the 'width' of the indexed column, if present. If not, returns 0. + /// Gets the Unicode display width of the indexed column, if present. If + /// not, returns 0. fn column_width(&self, index: usize) -> usize { match self.cells { Some(ref cells) => cells[index].length, @@ -289,19 +457,13 @@ impl Table where U: Users { let row = Row { depth: depth, cells: None, - name: Cell::paint(self.colours.perms.attribute, &format!("{}\t{}", xattr.name, xattr.size)), + name: Cell::paint(self.colours.perms.attribute, &format!("{} (len {})", xattr.name, xattr.size)), last: last, }; self.rows.push(row); } - /// Get the cells for the given file, and add the result to the table. - fn add_file(&mut self, file: &File, depth: usize, last: bool, links: bool, xattrs: bool) { - let cells = self.cells_for_file(file, xattrs); - self.add_file_with_cells(cells, file, depth, last, links) - } - pub fn add_file_with_cells(&mut self, cells: Vec, file: &File, depth: usize, last: bool, links: bool) { let row = Row { depth: depth,