Refactor the code after tree view changes

- Turn the views and main program loop into structs, rather than just as one gigantic function
- Separate views into their own files

The addition of the git column and the tree view meant that a lot of functions now just took extra arguments that didn't seem to fit. For example, it didn't really work to have only one 'view' method that printed out everything, as the different view options now all take different parameters.
This commit is contained in:
Ben S 2015-02-05 14:39:56 +00:00
parent 5f2acf570c
commit 42ae7b3d33
9 changed files with 459 additions and 385 deletions

View File

@ -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)]

View File

@ -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.

View File

@ -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<Path> = vec![];
let mut files: Vec<File> = vec![];
struct Exa<'a> {
count: usize,
options: Options,
dirs: Vec<Path>,
files: Vec<File<'a>>,
}
// 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),
impl<'a> Exa<'a> {
fn new(options: Options) -> Exa<'a> {
Exa {
count: 0,
options: options,
dirs: Vec::new(),
files: Vec::new(),
}
count += 1;
}
let mut first = files.is_empty();
if !files.is_empty() {
options.view(None, &files[], options.filter);
}
// 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");
}
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 load<T>(&mut self, iter: T) where T: Iterator<Item = &'a String> {
// 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 count > 1 {
println!("{}:", dir_path.display());
self.count += 1;
}
}
fn print_files(&self) {
if !self.files.is_empty() {
self.print(None, &self.files[]);
}
}
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 self.count > 1 {
println!("{}:", dir_path.display());
}
self.count += 1;
self.print(Some(dir), &files[]);
}
count += 1;
Err(e) => {
println!("{}: {}", dir_path.display(), e);
return;
}
};
}
}
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<String> = 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());

View File

@ -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<String>,
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<Options, Misfire> {
pub fn getopts(args: &[String]) -> Result<(Options, Vec<String>), 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<File<'a>>) {
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<View, Misfire> {
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<View, Misfire> {
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("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") {
Err(Misfire::Useless("oneline", true, "long"))
if matches.opt_present("across") {
Err(Misfire::Useless("across", true, "oneline"))
}
else {
Ok(View::Lines)
}
}
else {
Ok(View::Details(try!(Columns::new(matches)), matches.opt_present("header"), matches.opt_present("tree")))
}
}
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 {
Ok(View::Lines)
}
}
else {
match dimensions() {
None => Ok(View::Lines),
Some((width, _)) => Ok(View::Grid(matches.opt_present("across"), width)),
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<SizeFormat, Misfire> {
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<SizeFormat, Misfire> {
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<DirAction, Misfire> {
let recurse = matches.opt_present("recurse");
let list = matches.opt_present("list-dirs");
let tree = matches.opt_present("tree");
/// What to do when encountering a directory?
#[derive(PartialEq, Debug, Copy)]
pub enum DirAction {
AsFile, List, Recurse, 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),
impl DirAction {
pub fn deduce(matches: &getopts::Matches) -> Result<DirAction, Misfire> {
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<Columns, Misfire> {
pub fn deduce(matches: &getopts::Matches) -> Result<Columns, Misfire> {
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<Options, Misfire>) -> bool {
fn is_helpful<T>(misfire: Result<T, Misfire>) -> 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<String> = 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<String> = opts.path_strs;
let args = Options::getopts(&[]).unwrap().1;
assert_eq!(args, vec![ ".".to_string() ])
}

View File

@ -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<usize>)> {
// 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<usize> = 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<usize> = 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<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 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<Cell>,
pub name: String,
pub last: bool,
pub children: bool,
}

106
src/output/details.rs Normal file
View File

@ -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<usize> = 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<Row>, 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<Cell>,
pub name: String,
pub last: bool,
pub children: bool,
}

102
src/output/grid.rs Normal file
View File

@ -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<usize>)> {
// 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<usize> = 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);
}
}
}

8
src/output/lines.rs Normal file
View File

@ -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());
}
}

7
src/output/mod.rs Normal file
View File

@ -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;