diff --git a/src/column.rs b/src/column.rs index 74d449d8b7433c217454e101aa76203d8c0759da..030570a7b95e877f3596151161885a931f358ee4 100644 --- a/src/column.rs +++ b/src/column.rs @@ -2,6 +2,8 @@ use std::iter::repeat; use ansi_term::Style; +use options::SizeFormat; + #[derive(PartialEq, Debug, Copy)] pub enum Column { Permissions, @@ -15,13 +17,6 @@ pub enum Column { GitStatus, } -#[derive(PartialEq, Debug, Copy)] -pub enum SizeFormat { - DecimalBytes, - BinaryBytes, - JustBytes, -} - /// Each column can pick its own **Alignment**. Usually, numbers are /// right-aligned, and text is left-aligned. #[derive(Copy)] diff --git a/src/file.rs b/src/file.rs index c84171998fd44bdf00e6e93903d04f8b2ed6ffdc..bc968c75e066facfbc81fe50aa79276b55af6d01 100644 --- a/src/file.rs +++ b/src/file.rs @@ -10,10 +10,11 @@ use users::Users; use number_prefix::{binary_prefix, decimal_prefix, Prefixed, Standalone, PrefixNames}; -use column::{Column, SizeFormat, Cell}; +use column::{Column, Cell}; use column::Column::*; use dir::Dir; use filetype::HasType; +use options::SizeFormat; /// This grey value is directly in between white and black, so it's guaranteed /// to show up on either backgrounded terminal. diff --git a/src/main.rs b/src/main.rs index 4fdf7c159793bf791628642c32cdd8dea570fab0..fe97eb641c5e3f0d2ba8c39ccf87c9df2e220ec9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,7 +14,8 @@ use std::os::{args, set_exit_status}; use dir::Dir; use file::File; -use options::{Options, DirAction}; +use options::{Options, View, DirAction}; +use output::lines_view; pub mod column; pub mod dir; @@ -24,87 +25,112 @@ pub mod options; pub mod output; pub mod term; -fn exa(options: &Options) { - let mut dirs: Vec = vec![]; - let mut files: Vec = vec![]; - - // It's only worth printing out directory names if the user supplied - // more than one of them. - let mut count = 0; - - // Separate the user-supplied paths into directories and files. - // Files are shown first, and then each directory is expanded - // and listed second. - for file in options.path_strs.iter() { - let path = Path::new(file); - match fs::stat(&path) { - Ok(stat) => { - 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, false)); - } - } - Err(e) => println!("{}: {}", file, e), - } +struct Exa<'a> { + count: usize, + options: Options, + dirs: Vec, + files: Vec>, +} - count += 1; +impl<'a> Exa<'a> { + fn new(options: Options) -> Exa<'a> { + Exa { + count: 0, + options: options, + dirs: Vec::new(), + files: Vec::new(), + } } - let mut first = files.is_empty(); + fn load(&mut self, iter: T) where T: Iterator { + // Separate the user-supplied paths into directories and files. + // Files are shown first, and then each directory is expanded + // and listed second. + for file in iter { + let path = Path::new(file); + match fs::stat(&path) { + Ok(stat) => { + if stat.kind == FileType::Directory { + if self.options.dir_action == DirAction::Tree { + self.files.push(File::with_stat(stat, &path, None, true)); + } + else { + self.dirs.push(path); + } + } + else { + self.files.push(File::with_stat(stat, &path, None, false)); + } + } + Err(e) => println!("{}: {}", file, e), + } - if !files.is_empty() { - options.view(None, &files[], options.filter); + self.count += 1; + } } - // 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 dirs.pop() { - None => break, - Some(f) => f, - }; - - // Put a gap between directories, or between the list of files and the - // first directory. - if first { - first = false; - } - else { - print!("\n"); + fn print_files(&self) { + if !self.files.is_empty() { + self.print(None, &self.files[]); } + } - match Dir::readdir(&dir_path) { - Ok(ref dir) => { - let mut files = dir.files(false); - 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 options.dir_action == DirAction::Recurse { - for dir in files.iter().filter(|f| f.stat.kind == FileType::Directory).rev() { - dirs.push(dir.path.clone()); + fn print_dirs(&mut self) { + let mut first = self.files.is_empty(); + + // 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, + }; + + // Put a gap between directories, or between the list of files and the + // first directory. + if first { + first = false; + } + else { + print!("\n"); + } + + match Dir::readdir(&dir_path) { + Ok(ref dir) => { + let mut files = dir.files(false); + 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 self.options.dir_action == DirAction::Recurse { + for dir in files.iter().filter(|f| f.stat.kind == FileType::Directory).rev() { + self.dirs.push(dir.path.clone()); + } } - } - if count > 1 { - println!("{}:", dir_path.display()); + if self.count > 1 { + println!("{}:", dir_path.display()); + } + self.count += 1; + + self.print(Some(dir), &files[]); + } + Err(e) => { + println!("{}: {}", dir_path.display(), e); + return; } - count += 1; + }; + } + } - options.view(Some(dir), &files[], options.filter); - } - Err(e) => { - println!("{}: {}", dir_path.display(), e); - return; - } - }; + fn print(&self, dir: Option<&Dir>, files: &[File]) { + match self.options.view { + View::Grid(g) => g.view(files), + View::Details(d) => d.view(dir, files), + View::Lines => lines_view(files), + } } } @@ -112,7 +138,12 @@ fn main() { let args: Vec = args(); match Options::getopts(args.tail()) { - Ok(options) => exa(&options), + Ok((options, paths)) => { + let mut exa = Exa::new(options); + exa.load(paths.iter()); + exa.print_files(); + exa.print_dirs(); + }, Err(e) => { println!("{}", e); set_exit_status(e.error_code()); diff --git a/src/options.rs b/src/options.rs index ba0ff59d541176aa0b5a14c9c1d7516eac1726df..d37af1e0412f04e7ce7ca76251fdfd044c60b58c 100644 --- a/src/options.rs +++ b/src/options.rs @@ -1,8 +1,8 @@ use dir::Dir; use file::File; -use column::{Column, SizeFormat}; +use column::Column; use column::Column::*; -use output::View; +use output::{Grid, Details}; use term::dimensions; use std::ascii::AsciiExt; @@ -16,12 +16,11 @@ use self::Misfire::*; /// The *Options* struct represents a parsed version of the user's /// command-line options. -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Copy)] pub struct Options { pub dir_action: DirAction, - pub path_strs: Vec, pub filter: FileFilter, - view: View, + pub view: View, } #[derive(PartialEq, Debug, Copy)] @@ -31,10 +30,17 @@ pub struct FileFilter { sort_field: SortField, } +#[derive(PartialEq, Copy, Debug)] +pub enum View { + Details(Details), + Lines, + Grid(Grid), +} + impl Options { /// Call getopts on the given slice of command-line strings. - pub fn getopts(args: &[String]) -> Result { + pub fn getopts(args: &[String]) -> Result<(Options, Vec), Misfire> { let mut opts = getopts::Options::new(); opts.optflag("1", "oneline", "display one entry per line"); opts.optflag("a", "all", "show dot-files"); @@ -68,26 +74,29 @@ impl Options { None => SortField::Name, }; - Ok(Options { - 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, - }, - }) + let filter = FileFilter { + reverse: matches.opt_present("reverse"), + show_invisibles: matches.opt_present("all"), + sort_field: sort_field, + }; + + let path_strs = if matches.free.is_empty() { + vec![ ".".to_string() ] + } + else { + matches.free.clone() + }; + + Ok((Options { + dir_action: try!(DirAction::deduce(&matches)), + view: try!(View::deduce(&matches, filter)), + filter: filter, + }, path_strs)) } pub fn transform_files<'a>(&self, files: &mut Vec>) { self.filter.transform_files(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 { @@ -119,12 +128,6 @@ impl FileFilter { } } -/// What to do when encountering a directory? -#[derive(PartialEq, Debug, Copy)] -pub enum DirAction { - AsFile, List, Recurse, Tree -} - /// User-supplied field to sort by. #[derive(PartialEq, Debug, Copy)] pub enum SortField { @@ -190,77 +193,111 @@ impl fmt::Display for Misfire { } } -/// Turns the Getopts results object into a View object. -fn view(matches: &getopts::Matches) -> Result { - if matches.opt_present("long") { - if matches.opt_present("across") { - Err(Misfire::Useless("across", true, "long")) +impl View { + pub fn deduce(matches: &getopts::Matches, filter: FileFilter) -> Result { + if matches.opt_present("long") { + if matches.opt_present("across") { + Err(Misfire::Useless("across", true, "long")) + } + else if matches.opt_present("oneline") { + Err(Misfire::Useless("oneline", true, "long")) + } + else { + let details = Details { + columns: try!(Columns::deduce(matches)), + header: matches.opt_present("tree"), + tree: matches.opt_present("recurse"), + filter: filter, + }; + + Ok(View::Details(details)) + } } - else if matches.opt_present("oneline") { - Err(Misfire::Useless("oneline", true, "long")) + else if matches.opt_present("binary") { + Err(Misfire::Useless("binary", false, "long")) } - else { - Ok(View::Details(try!(Columns::new(matches)), matches.opt_present("header"), matches.opt_present("tree"))) + else if matches.opt_present("bytes") { + Err(Misfire::Useless("bytes", false, "long")) } - } - else if matches.opt_present("binary") { - Err(Misfire::Useless("binary", false, "long")) - } - else if matches.opt_present("bytes") { - Err(Misfire::Useless("bytes", false, "long")) - } - else if matches.opt_present("inode") { - Err(Misfire::Useless("inode", false, "long")) - } - else if matches.opt_present("links") { - Err(Misfire::Useless("links", false, "long")) - } - else if matches.opt_present("header") { - Err(Misfire::Useless("header", false, "long")) - } - else if matches.opt_present("blocks") { - Err(Misfire::Useless("blocks", false, "long")) - } - else if matches.opt_present("oneline") { - if matches.opt_present("across") { - Err(Misfire::Useless("across", true, "oneline")) + else if matches.opt_present("inode") { + Err(Misfire::Useless("inode", false, "long")) } - else { - Ok(View::Lines) + else if matches.opt_present("links") { + Err(Misfire::Useless("links", false, "long")) } - } - else { - match dimensions() { - None => Ok(View::Lines), - Some((width, _)) => Ok(View::Grid(matches.opt_present("across"), width)), + else if matches.opt_present("header") { + Err(Misfire::Useless("header", false, "long")) + } + else if matches.opt_present("blocks") { + Err(Misfire::Useless("blocks", false, "long")) + } + else if matches.opt_present("oneline") { + if matches.opt_present("across") { + Err(Misfire::Useless("across", true, "oneline")) + } + else { + Ok(View::Lines) + } + } + else { + if let Some((width, _)) = dimensions() { + let grid = Grid { + across: matches.opt_present("across"), + console_width: width + }; + + Ok(View::Grid(grid)) + } + else { + // If the terminal width couldn't be matched for some reason, such + // as the program's stdout being connected to a file, then + // fallback to the lines view. + Ok(View::Lines) + } } } } -/// Finds out which file size the user has asked for. -fn file_size(matches: &getopts::Matches) -> Result { - let binary = matches.opt_present("binary"); - let bytes = matches.opt_present("bytes"); +#[derive(PartialEq, Debug, Copy)] +pub enum SizeFormat { + DecimalBytes, + BinaryBytes, + JustBytes, +} - match (binary, bytes) { - (true, true ) => Err(Misfire::Conflict("binary", "bytes")), - (true, false) => Ok(SizeFormat::BinaryBytes), - (false, true ) => Ok(SizeFormat::JustBytes), - (false, false) => Ok(SizeFormat::DecimalBytes), +impl SizeFormat { + pub fn deduce(matches: &getopts::Matches) -> Result { + let binary = matches.opt_present("binary"); + let bytes = matches.opt_present("bytes"); + + match (binary, bytes) { + (true, true ) => Err(Misfire::Conflict("binary", "bytes")), + (true, false) => Ok(SizeFormat::BinaryBytes), + (false, true ) => Ok(SizeFormat::JustBytes), + (false, false) => Ok(SizeFormat::DecimalBytes), + } } } -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, 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), +/// What to do when encountering a directory? +#[derive(PartialEq, Debug, Copy)] +pub enum DirAction { + AsFile, List, Recurse, Tree +} + +impl DirAction { + pub fn deduce(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, 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), + } } } @@ -274,9 +311,9 @@ pub struct Columns { } impl Columns { - pub fn new(matches: &getopts::Matches) -> Result { + pub fn deduce(matches: &getopts::Matches) -> Result { Ok(Columns { - size_format: try!(file_size(matches)), + size_format: try!(SizeFormat::deduce(matches)), inode: matches.opt_present("inode"), links: matches.opt_present("links"), blocks: matches.opt_present("blocks"), @@ -327,7 +364,7 @@ mod test { use super::Misfire; use super::Misfire::*; - fn is_helpful(misfire: Result) -> bool { + fn is_helpful(misfire: Result) -> bool { match misfire { Err(Help(_)) => true, _ => false, @@ -348,15 +385,13 @@ mod test { #[test] fn files() { - let opts = Options::getopts(&[ "this file".to_string(), "that file".to_string() ]).unwrap(); - let args: Vec = opts.path_strs; + let args = Options::getopts(&[ "this file".to_string(), "that file".to_string() ]).unwrap().1; assert_eq!(args, vec![ "this file".to_string(), "that file".to_string() ]) } #[test] fn no_args() { - let opts = Options::getopts(&[]).unwrap(); - let args: Vec = opts.path_strs; + let args = Options::getopts(&[]).unwrap().1; assert_eq!(args, vec![ ".".to_string() ]) } diff --git a/src/output.rs b/src/output.rs deleted file mode 100644 index 0defbd9da79b290e25df0ae5a02f61a70c9f3a34..0000000000000000000000000000000000000000 --- a/src/output.rs +++ /dev/null @@ -1,211 +0,0 @@ -use std::cmp::max; -use std::iter::{AdditiveIterator, repeat}; - -use column::{Column, Cell}; -use column::Alignment::Left; -use dir::Dir; -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, bool), - Lines, - Grid(bool, usize), -} - -impl View { - 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, tree) => details_view(&*cols.for_dir(dir), files, header, tree, filter), - View::Lines => lines_view(files), - } - } -} - -/// The lines view literally just displays each file, line-by-line. -fn lines_view(files: &[File]) { - for file in files { - println!("{}", file.file_name_view()); - } -} - -fn fit_into_grid(across: bool, console_width: usize, files: &[File]) -> Option<(usize, Vec)> { - // TODO: this function could almost certainly be optimised... - // surely not *all* of the numbers of lines are worth searching through! - - // Instead of numbers of columns, try to find the fewest number of *lines* - // that the output will fit in. - for num_lines in 1 .. files.len() { - - // The number of columns is the number of files divided by the number - // of lines, *rounded up*. - let mut num_columns = files.len() / num_lines; - if files.len() % num_lines != 0 { - num_columns += 1; - } - - // Early abort: if there are so many columns that the width of the - // *column separators* is bigger than the width of the screen, then - // don't even try to tabulate it. - // This is actually a necessary check, because the width is stored as - // a usize, and making it go negative makes it huge instead, but it - // also serves as a speed-up. - let separator_width = (num_columns - 1) * 2; - if console_width < separator_width { - continue; - } - - // Remove the separator width from the available space. - let adjusted_width = console_width - separator_width; - - // Find the width of each column by adding the lengths of the file - // names in that column up. - let mut column_widths: Vec = repeat(0).take(num_columns).collect(); - for (index, file) in files.iter().enumerate() { - let index = if across { - index % num_columns - } - else { - index / num_lines - }; - column_widths[index] = max(column_widths[index], file.name.len()); - } - - // If they all fit in the terminal, combined, then success! - if column_widths.iter().map(|&x| x).sum() < adjusted_width { - return Some((num_lines, column_widths)); - } - } - - // If you get here you have really long file names. - return None; -} - -fn grid_view(across: bool, console_width: usize, files: &[File]) { - if let Some((num_lines, widths)) = fit_into_grid(across, console_width, files) { - for y in 0 .. num_lines { - for x in 0 .. widths.len() { - let num = if across { - y * widths.len() + x - } - else { - y + num_lines * x - }; - - // Show whitespace in the place of trailing files - if num >= files.len() { - continue; - } - - let ref file = files[num]; - let styled_name = file.file_colour().paint(file.name.as_slice()).to_string(); - if x == widths.len() - 1 { - // The final column doesn't need to have trailing spaces - print!("{}", styled_name); - } - else { - assert!(widths[x] >= file.name.len()); - print!("{}", Left.pad_string(&styled_name, widths[x] - file.name.len() + 2)); - } - } - print!("\n"); - } - } - else { - // Drop down to lines view if the file names are too big for a grid - lines_view(files); - } -} - -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 - // width of each column based on the length of the results and - // padding the fields during output. - - let mut cache = OSUsers::empty_cache(); - - let mut table = Vec::new(); - get_files(columns, &mut cache, tree, &mut table, files, 0, filter); - - if header { - 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.cells[n].length).max().unwrap_or(0)) - .collect(); - - let mut stack = Vec::new(); - - for row in table { - for (num, column) in columns.iter().enumerate() { - 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 1 .. row.depth + 1 { - print!("{}", GREY.paint(stack[i ])); - } - - if row.children { - stack[row.depth ] = if row.last { " " } else { "│ " }; - } - - 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 mut files = dir.files(true); - filter.transform_files(&mut files); - get_files(columns, cache, recurse, dest, files.as_slice(), depth + 1, filter); - } - } - } -} - -struct Row { - pub depth: usize, - pub cells: Vec, - pub name: String, - pub last: bool, - pub children: bool, -} diff --git a/src/output/details.rs b/src/output/details.rs new file mode 100644 index 0000000000000000000000000000000000000000..422a7cab5e4ef43d14a9f354daca5081a44dbfd9 --- /dev/null +++ b/src/output/details.rs @@ -0,0 +1,106 @@ +use column::{Column, Cell}; +use dir::Dir; +use file::{File, GREY}; +use options::{Columns, FileFilter}; +use users::OSUsers; + +use ansi_term::Style::Plain; + +#[derive(PartialEq, Debug, Copy)] +pub struct Details { + pub columns: Columns, + pub header: bool, + pub tree: bool, + pub filter: FileFilter, +} + +impl Details { + + pub fn view(&self, dir: Option<&Dir>, files: &[File]) { + // 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 + // width of each column based on the length of the results and + // padding the fields during output. + + let columns = self.columns.for_dir(dir); + let mut cache = OSUsers::empty_cache(); + let mut table = Vec::new(); + self.get_files(&columns[], &mut cache, &mut table, files, 0); + + if self.header { + 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.cells[n].length).max().unwrap_or(0)) + .collect(); + + let mut stack = Vec::new(); + + for row in table { + for (num, column) in columns.iter().enumerate() { + let padding = column_widths[num] - row.cells[num].length; + print!("{} ", column.alignment().pad_string(&row.cells[num].text, padding)); + } + + if self.tree { + stack.resize(row.depth + 1, "├──"); + stack[row.depth] = if row.last { "└──" } else { "├──" }; + + for i in 1 .. row.depth + 1 { + print!("{}", GREY.paint(stack[i])); + } + + if row.children { + stack[row.depth] = if row.last { " " } else { "│ " }; + } + + if row.depth != 0 { + print!(" "); + } + } + + print!("{}\n", row.name); + } + } + + fn get_files(&self, columns: &[Column], cache: &mut OSUsers, dest: &mut Vec, src: &[File], depth: usize) { + 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 self.tree { + if let Some(ref dir) = file.this { + let mut files = dir.files(true); + self.filter.transform_files(&mut files); + self.get_files(columns, cache, dest, files.as_slice(), depth + 1); + } + } + } + } +} + +struct Row { + pub depth: usize, + pub cells: Vec, + pub name: String, + pub last: bool, + pub children: bool, +} diff --git a/src/output/grid.rs b/src/output/grid.rs new file mode 100644 index 0000000000000000000000000000000000000000..8c5d6320e3bdd5f64f1d9237769409bf24f36de5 --- /dev/null +++ b/src/output/grid.rs @@ -0,0 +1,102 @@ +use column::Alignment::Left; +use file::File; +use super::lines::lines_view; + +use std::cmp::max; +use std::iter::{AdditiveIterator, repeat}; + +#[derive(PartialEq, Debug, Copy)] +pub struct Grid { + pub across: bool, + pub console_width: usize, +} + +impl Grid { + fn fit_into_grid(&self, files: &[File]) -> Option<(usize, Vec)> { + // TODO: this function could almost certainly be optimised... + // surely not *all* of the numbers of lines are worth searching through! + + // Instead of numbers of columns, try to find the fewest number of *lines* + // that the output will fit in. + for num_lines in 1 .. files.len() { + + // The number of columns is the number of files divided by the number + // of lines, *rounded up*. + let mut num_columns = files.len() / num_lines; + if files.len() % num_lines != 0 { + num_columns += 1; + } + + // Early abort: if there are so many columns that the width of the + // *column separators* is bigger than the width of the screen, then + // don't even try to tabulate it. + // This is actually a necessary check, because the width is stored as + // a usize, and making it go negative makes it huge instead, but it + // also serves as a speed-up. + let separator_width = (num_columns - 1) * 2; + if self.console_width < separator_width { + continue; + } + + // Remove the separator width from the available space. + let adjusted_width = self.console_width - separator_width; + + // Find the width of each column by adding the lengths of the file + // names in that column up. + let mut column_widths: Vec = repeat(0).take(num_columns).collect(); + for (index, file) in files.iter().enumerate() { + let index = if self.across { + index % num_columns + } + else { + index / num_lines + }; + column_widths[index] = max(column_widths[index], file.name.len()); + } + + // If they all fit in the terminal, combined, then success! + if column_widths.iter().map(|&x| x).sum() < adjusted_width { + return Some((num_lines, column_widths)); + } + } + + // If you get here you have really long file names. + return None; + } + + pub fn view(&self, files: &[File]) { + if let Some((num_lines, widths)) = self.fit_into_grid(files) { + for y in 0 .. num_lines { + for x in 0 .. widths.len() { + let num = if self.across { + y * widths.len() + x + } + else { + y + num_lines * x + }; + + // Show whitespace in the place of trailing files + if num >= files.len() { + continue; + } + + let ref file = files[num]; + let styled_name = file.file_colour().paint(file.name.as_slice()).to_string(); + if x == widths.len() - 1 { + // The final column doesn't need to have trailing spaces + print!("{}", styled_name); + } + else { + assert!(widths[x] >= file.name.len()); + print!("{}", Left.pad_string(&styled_name, widths[x] - file.name.len() + 2)); + } + } + print!("\n"); + } + } + else { + // Drop down to lines view if the file names are too big for a grid + lines_view(files); + } + } +} diff --git a/src/output/lines.rs b/src/output/lines.rs new file mode 100644 index 0000000000000000000000000000000000000000..6649c29c9a85fd41c8663ff279df0a246cb3d3ee --- /dev/null +++ b/src/output/lines.rs @@ -0,0 +1,8 @@ +use file::File; + +/// The lines view literally just displays each file, line-by-line. +pub fn lines_view(files: &[File]) { + for file in files { + println!("{}", file.file_name_view()); + } +} diff --git a/src/output/mod.rs b/src/output/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..054e6bf833dbed92822f1766d90315a4571c5764 --- /dev/null +++ b/src/output/mod.rs @@ -0,0 +1,7 @@ +mod grid; +mod details; +mod lines; + +pub use self::grid::Grid; +pub use self::details::Details; +pub use self::lines::lines_view;