Merge branch 'more-misc-refactorings'

This commit merges in a *lot* of refactoring work done to the tree, details, and grid-details views.

The main one is that the concept of a “table” has been completely separated from the details view. Previously, if you wanted to use the long view without drawing the table (such as with --tree), exa used a “table” with 0 columns in it behind the scenes. This is now reversed: the table is now the optional component in the details view, and a tree-only view is just a details view without a table!

Doing this has paved the way for all sorts of code cleanups: now, we only calculate values needed for the table if one’s going to be displayed. Some of these were rather expensive to compute (such as the user’s time zone, or locale)

This refactoring is not fully complete; there are still several things that can be done, including having the errors and xattrs in the tree view use the same TreeParams constructor as the others, and separating the column widths from the table so fewer mutable values need to be passed around.

Finally, this also merges in a lot of debuffering. There were at least two places in the code where values were collected into a vector before immediately being iterated over, instead of just using those values as they were generated! For example, when displaying the table, each row was rendered into a set of cells for displaying: but then it all went into a vector, and was only displayed at the very end, because that was what I needed for the grid-details view. Now, exa is smart enough to not do that.

Basically, even if exa doesn’t actually *get* that much faster, it should at least display its first line of output quicker.

Fixes #90, but also see #82.
This commit is contained in:
Benjamin Sago 2017-07-04 18:11:37 +01:00
commit 651d23fe8a
11 changed files with 756 additions and 485 deletions

80
Vagrantfile vendored
View File

@ -295,45 +295,73 @@ Vagrant.configure(2) do |config|
EOF
old = '200303030000.00'
med = '200606152314.29'
new = '200907221038.53'
# Awkward date and time testcases.
config.vm.provision :shell, privileged: false, inline: <<-EOF
set -xe
mkdir "#{test_dir}/dates"
# there's no way to touch the created date of a file...
# so we have to do this the old-fashioned way!
# (and make sure these don't actually get listed)
touch -t #{old} "#{test_dir}/dates/peach"; sleep 1
touch -t #{med} "#{test_dir}/dates/plum"; sleep 1
touch -t #{new} "#{test_dir}/dates/pear"
# modified dates
touch -t #{old} -m "#{test_dir}/dates/pear"
touch -t #{med} -m "#{test_dir}/dates/peach"
touch -t #{new} -m "#{test_dir}/dates/plum"
# accessed dates
touch -t #{old} -a "#{test_dir}/dates/plum"
touch -t #{med} -a "#{test_dir}/dates/pear"
touch -t #{new} -a "#{test_dir}/dates/peach"
EOF
# Awkward extended attribute testcases.
# We need to test combinations of various numbers of files *and*
# extended attributes in directories. Turns out, the easiest way to
# do this is to generate all combinations of files with “one-xattr”
# or “two-xattrs” in their name and directories with “empty” or
# “one-file” in their name, then just give the right number of
# xattrs and children to those.
config.vm.provision :shell, privileged: false, inline: <<-EOF
set -xe
mkdir "#{test_dir}/attributes"
touch "#{test_dir}/attributes/none"
touch "#{test_dir}/attributes/one"
setfattr -n user.greeting -v hello "#{test_dir}/attributes/one"
touch "#{test_dir}/attributes/two"
setfattr -n user.greeting -v hello "#{test_dir}/attributes/two"
setfattr -n user.another_greeting -v hi "#{test_dir}/attributes/two"
#touch "#{test_dir}/attributes/forbidden"
#setfattr -n user.greeting -v hello "#{test_dir}/attributes/forbidden"
#chmod +a "$YOU deny readextattr" "#{test_dir}/attributes/forbidden"
mkdir "#{test_dir}/attributes/files"
touch "#{test_dir}/attributes/files/"{no-xattrs,one-xattr,two-xattrs}{,_forbidden}
mkdir "#{test_dir}/attributes/dirs"
mkdir "#{test_dir}/attributes/dirs/"{no-xattrs,one-xattr,two-xattrs}_{empty,one-file,two-files}{,_forbidden}
mkdir "#{test_dir}/attributes/dirs/empty-with-attribute"
setfattr -n user.greeting -v hello "#{test_dir}/attributes/dirs/empty-with-attribute"
setfattr -n user.greeting -v hello "#{test_dir}/attributes"/**/*{one-xattr,two-xattrs}*
setfattr -n user.another_greeting -v hi "#{test_dir}/attributes"/**/*two-xattrs*
mkdir "#{test_dir}/attributes/dirs/full-with-attribute"
touch "#{test_dir}/attributes/dirs/full-with-attribute/file"
setfattr -n user.greeting -v hello "#{test_dir}/attributes/dirs/full-with-attribute"
for dir in "#{test_dir}/attributes/dirs/"*one-file*; do
touch $dir/file-in-question
done
mkdir "#{test_dir}/attributes/dirs/full-but-forbidden"
touch "#{test_dir}/attributes/dirs/full-but-forbidden/file"
#setfattr -n user.greeting -v hello "#{test_dir}/attributes/dirs/full-but-forbidden"
#chmod 000 "#{test_dir}/attributes/dirs/full-but-forbidden"
#chmod +a "$YOU deny readextattr" "#{test_dir}/attributes/dirs/full-but-forbidden"
for dir in "#{test_dir}/attributes/dirs/"*two-files*; do
touch $dir/this-file
touch $dir/that-file
done
touch -t #{some_date} "#{test_dir}/attributes"
touch -t #{some_date} "#{test_dir}/attributes/"*
touch -t #{some_date} "#{test_dir}/attributes/dirs/"*
touch -t #{some_date} "#{test_dir}/attributes/dirs/"*/*
touch -t #{some_date} "#{test_dir}/attributes" # there's probably
touch -t #{some_date} "#{test_dir}/attributes"/* # a better
touch -t #{some_date} "#{test_dir}/attributes"/*/* # way to
touch -t #{some_date} "#{test_dir}/attributes"/*/*/* # do this
# I want to use the following to test,
# but it only works on macos:
#chmod +a "#{user} deny readextattr" "#{test_dir}/attributes"/**/*_forbidden
sudo chmod 000 "#{test_dir}/attributes"/**/*_forbidden
sudo chown #{user}:#{user} -R "#{test_dir}/attributes"
EOF

View File

@ -174,7 +174,7 @@ impl<'w, W: Write + 'w> Exa<'w, W> {
Mode::Lines => lines::Render { files, colours, classify }.render(self.writer),
Mode::Grid(ref opts) => grid::Render { files, colours, classify, opts }.render(self.writer),
Mode::Details(ref opts) => details::Render { dir, files, colours, classify, opts, filter: &self.options.filter, recurse: self.options.dir_action.recurse_options() }.render(self.writer),
Mode::GridDetails(ref grid, ref details) => grid_details::Render { dir, files, colours, classify, grid, details }.render(self.writer),
Mode::GridDetails(ref grid, ref details) => grid_details::Render { dir, files, colours, classify, grid, details, filter: &self.options.filter }.render(self.writer),
}
}
else {

View File

@ -58,47 +58,21 @@
//! 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.
//!
//!
//! ## 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:
//!
//! ```text
//! .rw-r--r-- 0 ben 3 Sep 13:26 forbidden
//! └── <Permission denied (os error 13)>
//! .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::io::{Write, Error as IOError, Result as IOResult};
use std::ops::Add;
use std::path::PathBuf;
use std::sync::{Arc, Mutex, MutexGuard};
use std::vec::IntoIter as VecIntoIter;
use datetime::fmt::DateFormat;
use datetime::{LocalDateTime, DatePiece};
use datetime::TimeZone;
use zoneinfo_compiled::{CompiledData, Result as TZResult};
use locale;
use users::{Users, Groups, UsersCache};
use fs::{Dir, File, fields as f};
use fs::{Dir, File};
use fs::feature::xattr::{Attribute, FileAttributes};
use options::{FileFilter, RecurseOptions};
use output::colours::Colours;
use output::column::{Alignment, Column, Columns};
use output::cell::{TextCell, TextCellContents};
use output::tree::TreeTrunk;
use output::column::Columns;
use output::cell::TextCell;
use output::tree::{TreeTrunk, TreeParams, TreeDepth};
use output::file_name::{FileName, LinkStyle, Classify};
use output::table::{Table, Environment, Row as TableRow};
/// With the **Details** view, the output gets formatted into columns, with
@ -127,87 +101,6 @@ pub struct Options {
pub xattr: bool,
}
/// The **environment** struct contains any data that could change between
/// running instances of exa, depending on the user's computer's configuration.
///
/// Any environment field should be able to be mocked up for test runs.
pub struct Environment<U> { // where U: Users+Groups
/// The year of the current time. This gets used to determine which date
/// format to use.
current_year: i64,
/// Localisation rules for formatting numbers.
numeric: locale::Numeric,
/// Localisation rules for formatting timestamps.
time: locale::Time,
/// Date format for printing out timestamps that are in the current year.
date_and_time: DateFormat<'static>,
/// Date format for printing out timestamps that *arent*.
date_and_year: DateFormat<'static>,
/// The computer's current time zone. This gets used to determine how to
/// offset files' timestamps.
tz: Option<TimeZone>,
/// Mapping cache of user IDs to usernames.
users: Mutex<U>,
}
impl<U> Environment<U> {
pub fn lock_users(&self) -> MutexGuard<U> {
self.users.lock().unwrap()
}
}
impl Default for Environment<UsersCache> {
fn default() -> Self {
use unicode_width::UnicodeWidthStr;
let tz = determine_time_zone();
if let Err(ref e) = tz {
println!("Unable to determine time zone: {}", e);
}
let numeric = locale::Numeric::load_user_locale()
.unwrap_or_else(|_| locale::Numeric::english());
let time = locale::Time::load_user_locale()
.unwrap_or_else(|_| locale::Time::english());
// Some locales use a three-character wide month name (Jan to Dec);
// others vary between three and four (1月 to 12月). We assume that
// December is the month with the maximum width, and use the width of
// that to determine how to pad the other months.
let december_width = UnicodeWidthStr::width(&*time.short_month_name(11));
let date_and_time = match december_width {
4 => DateFormat::parse("{2>:D} {4>:M} {2>:h}:{02>:m}").unwrap(),
_ => DateFormat::parse("{2>:D} {:M} {2>:h}:{02>:m}").unwrap(),
};
let date_and_year = match december_width {
4 => DateFormat::parse("{2>:D} {4>:M} {5>:Y}").unwrap(),
_ => DateFormat::parse("{2>:D} {:M} {5>:Y}").unwrap()
};
Environment {
current_year: LocalDateTime::now().year(),
numeric: numeric,
date_and_time: date_and_time,
date_and_year: date_and_year,
time: time,
tz: tz.ok(),
users: Mutex::new(UsersCache::new()),
}
}
}
fn determine_time_zone() -> TZResult<TimeZone> {
TimeZone::from_file("/etc/localtime")
}
pub struct Render<'a> {
@ -226,36 +119,52 @@ pub struct Render<'a> {
pub filter: &'a FileFilter,
}
struct Egg<'a> {
table_row: Option<TableRow>,
xattrs: Vec<Attribute>,
errors: Vec<(IOError, Option<PathBuf>)>,
dir: Option<Dir>,
file: &'a File<'a>,
}
impl<'a> AsRef<File<'a>> for Egg<'a> {
fn as_ref(&self) -> &File<'a> {
self.file
}
}
impl<'a> Render<'a> {
pub fn render<W: Write>(&self, w: &mut W) -> IOResult<()> {
pub fn render<W: Write>(self, w: &mut W) -> IOResult<()> {
let mut rows = Vec::new();
// First, transform the Columns object into a vector of columns for
// the current directory.
let columns_for_dir = match self.opts.columns {
Some(cols) => cols.for_dir(self.dir),
None => Vec::new(),
};
if let Some(columns) = self.opts.columns {
let env = Environment::default();
let colz = columns.for_dir(self.dir);
let mut table = Table::new(&colz, &self.colours, &env);
// Then, retrieve various environment variables.
let env = Arc::new(Environment::<UsersCache>::default());
if self.opts.header {
let header = table.header_row();
table.add_widths(&header);
rows.push(self.render_header(header));
}
// Build the table to put rows in.
let mut table = Table {
columns: &*columns_for_dir,
colours: self.colours,
classify: self.classify,
xattr: self.opts.xattr,
env: env,
rows: Vec::new(),
};
// This is weird, but I can't find a way around it:
// https://internals.rust-lang.org/t/should-option-mut-t-implement-copy/3715/6
let mut table = Some(table);
self.add_files_to_table(&mut table, &mut rows, &self.files, TreeDepth::root());
// Next, add a header if the user requests it.
if self.opts.header { table.add_header() }
for row in self.iterate_with_table(table.unwrap(), rows) {
writeln!(w, "{}", row.strings())?
}
}
else {
self.add_files_to_table(&mut None, &mut rows, &self.files, TreeDepth::root());
// Then add files to the table and print it out.
self.add_files_to_table(&mut table, &self.files, 0);
for cell in table.print_table() {
writeln!(w, "{}", cell.strings())?;
for row in self.iterate(rows) {
writeln!(w, "{}", row.strings())?
}
}
Ok(())
@ -263,7 +172,7 @@ impl<'a> Render<'a> {
/// 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+Groups+Send>(&self, mut table: &mut Table<U>, src: &Vec<File<'dir>>, depth: usize) {
fn add_files_to_table<'dir>(&self, table: &mut Option<Table<'a>>, rows: &mut Vec<Row>, src: &Vec<File<'dir>>, depth: TreeDepth) {
use num_cpus;
use scoped_threadpool::Pool;
use std::sync::{Arc, Mutex};
@ -272,27 +181,12 @@ impl<'a> Render<'a> {
let mut pool = Pool::new(num_cpus::get() as u32);
let mut file_eggs = Vec::new();
struct Egg<'a> {
cells: Vec<TextCell>,
xattrs: Vec<Attribute>,
errors: Vec<(IOError, Option<PathBuf>)>,
dir: Option<Dir>,
file: &'a File<'a>,
}
impl<'a> AsRef<File<'a>> for Egg<'a> {
fn as_ref(&self) -> &File<'a> {
self.file
}
}
pool.scoped(|scoped| {
let file_eggs = Arc::new(Mutex::new(&mut file_eggs));
let table = Arc::new(&mut table);
let table = table.as_ref();
for file in src {
let file_eggs = file_eggs.clone();
let table = table.clone();
scoped.execute(move || {
let mut errors = Vec::new();
@ -305,23 +199,24 @@ impl<'a> Render<'a> {
};
}
let cells = table.cells_for_file(&file, !xattrs.is_empty());
let table_row = table.as_ref().map(|t| t.row_for_file(&file, !xattrs.is_empty()));
if !table.xattr {
if !self.opts.xattr {
xattrs.clear();
}
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);
if file.is_directory() && r.tree && !r.is_too_deep(depth.0) {
match file.to_dir(false) {
Ok(d) => { dir = Some(d); },
Err(e) => { errors.push((e, None)) },
}
}
};
let egg = Egg { cells, xattrs, errors, dir, file };
let egg = Egg { table_row, xattrs, errors, dir, file };
file_eggs.lock().unwrap().push(egg);
});
}
@ -329,19 +224,21 @@ impl<'a> Render<'a> {
self.filter.sort_files(&mut file_eggs);
let num_eggs = file_eggs.len();
for (index, egg) in file_eggs.into_iter().enumerate() {
for (tree_params, egg) in depth.iterate_over(file_eggs.into_iter()) {
let mut files = Vec::new();
let mut errors = egg.errors;
if let (Some(ref mut t), Some(ref row)) = (table.as_mut(), egg.table_row.as_ref()) {
t.add_widths(row);
}
let row = Row {
depth: depth,
cells: Some(egg.cells),
name: FileName::new(&egg.file, LinkStyle::FullLinkPaths, table.classify, table.colours).paint().promote(),
last: index == num_eggs - 1,
tree: tree_params,
cells: egg.table_row,
name: FileName::new(&egg.file, LinkStyle::FullLinkPaths, self.classify, self.colours).paint().promote(),
};
table.rows.push(row);
rows.push(row);
if let Some(ref dir) = egg.dir {
for file_to_add in dir.files(self.filter.dot_filter) {
@ -355,29 +252,74 @@ impl<'a> Render<'a> {
if !files.is_empty() {
for xattr in egg.xattrs {
table.add_xattr(xattr, depth + 1, false);
rows.push(self.render_xattr(xattr, TreeParams::new(depth.deeper(), false)));
}
for (error, path) in errors {
table.add_error(&error, depth + 1, false, path);
rows.push(self.render_error(&error, TreeParams::new(depth.deeper(), false), path));
}
self.add_files_to_table(table, &files, depth + 1);
self.add_files_to_table(table, rows, &files, depth.deeper());
continue;
}
}
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);
rows.push(self.render_xattr(xattr, TreeParams::new(depth.deeper(), errors.is_empty() && index == count - 1)));
}
let count = errors.len();
for (index, (error, path)) in errors.into_iter().enumerate() {
table.add_error(&error, depth + 1, index == count - 1, path);
rows.push(self.render_error(&error, TreeParams::new(depth.deeper(), index == count - 1), path));
}
}
}
pub fn render_header(&self, header: TableRow) -> Row {
Row {
tree: TreeParams::new(TreeDepth::root(), false),
cells: Some(header),
name: TextCell::paint_str(self.colours.header, "Name"),
}
}
fn render_error(&self, error: &IOError, tree: TreeParams, path: Option<PathBuf>) -> Row {
let error_message = match path {
Some(path) => format!("<{}: {}>", path.display(), error),
None => format!("<{}>", error),
};
let name = TextCell::paint(self.colours.broken_arrow, error_message);
Row { cells: None, name, tree }
}
fn render_xattr(&self, xattr: Attribute, tree: TreeParams) -> Row {
let name = TextCell::paint(self.colours.perms.attribute, format!("{} (len {})", xattr.name, xattr.size));
Row { cells: None, name, tree }
}
pub fn render_file(&self, cells: TableRow, name: TextCell, tree: TreeParams) -> Row {
Row { cells: Some(cells), name, tree }
}
pub fn iterate_with_table(&'a self, table: Table<'a>, rows: Vec<Row>) -> TableIter<'a> {
TableIter {
tree_trunk: TreeTrunk::default(),
total_width: table.widths().total(),
table: table,
inner: rows.into_iter(),
colours: self.colours,
}
}
pub fn iterate(&'a self, rows: Vec<Row>) -> Iter<'a> {
Iter {
tree_trunk: TreeTrunk::default(),
inner: rows.into_iter(),
colours: self.colours,
}
}
}
@ -389,189 +331,82 @@ pub struct Row {
/// 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<Vec<TextCell>>,
pub cells: Option<TableRow>,
/// This file's name, in coloured output. The name is treated separately
/// from the other cells, as it never requires padding.
name: TextCell,
pub name: TextCell,
/// How many directories deep into the tree structure this is. Directories
/// on top have depth 0.
depth: usize,
/// Whether this is the last entry in the directory. This flag is used
/// when calculating the tree view.
last: bool,
}
impl Row {
/// 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].width,
None => 0,
}
}
/// Information used to determine which symbols to display in a tree.
pub tree: TreeParams,
}
/// A **Table** object gets built up by the view as it lists files and
/// directories.
pub struct Table<'a, U: 'a> { // where U: Users+Groups
pub rows: Vec<Row>,
pub columns: &'a [Column],
pub colours: &'a Colours,
pub xattr: bool,
pub classify: Classify,
pub env: Arc<Environment<U>>,
pub struct TableIter<'a> {
table: Table<'a>,
tree_trunk: TreeTrunk,
total_width: usize,
colours: &'a Colours,
inner: VecIntoIter<Row>,
}
impl<'a, U: Users+Groups+'a> Table<'a, U> {
impl<'a> Iterator for TableIter<'a> {
type Item = TextCell;
/// Add a dummy "header" row to the table, which contains the names of all
/// the columns, underlined. This has dummy data for the cases that aren't
/// actually used, such as the depth or list of attributes.
pub fn add_header(&mut self) {
let row = Row {
depth: 0,
cells: Some(self.columns.iter().map(|c| TextCell::paint_str(self.colours.header, c.header())).collect()),
name: TextCell::paint_str(self.colours.header, "Name"),
last: false,
};
self.rows.push(row);
}
fn add_error(&mut self, error: &IOError, depth: usize, last: bool, path: Option<PathBuf>) {
let error_message = match path {
Some(path) => format!("<{}: {}>", path.display(), error),
None => format!("<{}>", error),
};
let row = Row {
depth: depth,
cells: None,
name: TextCell::paint(self.colours.broken_arrow, error_message),
last: last,
};
self.rows.push(row);
}
fn add_xattr(&mut self, xattr: Attribute, depth: usize, last: bool) {
let row = Row {
depth: depth,
cells: None,
name: TextCell::paint(self.colours.perms.attribute, format!("{} (len {})", xattr.name, xattr.size)),
last: last,
};
self.rows.push(row);
}
pub fn filename(&self, file: &File, links: LinkStyle) -> TextCellContents {
FileName::new(file, links, self.classify, &self.colours).paint()
}
pub fn add_file_with_cells(&mut self, cells: Vec<TextCell>, name_cell: TextCell, depth: usize, last: bool) {
let row = Row {
depth: depth,
cells: Some(cells),
name: name_cell,
last: last,
};
self.rows.push(row);
}
/// Use the list of columns to find which cells should be produced for
/// this file, per-column.
pub fn cells_for_file(&self, file: &File, xattrs: bool) -> Vec<TextCell> {
self.columns.iter()
.map(|c| self.display(file, c, xattrs))
.collect()
}
fn permissions_plus(&self, file: &File, xattrs: bool) -> f::PermissionsPlus {
f::PermissionsPlus {
file_type: file.type_char(),
permissions: file.permissions(),
xattrs: xattrs,
}
}
fn display(&self, file: &File, column: &Column, xattrs: bool) -> TextCell {
use output::column::TimeType::*;
match *column {
Column::Permissions => self.permissions_plus(file, xattrs).render(&self.colours),
Column::FileSize(fmt) => file.size().render(&self.colours, fmt, &self.env.numeric),
Column::Timestamp(Modified) => file.modified_time().render(&self.colours, &self.env.tz, &self.env.date_and_time, &self.env.date_and_year, &self.env.time, self.env.current_year),
Column::Timestamp(Created) => file.created_time().render( &self.colours, &self.env.tz, &self.env.date_and_time, &self.env.date_and_year, &self.env.time, self.env.current_year),
Column::Timestamp(Accessed) => file.accessed_time().render(&self.colours, &self.env.tz, &self.env.date_and_time, &self.env.date_and_year, &self.env.time, self.env.current_year),
Column::HardLinks => file.links().render(&self.colours, &self.env.numeric),
Column::Inode => file.inode().render(&self.colours),
Column::Blocks => file.blocks().render(&self.colours),
Column::User => file.user().render(&self.colours, &*self.env.lock_users()),
Column::Group => file.group().render(&self.colours, &*self.env.lock_users()),
Column::GitStatus => file.git_status().render(&self.colours),
}
}
/// Render the table as a vector of Cells, to be displayed on standard output.
pub fn print_table(self) -> Vec<TextCell> {
let mut tree_trunk = TreeTrunk::default();
let mut cells = Vec::new();
// Work out the list of column widths by finding the longest cell for
// each column, then formatting each cell in that column to be the
// width of that one.
let column_widths: Vec<usize> = (0 .. self.columns.len())
.map(|n| self.rows.iter().map(|row| row.column_width(n)).max().unwrap_or(0))
.collect();
let total_width: usize = self.columns.len() + column_widths.iter().fold(0, Add::add);
for row in self.rows {
let mut cell = TextCell::default();
if let Some(cells) = row.cells {
for (n, (this_cell, width)) in cells.into_iter().zip(column_widths.iter()).enumerate() {
let padding = width - *this_cell.width;
match self.columns[n].alignment() {
Alignment::Left => { cell.append(this_cell); cell.add_spaces(padding); }
Alignment::Right => { cell.add_spaces(padding); cell.append(this_cell); }
}
cell.add_spaces(1);
fn next(&mut self) -> Option<Self::Item> {
self.inner.next().map(|row| {
let mut cell =
if let Some(cells) = row.cells {
self.table.render(cells)
}
}
else {
cell.add_spaces(total_width)
}
else {
let mut cell = TextCell::default();
cell.add_spaces(self.total_width);
cell
};
let mut filename = TextCell::default();
for tree_part in tree_trunk.new_row(row.depth, row.last) {
filename.push(self.colours.punctuation.paint(tree_part.ascii_art()), 4);
for tree_part in self.tree_trunk.new_row(row.tree) {
cell.push(self.colours.punctuation.paint(tree_part.ascii_art()), 4);
}
// If any tree characters have been printed, then add an extra
// space, which makes the output look much better.
if row.depth != 0 {
filename.add_spaces(1);
if !row.tree.is_at_root() {
cell.add_spaces(1);
}
// Print the name without worrying about padding.
filename.append(row.name);
cell.append(filename);
cells.push(cell);
}
cells
cell.append(row.name);
cell
})
}
}
pub struct Iter<'a> {
tree_trunk: TreeTrunk,
colours: &'a Colours,
inner: VecIntoIter<Row>,
}
impl<'a> Iterator for Iter<'a> {
type Item = TextCell;
fn next(&mut self) -> Option<Self::Item> {
self.inner.next().map(|row| {
let mut cell = TextCell::default();
for tree_part in self.tree_trunk.new_row(row.tree) {
cell.push(self.colours.punctuation.paint(tree_part.ascii_art()), 4);
}
// If any tree characters have been printed, then add an extra
// space, which makes the output look much better.
if !row.tree.is_at_root() {
cell.add_spaces(1);
}
cell.append(row.name);
cell
})
}
}

View File

@ -1,19 +1,20 @@
use std::io::{Write, Result as IOResult};
use std::sync::Arc;
use ansi_term::ANSIStrings;
use users::UsersCache;
use term_grid as grid;
use fs::{Dir, File};
use fs::feature::xattr::FileAttributes;
use options::FileFilter;
use output::cell::TextCell;
use output::column::Column;
use output::colours::Colours;
use output::details::{Table, Environment, Options as DetailsOptions};
use output::details::{Options as DetailsOptions, Row as DetailsRow, Render as DetailsRender};
use output::grid::Options as GridOptions;
use output::file_name::{Classify, LinkStyle};
use output::file_name::{FileName, LinkStyle, Classify};
use output::table::{Table, Environment, Row as TableRow};
use output::tree::{TreeParams, TreeDepth};
pub struct Render<'a> {
@ -23,9 +24,22 @@ pub struct Render<'a> {
pub classify: Classify,
pub grid: &'a GridOptions,
pub details: &'a DetailsOptions,
pub filter: &'a FileFilter,
}
impl<'a> Render<'a> {
pub fn details(&self) -> DetailsRender<'a> {
DetailsRender {
dir: self.dir.clone(),
files: Vec::new(),
colours: self.colours,
classify: self.classify,
opts: self.details,
recurse: None,
filter: self.filter,
}
}
pub fn render<W: Write>(&self, w: &mut W) -> IOResult<()> {
let columns_for_dir = match self.details.columns {
@ -33,27 +47,24 @@ impl<'a> Render<'a> {
None => Vec::new(),
};
let env = Arc::new(Environment::default());
let env = Environment::default();
let (cells, file_names) = {
let drender = self.clone().details();
let first_table = self.make_table(env.clone(), &*columns_for_dir, self.colours, self.classify);
let (first_table, _) = self.make_table(&env, &columns_for_dir, &drender);
let cells = self.files.iter()
.map(|file| first_table.cells_for_file(file, file_has_xattrs(file)))
.collect::<Vec<_>>();
let rows = self.files.iter()
.map(|file| first_table.row_for_file(file, file_has_xattrs(file)))
.collect::<Vec<TableRow>>();
let file_names = self.files.iter()
.map(|file| first_table.filename(file, LinkStyle::JustFilenames).promote())
.collect::<Vec<_>>();
let file_names = self.files.iter()
.map(|file| FileName::new(file, LinkStyle::JustFilenames, self.classify, self.colours).paint().promote())
.collect::<Vec<TextCell>>();
(cells, file_names)
};
let mut last_working_table = self.make_grid(env.clone(), 1, &columns_for_dir, &file_names, cells.clone(), self.colours, self.classify);
let mut last_working_table = self.make_grid(&env, 1, &columns_for_dir, &file_names, rows.clone(), &drender);
for column_count in 2.. {
let grid = self.make_grid(env.clone(), column_count, &columns_for_dir, &file_names, cells.clone(), self.colours, self.classify);
let grid = self.make_grid(&env, column_count, &columns_for_dir, &file_names, rows.clone(), &drender);
let the_grid_fits = {
let d = grid.fit_into_columns(column_count);
@ -71,33 +82,35 @@ impl<'a> Render<'a> {
Ok(())
}
fn make_table<'g>(&'g self, env: Arc<Environment<UsersCache>>, columns_for_dir: &'g [Column], colours: &'g Colours, classify: Classify) -> Table<UsersCache> {
let mut table = Table {
columns: columns_for_dir,
colours, classify, env,
xattr: self.details.xattr,
rows: Vec::new(),
};
fn make_table<'t>(&'a self, env: &'a Environment, columns_for_dir: &'a [Column], drender: &DetailsRender) -> (Table<'a>, Vec<DetailsRow>) {
let mut table = Table::new(columns_for_dir, self.colours, env);
let mut rows = Vec::new();
if self.details.header { table.add_header() }
table
}
fn make_grid<'g>(&'g self, env: Arc<Environment<UsersCache>>, column_count: usize, columns_for_dir: &'g [Column], file_names: &[TextCell], cells: Vec<Vec<TextCell>>, colours: &'g Colours, classify: Classify) -> grid::Grid {
let mut tables = Vec::new();
for _ in 0 .. column_count {
tables.push(self.make_table(env.clone(), columns_for_dir, colours, classify));
if self.details.header {
let row = table.header_row();
table.add_widths(&row);
rows.push(drender.render_header(row));
}
let mut num_cells = cells.len();
(table, rows)
}
fn make_grid(&'a self, env: &'a Environment, column_count: usize, columns_for_dir: &'a [Column], file_names: &[TextCell], rows: Vec<TableRow>, drender: &DetailsRender) -> grid::Grid {
let mut tables = Vec::new();
for _ in 0 .. column_count {
tables.push(self.make_table(env.clone(), columns_for_dir, drender));
}
let mut num_cells = rows.len();
if self.details.header {
num_cells += column_count;
}
let original_height = divide_rounding_up(cells.len(), column_count);
let original_height = divide_rounding_up(rows.len(), column_count);
let height = divide_rounding_up(num_cells, column_count);
for (i, (file_name, row)) in file_names.iter().zip(cells.into_iter()).enumerate() {
for (i, (file_name, row)) in file_names.iter().zip(rows.into_iter()).enumerate() {
let index = if self.grid.across {
i % column_count
}
@ -105,10 +118,15 @@ impl<'a> Render<'a> {
i / original_height
};
tables[index].add_file_with_cells(row, file_name.clone(), 0, false);
let (ref mut table, ref mut rows) = tables[index];
table.add_widths(&row);
let details_row = drender.render_file(row, file_name.clone(), TreeParams::new(TreeDepth::root(), false));
rows.push(details_row);
}
let columns: Vec<_> = tables.into_iter().map(|t| t.print_table()).collect();
let columns: Vec<_> = tables.into_iter().map(|(table, details_rows)| {
drender.iterate_with_table(table, details_rows).collect::<Vec<_>>()
}).collect();
let direction = if self.grid.across { grid::Direction::LeftToRight }
else { grid::Direction::TopToBottom };

View File

@ -8,9 +8,11 @@ pub mod file_name;
pub mod grid_details;
pub mod grid;
pub mod lines;
pub mod time;
mod cell;
mod colours;
mod escape;
mod render;
mod tree;
mod table;

View File

@ -1,45 +1,25 @@
use datetime::TimeZone;
use fs::fields as f;
use output::cell::TextCell;
use output::colours::Colours;
use fs::fields as f;
use datetime::{LocalDateTime, TimeZone, DatePiece};
use datetime::fmt::DateFormat;
use locale;
use output::time::TimeFormat;
#[allow(trivial_numeric_casts)]
impl f::Time {
pub fn render(&self, colours: &Colours, tz: &Option<TimeZone>,
date_and_time: &DateFormat<'static>, date_and_year: &DateFormat<'static>,
time: &locale::Time, current_year: i64) -> TextCell {
// TODO(ogham): This method needs some serious de-duping!
// zoned and local times have different types at the moment,
// so it's tricky.
pub fn render(&self, colours: &Colours,
tz: &Option<TimeZone>,
style: &TimeFormat) -> TextCell {
if let Some(ref tz) = *tz {
let date = tz.to_zoned(LocalDateTime::at(self.0 as i64));
let datestamp = if date.year() == current_year {
date_and_time.format(&date, time)
}
else {
date_and_year.format(&date, time)
};
let datestamp = style.format_zoned(self.0 as i64, tz);
TextCell::paint(colours.date, datestamp)
}
else {
let date = LocalDateTime::at(self.0 as i64);
let datestamp = if date.year() == current_year {
date_and_time.format(&date, time)
}
else {
date_and_year.format(&date, time)
};
let datestamp = style.format_local(self.0 as i64);
TextCell::paint(colours.date, datestamp)
}
}
}

188
src/output/table.rs Normal file
View File

@ -0,0 +1,188 @@
use std::cmp::max;
use std::ops::Deref;
use std::sync::{Mutex, MutexGuard};
use datetime::TimeZone;
use zoneinfo_compiled::{CompiledData, Result as TZResult};
use locale;
use users::UsersCache;
use output::cell::TextCell;
use output::colours::Colours;
use output::column::{Alignment, Column};
use output::time::TimeFormat;
use fs::{File, fields as f};
/// The **environment** struct contains any data that could change between
/// running instances of exa, depending on the user's computer's configuration.
///
/// Any environment field should be able to be mocked up for test runs.
pub struct Environment {
/// Localisation rules for formatting numbers.
numeric: locale::Numeric,
/// Rules for formatting timestamps.
time_format: TimeFormat,
/// The computer's current time zone. This gets used to determine how to
/// offset files' timestamps.
tz: Option<TimeZone>,
/// Mapping cache of user IDs to usernames.
users: Mutex<UsersCache>,
}
impl Environment {
pub fn lock_users(&self) -> MutexGuard<UsersCache> {
self.users.lock().unwrap()
}
}
impl Default for Environment {
fn default() -> Self {
let tz = match determine_time_zone() {
Ok(t) => Some(t),
Err(ref e) => {
println!("Unable to determine time zone: {}", e);
None
}
};
let time_format = TimeFormat::deduce();
let numeric = locale::Numeric::load_user_locale()
.unwrap_or_else(|_| locale::Numeric::english());
let users = Mutex::new(UsersCache::new());
Environment { tz, time_format, numeric, users }
}
}
fn determine_time_zone() -> TZResult<TimeZone> {
TimeZone::from_file("/etc/localtime")
}
pub struct Table<'a> {
columns: &'a [Column],
colours: &'a Colours,
env: &'a Environment,
widths: TableWidths,
}
#[derive(Clone)]
pub struct Row {
cells: Vec<TextCell>,
}
impl<'a, 'f> Table<'a> {
pub fn new(columns: &'a [Column], colours: &'a Colours, env: &'a Environment) -> Table<'a> {
let widths = TableWidths::zero(columns.len());
Table { columns, colours, env, widths }
}
pub fn widths(&self) -> &TableWidths {
&self.widths
}
pub fn header_row(&self) -> Row {
let cells = self.columns.iter()
.map(|c| TextCell::paint_str(self.colours.header, c.header()))
.collect();
Row { cells }
}
pub fn row_for_file(&self, file: &File, xattrs: bool) -> Row {
let cells = self.columns.iter()
.map(|c| self.display(file, c, xattrs))
.collect();
Row { cells }
}
pub fn add_widths(&mut self, row: &Row) {
self.widths.add_widths(row)
}
fn permissions_plus(&self, file: &File, xattrs: bool) -> f::PermissionsPlus {
f::PermissionsPlus {
file_type: file.type_char(),
permissions: file.permissions(),
xattrs: xattrs,
}
}
fn display(&self, file: &File, column: &Column, xattrs: bool) -> TextCell {
use output::column::TimeType::*;
match *column {
Column::Permissions => self.permissions_plus(file, xattrs).render(&self.colours),
Column::FileSize(fmt) => file.size().render(&self.colours, fmt, &self.env.numeric),
Column::HardLinks => file.links().render(&self.colours, &self.env.numeric),
Column::Inode => file.inode().render(&self.colours),
Column::Blocks => file.blocks().render(&self.colours),
Column::User => file.user().render(&self.colours, &*self.env.lock_users()),
Column::Group => file.group().render(&self.colours, &*self.env.lock_users()),
Column::GitStatus => file.git_status().render(&self.colours),
Column::Timestamp(Modified) => file.modified_time().render(&self.colours, &self.env.tz, &self.env.time_format),
Column::Timestamp(Created) => file.created_time().render( &self.colours, &self.env.tz, &self.env.time_format),
Column::Timestamp(Accessed) => file.accessed_time().render(&self.colours, &self.env.tz, &self.env.time_format),
}
}
pub fn render(&self, row: Row) -> TextCell {
let mut cell = TextCell::default();
for (n, (this_cell, width)) in row.cells.into_iter().zip(self.widths.iter()).enumerate() {
let padding = width - *this_cell.width;
match self.columns[n].alignment() {
Alignment::Left => { cell.append(this_cell); cell.add_spaces(padding); }
Alignment::Right => { cell.add_spaces(padding); cell.append(this_cell); }
}
cell.add_spaces(1);
}
cell
}
}
pub struct TableWidths(Vec<usize>);
impl Deref for TableWidths {
type Target = [usize];
fn deref<'a>(&'a self) -> &'a Self::Target {
&self.0
}
}
impl TableWidths {
pub fn zero(count: usize) -> TableWidths {
TableWidths(vec![ 0; count ])
}
pub fn add_widths(&mut self, row: &Row) {
for (old_width, cell) in self.0.iter_mut().zip(row.cells.iter()) {
*old_width = max(*old_width, *cell.width);
}
}
pub fn total(&self) -> usize {
self.0.len() + self.0.iter().sum::<usize>()
}
}

79
src/output/time.rs Normal file
View File

@ -0,0 +1,79 @@
use datetime::{LocalDateTime, TimeZone, DatePiece};
use datetime::fmt::DateFormat;
use locale;
use fs::fields::time_t;
#[derive(Debug, Clone)]
pub struct TimeFormat {
/// The year of the current time. This gets used to determine which date
/// format to use.
pub current_year: i64,
/// Localisation rules for formatting timestamps.
pub locale: locale::Time,
/// Date format for printing out timestamps that are in the current year.
pub date_and_time: DateFormat<'static>,
/// Date format for printing out timestamps that *arent*.
pub date_and_year: DateFormat<'static>,
}
impl TimeFormat {
fn is_recent(&self, date: LocalDateTime) -> bool {
date.year() == self.current_year
}
#[allow(trivial_numeric_casts)]
pub fn format_local(&self, time: time_t) -> String {
let date = LocalDateTime::at(time as i64);
if self.is_recent(date) {
self.date_and_time.format(&date, &self.locale)
}
else {
self.date_and_year.format(&date, &self.locale)
}
}
#[allow(trivial_numeric_casts)]
pub fn format_zoned(&self, time: time_t, zone: &TimeZone) -> String {
let date = zone.to_zoned(LocalDateTime::at(time as i64));
if self.is_recent(date) {
self.date_and_time.format(&date, &self.locale)
}
else {
self.date_and_year.format(&date, &self.locale)
}
}
pub fn deduce() -> TimeFormat {
use unicode_width::UnicodeWidthStr;
let locale = locale::Time::load_user_locale()
.unwrap_or_else(|_| locale::Time::english());
let current_year = LocalDateTime::now().year();
// Some locales use a three-character wide month name (Jan to Dec);
// others vary between three and four (1月 to 12月). We assume that
// December is the month with the maximum width, and use the width of
// that to determine how to pad the other months.
let december_width = UnicodeWidthStr::width(&*locale.short_month_name(11));
let date_and_time = match december_width {
4 => DateFormat::parse("{2>:D} {4>:M} {2>:h}:{02>:m}").unwrap(),
_ => DateFormat::parse("{2>:D} {:M} {2>:h}:{02>:m}").unwrap(),
};
let date_and_year = match december_width {
4 => DateFormat::parse("{2>:D} {4>:M} {5>:Y}").unwrap(),
_ => DateFormat::parse("{2>:D} {:M} {5>:Y}").unwrap()
};
TimeFormat { current_year, locale, date_and_time, date_and_year }
}
}

View File

@ -38,6 +38,7 @@
//! successfully `stat`ted, we dont know how many files are going to exist in
//! each directory)
#[derive(PartialEq, Debug, Clone)]
pub enum TreePart {
@ -79,9 +80,23 @@ pub struct TreeTrunk {
stack: Vec<TreePart>,
/// A tuple for the last depth and last parameters that are passed in.
last_params: Option<(usize, bool)>,
last_params: Option<TreeParams>,
}
#[derive(Debug, Copy, Clone)]
pub struct TreeParams {
/// How many directories deep into the tree structure this is. Directories
/// on top have depth 0.
depth: TreeDepth,
/// Whether this is the last entry in the directory.
last: bool,
}
#[derive(Debug, Copy, Clone)]
pub struct TreeDepth(pub usize);
impl TreeTrunk {
/// Calculates the tree parts for an entry at the given depth and
@ -91,87 +106,169 @@ impl TreeTrunk {
///
/// This takes a `&mut self` because the results of each file are stored
/// and used in future rows.
pub fn new_row(&mut self, depth: usize, last: bool) -> &[TreePart] {
pub fn new_row(&mut self, params: TreeParams) -> &[TreePart] {
// If this isnt our first iteration, then update the tree parts thus
// far to account for there being another row after it.
if let Some((last_depth, last_last)) = self.last_params {
self.stack[last_depth] = if last_last { TreePart::Blank } else { TreePart::Line };
if let Some(last) = self.last_params {
self.stack[last.depth.0] = if last.last { TreePart::Blank } else { TreePart::Line };
}
// Make sure the stack has enough space, then add or modify another
// part into it.
self.stack.resize(depth + 1, TreePart::Edge);
self.stack[depth] = if last { TreePart::Corner } else { TreePart::Edge };
self.last_params = Some((depth, last));
self.stack.resize(params.depth.0 + 1, TreePart::Edge);
self.stack[params.depth.0] = if params.last { TreePart::Corner } else { TreePart::Edge };
self.last_params = Some(params);
// Return the tree parts as a slice of the stack.
//
// Ignoring the first component is specific to exa: when a user prints
// a tree view for multiple directories, we dont want there to be a
// zeroth level connecting the initial directories. Otherwise, not
// only are unrelated directories seemingly connected to each other,
// but the tree part of the first row doesnt connect to anything:
// Ignore the first element here to prevent a 'zeroth level' from
// appearing before the very first directory. This level would
// join unrelated directories without connecting to anything:
//
// with [0..] with [1..]
// ========== ==========
// ├── folder folder
// │ └── file └── file
// └── folder folder
// └── file └──file
//
// with [0..] with [1..]
// ========== ==========
// ├──folder folder
// │ └──file └──file
// └──folder folder
// └──file └──file
&self.stack[1..]
}
}
impl TreeParams {
pub fn new(depth: TreeDepth, last: bool) -> TreeParams {
TreeParams { depth, last }
}
pub fn is_at_root(&self) -> bool {
self.depth.0 == 0
}
}
impl TreeDepth {
pub fn root() -> TreeDepth {
TreeDepth(0)
}
pub fn deeper(self) -> TreeDepth {
TreeDepth(self.0 + 1)
}
/// Creates an iterator that, as well as yielding each value, yields a
/// `TreeParams` with the current depth and last flag filled in.
pub fn iterate_over<I, T>(self, inner: I) -> Iter<I>
where I: ExactSizeIterator+Iterator<Item=T> {
Iter { current_depth: self, inner }
}
}
pub struct Iter<I> {
current_depth: TreeDepth,
inner: I,
}
impl<I, T> Iterator for Iter<I>
where I: ExactSizeIterator+Iterator<Item=T> {
type Item = (TreeParams, T);
fn next(&mut self) -> Option<Self::Item> {
self.inner.next().map(|t| {
// use exact_size_is_empty API soon
(TreeParams::new(self.current_depth, self.inner.len() == 0), t)
})
}
}
#[cfg(test)]
mod test {
mod trunk_test {
use super::*;
fn params(depth: usize, last: bool) -> TreeParams {
TreeParams::new(TreeDepth(depth), last)
}
#[test]
fn empty_at_first() {
let mut tt = TreeTrunk::default();
assert_eq!(tt.new_row(0, true), &[]);
assert_eq!(tt.new_row(params(0, true)), &[]);
}
#[test]
fn one_child() {
let mut tt = TreeTrunk::default();
assert_eq!(tt.new_row(0, true), &[]);
assert_eq!(tt.new_row(1, true), &[ TreePart::Corner ]);
assert_eq!(tt.new_row(params(0, true)), &[]);
assert_eq!(tt.new_row(params(1, true)), &[ TreePart::Corner ]);
}
#[test]
fn two_children() {
let mut tt = TreeTrunk::default();
assert_eq!(tt.new_row(0, true), &[]);
assert_eq!(tt.new_row(1, false), &[ TreePart::Edge ]);
assert_eq!(tt.new_row(1, true), &[ TreePart::Corner ]);
assert_eq!(tt.new_row(params(0, true)), &[]);
assert_eq!(tt.new_row(params(1, false)), &[ TreePart::Edge ]);
assert_eq!(tt.new_row(params(1, true)), &[ TreePart::Corner ]);
}
#[test]
fn two_times_two_children() {
let mut tt = TreeTrunk::default();
assert_eq!(tt.new_row(0, false), &[]);
assert_eq!(tt.new_row(1, false), &[ TreePart::Edge ]);
assert_eq!(tt.new_row(1, true), &[ TreePart::Corner ]);
assert_eq!(tt.new_row(params(0, false)), &[]);
assert_eq!(tt.new_row(params(1, false)), &[ TreePart::Edge ]);
assert_eq!(tt.new_row(params(1, true)), &[ TreePart::Corner ]);
assert_eq!(tt.new_row(0, true), &[]);
assert_eq!(tt.new_row(1, false), &[ TreePart::Edge ]);
assert_eq!(tt.new_row(1, true), &[ TreePart::Corner ]);
assert_eq!(tt.new_row(params(0, true)), &[]);
assert_eq!(tt.new_row(params(1, false)), &[ TreePart::Edge ]);
assert_eq!(tt.new_row(params(1, true)), &[ TreePart::Corner ]);
}
#[test]
fn two_times_two_nested_children() {
let mut tt = TreeTrunk::default();
assert_eq!(tt.new_row(0, true), &[]);
assert_eq!(tt.new_row(params(0, true)), &[]);
assert_eq!(tt.new_row(1, false), &[ TreePart::Edge ]);
assert_eq!(tt.new_row(2, false), &[ TreePart::Line, TreePart::Edge ]);
assert_eq!(tt.new_row(2, true), &[ TreePart::Line, TreePart::Corner ]);
assert_eq!(tt.new_row(params(1, false)), &[ TreePart::Edge ]);
assert_eq!(tt.new_row(params(2, false)), &[ TreePart::Line, TreePart::Edge ]);
assert_eq!(tt.new_row(params(2, true)), &[ TreePart::Line, TreePart::Corner ]);
assert_eq!(tt.new_row(1, true), &[ TreePart::Corner ]);
assert_eq!(tt.new_row(2, false), &[ TreePart::Blank, TreePart::Edge ]);
assert_eq!(tt.new_row(2, true), &[ TreePart::Blank, TreePart::Corner ]);
assert_eq!(tt.new_row(params(1, true)), &[ TreePart::Corner ]);
assert_eq!(tt.new_row(params(2, false)), &[ TreePart::Blank, TreePart::Edge ]);
assert_eq!(tt.new_row(params(2, true)), &[ TreePart::Blank, TreePart::Corner ]);
}
}
#[cfg(test)]
mod iter_test {
use super::*;
#[test]
fn test_iteration() {
let foos = &[ "first", "middle", "last" ];
let mut iter = TreeDepth::root().iterate_over(foos.into_iter());
let next = iter.next().unwrap();
assert_eq!(&"first", next.1);
assert_eq!(false, next.0.last);
let next = iter.next().unwrap();
assert_eq!(&"middle", next.1);
assert_eq!(false, next.0.last);
let next = iter.next().unwrap();
assert_eq!(&"last", next.1);
assert_eq!(true, next.0.last);
assert!(iter.next().is_none());
}
#[test]
fn test_empty() {
let nothing: &[usize] = &[];
let mut iter = TreeDepth::root().iterate_over(nothing.into_iter());
assert!(iter.next().is_none());
}
}

View File

@ -1,15 +1,57 @@
drwxrwxr-x - cassowary  1 Jan 12:34 /testcases/attributes
drwxrwxr-x - cassowary  1 Jan 12:34 ├── dirs
drwxrwxr-x@ - cassowary  1 Jan 12:34 │ ├── empty-with-attribute
drwxrwxr-x - cassowary  1 Jan 12:34 │ ├── no-xattrs_empty
d--------- - cassowary  1 Jan 12:34 │ ├── no-xattrs_empty_forbidden
│ │ └── <Permission denied (os error 13)>
drwxrwxr-x - cassowary  1 Jan 12:34 │ ├── no-xattrs_one-file
.rw-rw-r-- 0 cassowary  1 Jan 12:34 │ │ └── file-in-question
d--------- - cassowary  1 Jan 12:34 │ ├── no-xattrs_one-file_forbidden
│ │ └── <Permission denied (os error 13)>
drwxrwxr-x - cassowary  1 Jan 12:34 │ ├── no-xattrs_two-files
.rw-rw-r-- 0 cassowary  1 Jan 12:34 │ │ ├── that-file
.rw-rw-r-- 0 cassowary  1 Jan 12:34 │ │ └── this-file
d--------- - cassowary  1 Jan 12:34 │ ├── no-xattrs_two-files_forbidden
│ │ └── <Permission denied (os error 13)>
drwxrwxr-x@ - cassowary  1 Jan 12:34 │ ├── one-xattr_empty
│ │ └── user.greeting (len 5)
drwxrwxr-x - cassowary  1 Jan 12:34 │ ├── full-but-forbidden
.rw-rw-r-- 0 cassowary  1 Jan 12:34 │ │ └── file
drwxrwxr-x@ - cassowary  1 Jan 12:34 │ └── full-with-attribute
│ ├── user.greeting (len 5)
.rw-rw-r-- 0 cassowary  1 Jan 12:34 │ └── file
.rw-rw-r-- 0 cassowary  1 Jan 12:34 ├── none
.rw-rw-r--@ 0 cassowary  1 Jan 12:34 ├── one
│ └── user.greeting (len 5)
.rw-rw-r--@ 0 cassowary  1 Jan 12:34 └── two
 ├── user.greeting (len 5)
 └── user.another_greeting (len 2)
d--------- - cassowary  1 Jan 12:34 │ ├── one-xattr_empty_forbidden
│ │ └── <Permission denied (os error 13)>
drwxrwxr-x@ - cassowary  1 Jan 12:34 │ ├── one-xattr_one-file
│ │ ├── user.greeting (len 5)
.rw-rw-r-- 0 cassowary  1 Jan 12:34 │ │ └── file-in-question
d--------- - cassowary  1 Jan 12:34 │ ├── one-xattr_one-file_forbidden
│ │ └── <Permission denied (os error 13)>
drwxrwxr-x@ - cassowary  1 Jan 12:34 │ ├── one-xattr_two-files
│ │ ├── user.greeting (len 5)
.rw-rw-r-- 0 cassowary  1 Jan 12:34 │ │ ├── that-file
.rw-rw-r-- 0 cassowary  1 Jan 12:34 │ │ └── this-file
d--------- - cassowary  1 Jan 12:34 │ ├── one-xattr_two-files_forbidden
│ │ └── <Permission denied (os error 13)>
drwxrwxr-x@ - cassowary  1 Jan 12:34 │ ├── two-xattrs_empty
│ │ ├── user.greeting (len 5)
│ │ └── user.another_greeting (len 2)
d--------- - cassowary  1 Jan 12:34 │ ├── two-xattrs_empty_forbidden
│ │ └── <Permission denied (os error 13)>
drwxrwxr-x@ - cassowary  1 Jan 12:34 │ ├── two-xattrs_one-file
│ │ ├── user.greeting (len 5)
│ │ ├── user.another_greeting (len 2)
.rw-rw-r-- 0 cassowary  1 Jan 12:34 │ │ └── file-in-question
d--------- - cassowary  1 Jan 12:34 │ ├── two-xattrs_one-file_forbidden
│ │ └── <Permission denied (os error 13)>
drwxrwxr-x@ - cassowary  1 Jan 12:34 │ ├── two-xattrs_two-files
│ │ ├── user.greeting (len 5)
│ │ ├── user.another_greeting (len 2)
.rw-rw-r-- 0 cassowary  1 Jan 12:34 │ │ ├── that-file
.rw-rw-r-- 0 cassowary  1 Jan 12:34 │ │ └── this-file
d--------- - cassowary  1 Jan 12:34 │ └── two-xattrs_two-files_forbidden
│ └── <Permission denied (os error 13)>
drwxrwxr-x - cassowary  1 Jan 12:34 └── files
.rw-rw-r-- 0 cassowary  1 Jan 12:34  ├── no-xattrs
.--------- 0 cassowary  1 Jan 12:34  ├── no-xattrs_forbidden
.rw-rw-r--@ 0 cassowary  1 Jan 12:34  ├── one-xattr
 │ └── user.greeting (len 5)
.--------- 0 cassowary  1 Jan 12:34  ├── one-xattr_forbidden
.rw-rw-r--@ 0 cassowary  1 Jan 12:34  ├── two-xattrs
 │ ├── user.greeting (len 5)
 │ └── user.another_greeting (len 2)
.--------- 0 cassowary  1 Jan 12:34  └── two-xattrs_forbidden

View File

@ -18,7 +18,8 @@ results="/vagrant/xtests"
# Check that no files were created more than a year ago.
# Files not from the current year use a different date format, meaning
# that tests will fail until the VM gets re-provisioned.
sudo find $testcases -mtime +365 -printf "File %p has not been modified since %TY! Consider re-provisioning; tests will probably fail.\n"
# (Ignore the folder that deliberately has dates in the past)
sudo find $testcases -mtime +365 -not -path "*/dates/*" -printf "File %p has not been modified since %TY! Consider re-provisioning; tests will probably fail.\n"
# Long view tests
@ -26,7 +27,7 @@ $exa $testcases/files -l | diff -q - $results/files_l || exit 1
$exa $testcases/files -lh | diff -q - $results/files_lh || exit 1
$exa $testcases/files -lhb | diff -q - $results/files_lhb || exit 1
$exa $testcases/files -lhB | diff -q - $results/files_lhb2 || exit 1
$exa $testcases/attributes/dirs/empty-with-attribute -lh | diff -q - $results/empty || exit 1
$exa $testcases/attributes/dirs/no-xattrs_empty -lh | diff -q - $results/empty || exit 1
$exa --color-scale $testcases/files -l | diff -q - $results/files_l_scale || exit 1
@ -56,6 +57,7 @@ COLUMNS=200 $exa $testcases/files/* -lG | diff -q - $results/files_star_lG_200
# Attributes
# (there are many tests, but they're all done in one go)
$exa $testcases/attributes -l@T | diff -q - $results/attributes || exit 1