Merge branch 'tree'

This commit is contained in:
Ben S 2015-02-04 01:05:31 +00:00
commit d5aa3208b6
6 changed files with 144 additions and 76 deletions

View File

@ -5,7 +5,6 @@ use ansi_term::Style;
#[derive(PartialEq, Debug, Copy)] #[derive(PartialEq, Debug, Copy)]
pub enum Column { pub enum Column {
Permissions, Permissions,
FileName,
FileSize(SizeFormat), FileSize(SizeFormat),
Blocks, Blocks,
User, User,
@ -49,7 +48,6 @@ impl Column {
pub fn header(&self) -> &'static str { pub fn header(&self) -> &'static str {
match *self { match *self {
Column::Permissions => "Permissions", Column::Permissions => "Permissions",
Column::FileName => "Name",
Column::FileSize(_) => "Size", Column::FileSize(_) => "Size",
Column::Blocks => "Blocks", Column::Blocks => "Blocks",
Column::User => "User", Column::User => "User",

View File

@ -31,11 +31,14 @@ impl Dir {
/// Produce a vector of File objects from an initialised directory, /// Produce a vector of File objects from an initialised directory,
/// printing out an error if any of the Files fail to be created. /// printing out an error if any of the Files fail to be created.
pub fn files(&self) -> Vec<File> { ///
/// Passing in `recurse` means that any directories will be scanned for
/// their contents, as well.
pub fn files(&self, recurse: bool) -> Vec<File> {
let mut files = vec![]; let mut files = vec![];
for path in self.contents.iter() { 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), Ok(file) => files.push(file),
Err(e) => println!("{}: {}", path.display(), e), Err(e) => println!("{}: {}", path.display(), e),
} }

View File

@ -32,6 +32,7 @@ pub struct File<'a> {
pub ext: Option<String>, pub ext: Option<String>,
pub path: Path, pub path: Path,
pub stat: io::FileStat, pub stat: io::FileStat,
pub this: Option<Dir>,
} }
impl<'a> File<'a> { impl<'a> File<'a> {
@ -39,12 +40,12 @@ impl<'a> File<'a> {
/// appropriate. Paths specified directly on the command-line have no Dirs. /// appropriate. Paths specified directly on the command-line have no Dirs.
/// ///
/// This uses lstat instead of stat, which doesn't follow symbolic links. /// This uses lstat instead of stat, which doesn't follow symbolic links.
pub fn from_path(path: &Path, parent: Option<&'a Dir>) -> IoResult<File<'a>> { pub fn from_path(path: &Path, parent: Option<&'a Dir>, recurse: bool) -> IoResult<File<'a>> {
fs::lstat(path).map(|stat| File::with_stat(stat, path, parent)) 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. /// 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 filename to display is the last component of the path. However,
// the path has no components for `.`, `..`, and `/`, so in these // the path has no components for `.`, `..`, and `/`, so in these
@ -58,12 +59,23 @@ impl<'a> File<'a> {
// replacement characters. // replacement characters.
let filename = String::from_utf8_lossy(bytes); 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 { File {
path: path.clone(), path: path.clone(),
dir: parent, dir: parent,
stat: stat, stat: stat,
name: filename.to_string(), name: filename.to_string(),
ext: ext(filename.as_slice()), ext: ext(filename.as_slice()),
this: this,
} }
} }
@ -82,7 +94,6 @@ impl<'a> File<'a> {
pub fn display<U: Users>(&self, column: &Column, users_cache: &mut U) -> Cell { pub fn display<U: Users>(&self, column: &Column, users_cache: &mut U) -> Cell {
match *column { match *column {
Permissions => self.permissions_string(), Permissions => self.permissions_string(),
FileName => self.file_name_view(),
FileSize(f) => self.file_size(f), FileSize(f) => self.file_size(f),
HardLinks => self.hard_links(), HardLinks => self.hard_links(),
Inode => self.inode(), Inode => self.inode(),
@ -98,15 +109,12 @@ impl<'a> File<'a> {
/// ///
/// It consists of the file name coloured in the appropriate style, /// It consists of the file name coloured in the appropriate style,
/// with special formatting for a symlink. /// 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 { if self.stat.kind == io::FileType::Symlink {
self.symlink_file_name_view() self.symlink_file_name_view()
} }
else { else {
Cell { self.file_colour().paint(&*self.name).to_string()
length: 0, // This length is ignored (rightmost column)
text: 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 /// an error, highlight the target and arrow in red. The error would
/// be shown out of context, and it's almost always because the /// be shown out of context, and it's almost always because the
/// target doesn't exist. /// target doesn't exist.
fn symlink_file_name_view(&self) -> Cell { fn symlink_file_name_view(&self) -> String {
let name = &*self.name; let name = &*self.name;
let style = self.file_colour(); let style = self.file_colour();
@ -129,26 +137,20 @@ impl<'a> File<'a> {
}; };
match self.target_file(&target_path) { match self.target_file(&target_path) {
Ok(file) => Cell { Ok(file) => format!("{} {} {}{}{}",
length: 0, // These lengths are never actually used... style.paint(name),
text: format!("{} {} {}{}{}", GREY.paint("=>"),
style.paint(name), Cyan.paint(target_path.dirname_str().unwrap()),
GREY.paint("=>"), Cyan.paint("/"),
Cyan.paint(target_path.dirname_str().unwrap()), file.file_colour().paint(file.name.as_slice())),
Cyan.paint("/"), Err(filename) => format!("{} {} {}",
file.file_colour().paint(file.name.as_slice())), style.paint(name),
}, Red.paint("=>"),
Err(filename) => Cell { Red.underline().paint(filename.as_slice())),
length: 0, // ...because the rightmost column lengths are ignored!
text: format!("{} {} {}",
style.paint(name),
Red.paint("=>"),
Red.underline().paint(filename.as_slice())),
},
} }
} }
else { else {
Cell::paint(style, name) style.paint(name).to_string()
} }
} }
@ -184,6 +186,7 @@ impl<'a> File<'a> {
stat: stat, stat: stat,
name: filename.to_string(), name: filename.to_string(),
ext: ext(filename.as_slice()), ext: ext(filename.as_slice()),
this: None,
}) })
} }
else { else {

View File

@ -39,11 +39,14 @@ fn exa(options: &Options) {
let path = Path::new(file); let path = Path::new(file);
match fs::stat(&path) { match fs::stat(&path) {
Ok(stat) => { 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); dirs.push(path);
} }
else { else {
files.push(File::with_stat(stat, &path, None)); files.push(File::with_stat(stat, &path, None, false));
} }
} }
Err(e) => println!("{}: {}", file, e), Err(e) => println!("{}: {}", file, e),
@ -55,7 +58,7 @@ fn exa(options: &Options) {
let mut first = files.is_empty(); let mut first = files.is_empty();
if !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, // 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) { match Dir::readdir(&dir_path) {
Ok(ref dir) => { Ok(ref dir) => {
let unsorted_files = dir.files(); let files = options.transform_files(dir.files(false));
let files: Vec<File> = options.transform_files(unsorted_files);
// When recursing, add any directories to the dirs stack // When recursing, add any directories to the dirs stack
// backwards: the *last* element of the stack is used each // backwards: the *last* element of the stack is used each
@ -95,7 +97,7 @@ fn exa(options: &Options) {
} }
count += 1; count += 1;
options.view(Some(dir), &files[]); options.view(Some(dir), &files[], options.filter);
} }
Err(e) => { Err(e) => {
println!("{}: {}", dir_path.display(), e); println!("{}: {}", dir_path.display(), e);

View File

@ -20,10 +20,15 @@ use self::Misfire::*;
pub struct Options { pub struct Options {
pub dir_action: DirAction, pub dir_action: DirAction,
pub path_strs: Vec<String>, pub path_strs: Vec<String>,
pub filter: FileFilter,
view: View,
}
#[derive(PartialEq, Debug, Copy)]
pub struct FileFilter {
reverse: bool, reverse: bool,
show_invisibles: bool, show_invisibles: bool,
sort_field: SortField, sort_field: SortField,
view: View,
} }
impl Options { impl Options {
@ -45,6 +50,7 @@ impl Options {
getopts::optflag("R", "recurse", "recurse into directories"), getopts::optflag("R", "recurse", "recurse into directories"),
getopts::optopt ("s", "sort", "field to sort by", "WORD"), getopts::optopt ("s", "sort", "field to sort by", "WORD"),
getopts::optflag("S", "blocks", "show number of file system blocks"), 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("x", "across", "sort multi-column view entries across"),
getopts::optflag("?", "help", "show list of command-line options"), getopts::optflag("?", "help", "show list of command-line options"),
]; ];
@ -64,20 +70,28 @@ impl Options {
}; };
Ok(Options { Ok(Options {
dir_action: try!(dir_action(&matches)), dir_action: try!(dir_action(&matches)),
path_strs: if matches.free.is_empty() { vec![ ".".to_string() ] } else { matches.free.clone() }, path_strs: if matches.free.is_empty() { vec![ ".".to_string() ] } else { matches.free.clone() },
reverse: matches.opt_present("reverse"), view: try!(view(&matches)),
show_invisibles: matches.opt_present("all"), filter: FileFilter {
sort_field: sort_field, reverse: matches.opt_present("reverse"),
view: try!(view(&matches)), show_invisibles: matches.opt_present("all"),
sort_field: sort_field,
},
}) })
} }
/// Display the files using this Option's View. pub fn transform_files<'a>(&self, files: Vec<File<'a>>) -> Vec<File<'a>> {
pub fn view(&self, dir: Option<&Dir>, files: &[File]) { self.filter.transform_files(files)
self.view.view(dir, files)
} }
/// Display the files using this Option's View.
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. /// Transform the files (sorting, reversing, filtering) before listing them.
pub fn transform_files<'a>(&self, mut files: Vec<File<'a>>) -> Vec<File<'a>> { pub fn transform_files<'a>(&self, mut files: Vec<File<'a>>) -> Vec<File<'a>> {
@ -111,7 +125,7 @@ impl Options {
/// What to do when encountering a directory? /// What to do when encountering a directory?
#[derive(PartialEq, Debug, Copy)] #[derive(PartialEq, Debug, Copy)]
pub enum DirAction { pub enum DirAction {
AsFile, List, Recurse AsFile, List, Recurse, Tree
} }
/// User-supplied field to sort by. /// User-supplied field to sort by.
@ -189,7 +203,7 @@ fn view(matches: &getopts::Matches) -> Result<View, Misfire> {
Err(Misfire::Useless("oneline", true, "long")) Err(Misfire::Useless("oneline", true, "long"))
} }
else { 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") { else if matches.opt_present("binary") {
@ -242,12 +256,14 @@ fn file_size(matches: &getopts::Matches) -> Result<SizeFormat, Misfire> {
fn dir_action(matches: &getopts::Matches) -> Result<DirAction, Misfire> { fn dir_action(matches: &getopts::Matches) -> Result<DirAction, Misfire> {
let recurse = matches.opt_present("recurse"); let recurse = matches.opt_present("recurse");
let list = matches.opt_present("list-dirs"); let list = matches.opt_present("list-dirs");
let tree = matches.opt_present("tree");
match (recurse, list) { match (recurse, list, tree) {
(true, true ) => Err(Misfire::Conflict("recurse", "list-dirs")), (true, true, _ ) => Err(Misfire::Conflict("recurse", "list-dirs")),
(true, false) => Ok(DirAction::Recurse), (true, false, false) => Ok(DirAction::Recurse),
(false, true ) => Ok(DirAction::AsFile), (true, false, true ) => Ok(DirAction::Tree),
(false, false) => Ok(DirAction::List), (false, true, _ ) => Ok(DirAction::AsFile),
(false, false, _ ) => Ok(DirAction::List),
} }
} }
@ -304,7 +320,6 @@ impl Columns {
} }
} }
columns.push(FileName);
columns columns
} }
} }

View File

@ -4,24 +4,24 @@ use std::iter::{AdditiveIterator, repeat};
use column::{Column, Cell}; use column::{Column, Cell};
use column::Alignment::Left; use column::Alignment::Left;
use dir::Dir; use dir::Dir;
use file::File; use file::{File, GREY};
use options::Columns; use options::{Columns, FileFilter};
use users::OSUsers; use users::OSUsers;
use ansi_term::Style::Plain; use ansi_term::Style::Plain;
#[derive(PartialEq, Copy, Debug)] #[derive(PartialEq, Copy, Debug)]
pub enum View { pub enum View {
Details(Columns, bool), Details(Columns, bool, bool),
Lines, Lines,
Grid(bool, usize), Grid(bool, usize),
} }
impl View { impl View {
pub fn view(&self, dir: Option<&Dir>, files: &[File]) { pub fn view(&self, dir: Option<&Dir>, files: &[File], filter: FileFilter) {
match *self { match *self {
View::Grid(across, width) => grid_view(across, width, files), 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), View::Lines => lines_view(files),
} }
} }
@ -30,7 +30,7 @@ impl View {
/// The lines view literally just displays each file, line-by-line. /// The lines view literally just displays each file, line-by-line.
fn lines_view(files: &[File]) { fn lines_view(files: &[File]) {
for file in files.iter() { 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 // The output gets formatted into columns, which looks nicer. To
// do this, we have to write the results into a table, instead of // do this, we have to write the results into a table, instead of
// displaying each file immediately, then calculating the maximum // 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 cache = OSUsers::empty_cache();
let mut table: Vec<Vec<Cell>> = files.iter() let mut table = Vec::new();
.map(|f| columns.iter().map(|c| f.display(c, &mut cache)).collect()) get_files(columns, &mut cache, tree, &mut table, files, 0, filter);
.collect();
if header { 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<usize> = range(0, columns.len()) let column_widths: Vec<usize> = 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(); .collect();
let mut stack = Vec::new();
for row in table.iter() { for row in table.iter() {
for (num, column) in columns.iter().enumerate() { for (num, column) in columns.iter().enumerate() {
if num != 0 { let padding = column_widths[num] - row.cells[num].length;
print!(" "); // Separator 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 { if row.children {
// The final column doesn't need to have trailing spaces stack[row.depth ] = if row.last { " " } else { "" };
print!("{}", row[num].text);
} }
else {
let padding = column_widths[num] - row[num].length; if row.depth != 0 {
print!("{}", column.alignment().pad_string(&row[num].text, padding)); print!(" ");
} }
} }
print!("\n");
print!("{}\n", row.name);
} }
} }
fn get_files(columns: &[Column], cache: &mut OSUsers, recurse: bool, dest: &mut Vec<Row>, 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);
}
}
}
}
struct Row {
pub depth: usize,
pub cells: Vec<Cell>,
pub name: String,
pub last: bool,
pub children: bool,
}