diff --git a/src/column.rs b/src/column.rs index 210e69ebc7d52c969fda68e0654d81fedd01a006..74d449d8b7433c217454e101aa76203d8c0759da 100644 --- a/src/column.rs +++ b/src/column.rs @@ -5,7 +5,6 @@ use ansi_term::Style; #[derive(PartialEq, Debug, Copy)] pub enum Column { Permissions, - FileName, FileSize(SizeFormat), Blocks, User, @@ -49,7 +48,6 @@ impl Column { pub fn header(&self) -> &'static str { match *self { Column::Permissions => "Permissions", - Column::FileName => "Name", Column::FileSize(_) => "Size", Column::Blocks => "Blocks", Column::User => "User", diff --git a/src/dir.rs b/src/dir.rs index 8a567fd6e0a5ae87847b23a15f83f9fc26e00d53..e0a2f25eb844c9bdddad043c260f9c3ab8a90f00 100644 --- a/src/dir.rs +++ b/src/dir.rs @@ -31,11 +31,14 @@ impl Dir { /// Produce a vector of File objects from an initialised directory, /// printing out an error if any of the Files fail to be created. - pub fn files(&self) -> Vec { + /// + /// Passing in `recurse` means that any directories will be scanned for + /// their contents, as well. + pub fn files(&self, recurse: bool) -> Vec { let mut files = vec![]; for path in self.contents.iter() { - match File::from_path(path, Some(self)) { + match File::from_path(path, Some(self), recurse) { Ok(file) => files.push(file), Err(e) => println!("{}: {}", path.display(), e), } diff --git a/src/file.rs b/src/file.rs index 63d50ece01ac7451eb6a780af481324047560fad..6d259c0fdcb1df4289eddef8ae0e40c283f1643c 100644 --- a/src/file.rs +++ b/src/file.rs @@ -32,6 +32,7 @@ pub struct File<'a> { pub ext: Option, pub path: Path, pub stat: io::FileStat, + pub this: Option, } impl<'a> File<'a> { @@ -39,12 +40,12 @@ impl<'a> File<'a> { /// appropriate. Paths specified directly on the command-line have no Dirs. /// /// This uses lstat instead of stat, which doesn't follow symbolic links. - pub fn from_path(path: &Path, parent: Option<&'a Dir>) -> IoResult> { - fs::lstat(path).map(|stat| File::with_stat(stat, path, parent)) + pub fn from_path(path: &Path, parent: Option<&'a Dir>, recurse: bool) -> IoResult> { + fs::lstat(path).map(|stat| File::with_stat(stat, path, parent, recurse)) } /// Create a new File object from the given Stat result, and other data. - pub fn with_stat(stat: io::FileStat, path: &Path, parent: Option<&'a Dir>) -> File<'a> { + pub fn with_stat(stat: io::FileStat, path: &Path, parent: Option<&'a Dir>, recurse: bool) -> File<'a> { // The filename to display is the last component of the path. However, // the path has no components for `.`, `..`, and `/`, so in these @@ -58,12 +59,23 @@ impl<'a> File<'a> { // replacement characters. let filename = String::from_utf8_lossy(bytes); + // If we are recursing, then the `this` field contains a Dir object + // that represents the current File as a directory, if it is a + // directory. This is used for the --tree option. + let this = if recurse && stat.kind == io::FileType::Directory { + Dir::readdir(path).ok() + } + else { + None + }; + File { path: path.clone(), dir: parent, stat: stat, name: filename.to_string(), ext: ext(filename.as_slice()), + this: this, } } @@ -82,7 +94,6 @@ impl<'a> File<'a> { pub fn display(&self, column: &Column, users_cache: &mut U) -> Cell { match *column { Permissions => self.permissions_string(), - FileName => self.file_name_view(), FileSize(f) => self.file_size(f), HardLinks => self.hard_links(), Inode => self.inode(), @@ -98,15 +109,12 @@ impl<'a> File<'a> { /// /// It consists of the file name coloured in the appropriate style, /// with special formatting for a symlink. - pub fn file_name_view(&self) -> Cell { + pub fn file_name_view(&self) -> String { if self.stat.kind == io::FileType::Symlink { self.symlink_file_name_view() } else { - Cell { - length: 0, // This length is ignored (rightmost column) - text: self.file_colour().paint(&*self.name).to_string(), - } + self.file_colour().paint(&*self.name).to_string() } } @@ -118,7 +126,7 @@ impl<'a> File<'a> { /// an error, highlight the target and arrow in red. The error would /// be shown out of context, and it's almost always because the /// target doesn't exist. - fn symlink_file_name_view(&self) -> Cell { + fn symlink_file_name_view(&self) -> String { let name = &*self.name; let style = self.file_colour(); @@ -129,26 +137,20 @@ impl<'a> File<'a> { }; match self.target_file(&target_path) { - Ok(file) => Cell { - length: 0, // These lengths are never actually used... - text: format!("{} {} {}{}{}", - style.paint(name), - GREY.paint("=>"), - Cyan.paint(target_path.dirname_str().unwrap()), - Cyan.paint("/"), - file.file_colour().paint(file.name.as_slice())), - }, - Err(filename) => Cell { - length: 0, // ...because the rightmost column lengths are ignored! - text: format!("{} {} {}", - style.paint(name), - Red.paint("=>"), - Red.underline().paint(filename.as_slice())), - }, + Ok(file) => format!("{} {} {}{}{}", + style.paint(name), + GREY.paint("=>"), + Cyan.paint(target_path.dirname_str().unwrap()), + Cyan.paint("/"), + file.file_colour().paint(file.name.as_slice())), + Err(filename) => format!("{} {} {}", + style.paint(name), + Red.paint("=>"), + Red.underline().paint(filename.as_slice())), } } else { - Cell::paint(style, name) + style.paint(name).to_string() } } @@ -184,6 +186,7 @@ impl<'a> File<'a> { stat: stat, name: filename.to_string(), ext: ext(filename.as_slice()), + this: None, }) } else { diff --git a/src/main.rs b/src/main.rs index c31c6cba90d2f71292bf98f193c9bae2070cdfe5..edd9c66dfc4df6fbb7a7374a32c7616e6441c992 100644 --- a/src/main.rs +++ b/src/main.rs @@ -39,11 +39,14 @@ fn exa(options: &Options) { let path = Path::new(file); match fs::stat(&path) { Ok(stat) => { - if stat.kind == FileType::Directory && options.dir_action != DirAction::AsFile { + if stat.kind == FileType::Directory && options.dir_action == DirAction::Tree { + files.push(File::with_stat(stat, &path, None, true)); + } + else if stat.kind == FileType::Directory && options.dir_action != DirAction::AsFile { dirs.push(path); } else { - files.push(File::with_stat(stat, &path, None)); + files.push(File::with_stat(stat, &path, None, false)); } } Err(e) => println!("{}: {}", file, e), @@ -55,7 +58,7 @@ fn exa(options: &Options) { let mut first = files.is_empty(); if !files.is_empty() { - options.view(None, &files[]); + options.view(None, &files[], options.filter); } // Directories are put on a stack rather than just being iterated through, @@ -77,8 +80,7 @@ fn exa(options: &Options) { match Dir::readdir(&dir_path) { Ok(ref dir) => { - let unsorted_files = dir.files(); - let files: Vec = options.transform_files(unsorted_files); + let files = options.transform_files(dir.files(false)); // When recursing, add any directories to the dirs stack // backwards: the *last* element of the stack is used each @@ -95,7 +97,7 @@ fn exa(options: &Options) { } count += 1; - options.view(Some(dir), &files[]); + options.view(Some(dir), &files[], options.filter); } Err(e) => { println!("{}: {}", dir_path.display(), e); diff --git a/src/options.rs b/src/options.rs index 8d1f35544b242288257d6850c3c2514c89bad9bb..b3d5f4c8ccccff1069436794edfc0328e437e3ec 100644 --- a/src/options.rs +++ b/src/options.rs @@ -20,10 +20,15 @@ use self::Misfire::*; pub struct Options { pub dir_action: DirAction, pub path_strs: Vec, + pub filter: FileFilter, + view: View, +} + +#[derive(PartialEq, Debug, Copy)] +pub struct FileFilter { reverse: bool, show_invisibles: bool, sort_field: SortField, - view: View, } impl Options { @@ -45,6 +50,7 @@ impl Options { getopts::optflag("R", "recurse", "recurse into directories"), getopts::optopt ("s", "sort", "field to sort by", "WORD"), getopts::optflag("S", "blocks", "show number of file system blocks"), + getopts::optflag("T", "tree", "recurse into subdirectories in a tree view"), getopts::optflag("x", "across", "sort multi-column view entries across"), getopts::optflag("?", "help", "show list of command-line options"), ]; @@ -64,20 +70,28 @@ impl Options { }; Ok(Options { - dir_action: try!(dir_action(&matches)), - path_strs: if matches.free.is_empty() { vec![ ".".to_string() ] } else { matches.free.clone() }, - reverse: matches.opt_present("reverse"), - show_invisibles: matches.opt_present("all"), - sort_field: sort_field, - view: try!(view(&matches)), + dir_action: try!(dir_action(&matches)), + path_strs: if matches.free.is_empty() { vec![ ".".to_string() ] } else { matches.free.clone() }, + view: try!(view(&matches)), + filter: FileFilter { + reverse: matches.opt_present("reverse"), + show_invisibles: matches.opt_present("all"), + sort_field: sort_field, + }, }) } + pub fn transform_files<'a>(&self, files: Vec>) -> Vec> { + self.filter.transform_files(files) + } + /// Display the files using this Option's View. - pub fn view(&self, dir: Option<&Dir>, files: &[File]) { - self.view.view(dir, files) + pub fn view(&self, dir: Option<&Dir>, files: &[File], filter: FileFilter) { + self.view.view(dir, files, filter) } +} +impl FileFilter { /// Transform the files (sorting, reversing, filtering) before listing them. pub fn transform_files<'a>(&self, mut files: Vec>) -> Vec> { @@ -111,7 +125,7 @@ impl Options { /// What to do when encountering a directory? #[derive(PartialEq, Debug, Copy)] pub enum DirAction { - AsFile, List, Recurse + AsFile, List, Recurse, Tree } /// User-supplied field to sort by. @@ -189,7 +203,7 @@ fn view(matches: &getopts::Matches) -> Result { Err(Misfire::Useless("oneline", true, "long")) } else { - Ok(View::Details(try!(Columns::new(matches)), matches.opt_present("header"))) + Ok(View::Details(try!(Columns::new(matches)), matches.opt_present("header"), matches.opt_present("tree"))) } } else if matches.opt_present("binary") { @@ -242,12 +256,14 @@ fn file_size(matches: &getopts::Matches) -> Result { fn dir_action(matches: &getopts::Matches) -> Result { let recurse = matches.opt_present("recurse"); let list = matches.opt_present("list-dirs"); + let tree = matches.opt_present("tree"); - match (recurse, list) { - (true, true ) => Err(Misfire::Conflict("recurse", "list-dirs")), - (true, false) => Ok(DirAction::Recurse), - (false, true ) => Ok(DirAction::AsFile), - (false, false) => Ok(DirAction::List), + match (recurse, list, tree) { + (true, true, _ ) => Err(Misfire::Conflict("recurse", "list-dirs")), + (true, false, false) => Ok(DirAction::Recurse), + (true, false, true ) => Ok(DirAction::Tree), + (false, true, _ ) => Ok(DirAction::AsFile), + (false, false, _ ) => Ok(DirAction::List), } } @@ -304,7 +320,6 @@ impl Columns { } } - columns.push(FileName); columns } } diff --git a/src/output.rs b/src/output.rs index 52251d4ce64cf3af115950208988c9a4d8d3f75c..dab2b54700249b7c1074710dd032ce7e6c16134c 100644 --- a/src/output.rs +++ b/src/output.rs @@ -4,24 +4,24 @@ use std::iter::{AdditiveIterator, repeat}; use column::{Column, Cell}; use column::Alignment::Left; use dir::Dir; -use file::File; -use options::Columns; +use file::{File, GREY}; +use options::{Columns, FileFilter}; use users::OSUsers; use ansi_term::Style::Plain; #[derive(PartialEq, Copy, Debug)] pub enum View { - Details(Columns, bool), + Details(Columns, bool, bool), Lines, Grid(bool, usize), } impl View { - pub fn view(&self, dir: Option<&Dir>, files: &[File]) { + pub fn view(&self, dir: Option<&Dir>, files: &[File], filter: FileFilter) { match *self { View::Grid(across, width) => grid_view(across, width, files), - View::Details(ref cols, header) => details_view(&*cols.for_dir(dir), files, header), + View::Details(ref cols, header, tree) => details_view(&*cols.for_dir(dir), files, header, tree, filter), View::Lines => lines_view(files), } } @@ -30,7 +30,7 @@ impl View { /// The lines view literally just displays each file, line-by-line. fn lines_view(files: &[File]) { for file in files.iter() { - println!("{}", file.file_name_view().text); + println!("{}", file.file_name_view()); } } @@ -122,7 +122,7 @@ fn grid_view(across: bool, console_width: usize, files: &[File]) { } } -fn details_view(columns: &[Column], files: &[File], header: bool) { +fn details_view(columns: &[Column], files: &[File], header: bool, tree: bool, filter: FileFilter) { // The output gets formatted into columns, which looks nicer. To // do this, we have to write the results into a table, instead of // displaying each file immediately, then calculating the maximum @@ -131,33 +131,80 @@ fn details_view(columns: &[Column], files: &[File], header: bool) { let mut cache = OSUsers::empty_cache(); - let mut table: Vec> = files.iter() - .map(|f| columns.iter().map(|c| f.display(c, &mut cache)).collect()) - .collect(); + let mut table = Vec::new(); + get_files(columns, &mut cache, tree, &mut table, files, 0, filter); if header { - table.insert(0, columns.iter().map(|c| Cell::paint(Plain.underline(), c.header())).collect()); + let row = Row { + depth: 0, + cells: columns.iter().map(|c| Cell::paint(Plain.underline(), c.header())).collect(), + name: Plain.underline().paint("Name").to_string(), + last: false, + children: false, + }; + + table.insert(0, row); } let column_widths: Vec = range(0, columns.len()) - .map(|n| table.iter().map(|row| row[n].length).max().unwrap_or(0)) + .map(|n| table.iter().map(|row| row.cells[n].length).max().unwrap_or(0)) .collect(); + let mut stack = Vec::new(); + for row in table.iter() { for (num, column) in columns.iter().enumerate() { - if num != 0 { - print!(" "); // Separator + let padding = column_widths[num] - row.cells[num].length; + print!("{} ", column.alignment().pad_string(&row.cells[num].text, padding)); + } + + if tree { + stack.resize(row.depth + 1, "├──"); + stack[row.depth ] = if row.last { "└──" } else { "├──" }; + + for i in range(1, row.depth + 1) { + print!("{}", GREY.paint(stack[i ])); } - if num == columns.len() - 1 { - // The final column doesn't need to have trailing spaces - print!("{}", row[num].text); + if row.children { + stack[row.depth ] = if row.last { " " } else { "│ " }; } - else { - let padding = column_widths[num] - row[num].length; - print!("{}", column.alignment().pad_string(&row[num].text, padding)); + + if row.depth != 0 { + print!(" "); + } + } + + print!("{}\n", row.name); + } +} + +fn get_files(columns: &[Column], cache: &mut OSUsers, recurse: bool, dest: &mut Vec, src: &[File], depth: usize, filter: FileFilter) { + for (index, file) in src.iter().enumerate() { + + let row = Row { + depth: depth, + cells: columns.iter().map(|c| file.display(c, cache)).collect(), + name: file.file_name_view(), + last: index == src.len() - 1, + children: file.this.is_some(), + }; + + dest.push(row); + + if recurse { + if let Some(ref dir) = file.this { + let files = filter.transform_files(dir.files(true)); + get_files(columns, cache, recurse, dest, files.as_slice(), depth + 1, filter); } } - print!("\n"); } } + +struct Row { + pub depth: usize, + pub cells: Vec, + pub name: String, + pub last: bool, + pub children: bool, +}