mirror of
https://github.com/Llewellynvdm/exa.git
synced 2025-01-14 17:19:56 +00:00
Extract table from details and grid_details
This commit extracts the common table element from the details and grid_details modules, and makes it its own reusable thing. - A Table no longer holds the values it’s rendering; it just holds a continually-updated version of the maximum widths for each column. This means that all of the resulting values that turn into Rows — which here are either files, or file eggs — need to be stored *somewhere*, and that somewhere is a secondary vector that gets passed around and modified alongside the Table. - Likewise, all the mutable methods that were on Table that added a Row now *return* the row that would have been added, hoping that the row does get stored somewhere. (It does, don’t worry.) - Because rendering with mock users is tested in the user-field-rendering module, we don’t need to bother threading different types of U through the Environment, so now it’s just been specialised to UsersCache. - Accidentally speed up printing a table by not buffering its entire output first when not necessary.
This commit is contained in:
parent
22c6fb048f
commit
fc60838ff3
@ -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 {
|
||||
|
@ -58,47 +58,22 @@
|
||||
//! 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::column::Columns;
|
||||
use output::cell::TextCell;
|
||||
use output::tree::TreeTrunk;
|
||||
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 +102,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 *aren’t*.
|
||||
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 +120,42 @@ pub struct Render<'a> {
|
||||
pub filter: &'a FileFilter,
|
||||
}
|
||||
|
||||
impl<'a> Render<'a> {
|
||||
pub fn render<W: Write>(&self, w: &mut W) -> IOResult<()> {
|
||||
|
||||
// First, transform the Columns object into a vector of columns for
|
||||
// the current directory.
|
||||
struct Egg<'a> {
|
||||
table_row: 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<()> {
|
||||
let columns_for_dir = match self.opts.columns {
|
||||
Some(cols) => cols.for_dir(self.dir),
|
||||
None => Vec::new(),
|
||||
};
|
||||
|
||||
// Then, retrieve various environment variables.
|
||||
let env = Arc::new(Environment::<UsersCache>::default());
|
||||
let env = Environment::default();
|
||||
let mut table = Table::new(&columns_for_dir, &self.colours, &env);
|
||||
let mut rows = Vec::new();
|
||||
|
||||
// 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(),
|
||||
};
|
||||
if self.opts.header {
|
||||
let header = table.header_row();
|
||||
rows.push(self.render_header(header));
|
||||
}
|
||||
|
||||
// Next, add a header if the user requests it.
|
||||
if self.opts.header { table.add_header() }
|
||||
self.add_files_to_table(&mut table, &mut rows, &self.files, 0);
|
||||
|
||||
// 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(&table, rows) {
|
||||
writeln!(w, "{}", row.strings())?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -263,7 +163,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, mut table: &mut Table, rows: &mut Vec<Row>, src: &Vec<File<'dir>>, depth: usize) {
|
||||
use num_cpus;
|
||||
use scoped_threadpool::Pool;
|
||||
use std::sync::{Arc, Mutex};
|
||||
@ -272,23 +172,9 @@ 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 = Arc::new(Mutex::new(&mut table));
|
||||
|
||||
for file in src {
|
||||
let file_eggs = file_eggs.clone();
|
||||
@ -305,9 +191,9 @@ impl<'a> Render<'a> {
|
||||
};
|
||||
}
|
||||
|
||||
let cells = table.cells_for_file(&file, !xattrs.is_empty());
|
||||
let table_row = table.lock().unwrap().row_for_file(&file, !xattrs.is_empty());
|
||||
|
||||
if !table.xattr {
|
||||
if !self.opts.xattr {
|
||||
xattrs.clear();
|
||||
}
|
||||
|
||||
@ -321,7 +207,7 @@ impl<'a> Render<'a> {
|
||||
}
|
||||
};
|
||||
|
||||
let egg = Egg { cells, xattrs, errors, dir, file };
|
||||
let egg = Egg { table_row, xattrs, errors, dir, file };
|
||||
file_eggs.lock().unwrap().push(egg);
|
||||
});
|
||||
}
|
||||
@ -336,12 +222,12 @@ impl<'a> Render<'a> {
|
||||
|
||||
let row = Row {
|
||||
depth: depth,
|
||||
cells: Some(egg.cells),
|
||||
name: FileName::new(&egg.file, LinkStyle::FullLinkPaths, table.classify, table.colours).paint().promote(),
|
||||
cells: Some(egg.table_row),
|
||||
name: FileName::new(&egg.file, LinkStyle::FullLinkPaths, self.classify, self.colours).paint().promote(),
|
||||
last: index == num_eggs - 1,
|
||||
};
|
||||
|
||||
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,207 +241,109 @@ 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, depth + 1, false));
|
||||
}
|
||||
|
||||
for (error, path) in errors {
|
||||
table.add_error(&error, depth + 1, false, path);
|
||||
rows.push(self.render_error(&error, depth + 1, false, path));
|
||||
}
|
||||
|
||||
self.add_files_to_table(table, &files, depth + 1);
|
||||
self.add_files_to_table(table, rows, &files, depth + 1);
|
||||
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, depth + 1, 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, depth + 1, index == count - 1, path));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub struct Row {
|
||||
|
||||
/// Vector of cells to display.
|
||||
///
|
||||
/// Most of the rows will be used to display files' metadata, so this will
|
||||
/// 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>>,
|
||||
|
||||
/// This file's name, in coloured output. The name is treated separately
|
||||
/// from the other cells, as it never requires padding.
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 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>>,
|
||||
}
|
||||
|
||||
impl<'a, U: Users+Groups+'a> Table<'a, U> {
|
||||
|
||||
/// 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 {
|
||||
pub fn render_header(&self, header: TableRow) -> Row {
|
||||
Row {
|
||||
depth: 0,
|
||||
cells: Some(self.columns.iter().map(|c| TextCell::paint_str(self.colours.header, c.header())).collect()),
|
||||
cells: Some(header),
|
||||
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>) {
|
||||
fn render_error(&self, error: &IOError, depth: usize, last: bool, path: Option<PathBuf>) -> Row {
|
||||
let error_message = match path {
|
||||
Some(path) => format!("<{}: {}>", path.display(), error),
|
||||
None => format!("<{}>", error),
|
||||
};
|
||||
|
||||
let row = 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 {
|
||||
fn render_xattr(&self, xattr: Attribute, depth: usize, last: bool) -> 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 {
|
||||
pub fn render_file(&self, cells: TableRow, name_cell: TextCell, depth: usize, last: bool) -> 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();
|
||||
pub fn iterate(&self, table: &'a Table<'a>, rows: Vec<Row>) -> Iter<'a> {
|
||||
Iter {
|
||||
tree_trunk: TreeTrunk::default(),
|
||||
total_width: table.columns_count() + table.widths().iter().fold(0, Add::add),
|
||||
table: table,
|
||||
inner: rows.into_iter(),
|
||||
colours: self.colours,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
pub struct Iter<'a> {
|
||||
table: &'a Table<'a>,
|
||||
tree_trunk: TreeTrunk,
|
||||
total_width: usize,
|
||||
colours: &'a Colours,
|
||||
inner: VecIntoIter<Row>,
|
||||
}
|
||||
|
||||
let total_width: usize = self.columns.len() + column_widths.iter().fold(0, Add::add);
|
||||
impl<'a> Iterator for Iter<'a> {
|
||||
type Item = TextCell;
|
||||
|
||||
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) {
|
||||
for tree_part in self.tree_trunk.new_row(row.depth, row.last) {
|
||||
filename.push(self.colours.punctuation.paint(tree_part.ascii_art()), 4);
|
||||
}
|
||||
|
||||
@ -569,9 +357,31 @@ impl<'a, U: Users+Groups+'a> Table<'a, U> {
|
||||
filename.append(row.name);
|
||||
|
||||
cell.append(filename);
|
||||
cells.push(cell);
|
||||
}
|
||||
|
||||
cells
|
||||
cell
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Row {
|
||||
|
||||
/// Vector of cells to display.
|
||||
///
|
||||
/// Most of the rows will be used to display files' metadata, so this will
|
||||
/// 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.
|
||||
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.
|
||||
pub name: TextCell,
|
||||
|
||||
/// How many directories deep into the tree structure this is. Directories
|
||||
/// on top have depth 0.
|
||||
pub depth: usize,
|
||||
|
||||
/// Whether this is the last entry in the directory. This flag is used
|
||||
/// when calculating the tree view.
|
||||
pub last: bool,
|
||||
}
|
||||
|
@ -1,19 +1,19 @@
|
||||
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};
|
||||
|
||||
|
||||
pub struct Render<'a> {
|
||||
@ -23,9 +23,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 +46,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 (mut 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 +81,33 @@ 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 {
|
||||
rows.push(drender.render_header(table.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 +115,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(), 0, 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(&table, details_rows).collect::<Vec<_>>()
|
||||
}).collect();
|
||||
|
||||
let direction = if self.grid.across { grid::Direction::LeftToRight }
|
||||
else { grid::Direction::TopToBottom };
|
||||
|
@ -14,3 +14,4 @@ mod colours;
|
||||
mod escape;
|
||||
mod render;
|
||||
mod tree;
|
||||
mod table;
|
||||
|
204
src/output/table.rs
Normal file
204
src/output/table.rs
Normal file
@ -0,0 +1,204 @@
|
||||
use std::cmp::max;
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
|
||||
use datetime::fmt::DateFormat;
|
||||
use datetime::{LocalDateTime, DatePiece};
|
||||
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 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 {
|
||||
|
||||
/// 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 *aren’t*.
|
||||
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<UsersCache>,
|
||||
}
|
||||
|
||||
impl Environment {
|
||||
pub fn lock_users(&self) -> MutexGuard<UsersCache> {
|
||||
self.users.lock().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Environment {
|
||||
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 Table<'a> {
|
||||
columns: &'a [Column],
|
||||
colours: &'a Colours,
|
||||
env: &'a Environment,
|
||||
widths: Vec<usize>,
|
||||
}
|
||||
|
||||
#[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 = vec![ 0; columns.len() ];
|
||||
Table { columns, colours, env, widths }
|
||||
}
|
||||
|
||||
pub fn columns_count(&self) -> usize {
|
||||
self.columns.len()
|
||||
}
|
||||
|
||||
pub fn widths(&self) -> &[usize] {
|
||||
&self.widths
|
||||
}
|
||||
|
||||
pub fn header_row(&mut self) -> Row {
|
||||
let mut cells = Vec::with_capacity(self.columns.len());
|
||||
|
||||
for (old_width, column) in self.widths.iter_mut().zip(self.columns.iter()) {
|
||||
let column = TextCell::paint_str(self.colours.header, column.header());
|
||||
*old_width = max(*old_width, *column.width);
|
||||
cells.push(column);
|
||||
}
|
||||
|
||||
Row { cells }
|
||||
}
|
||||
|
||||
pub fn row_for_file(&mut self, file: &File, xattrs: bool) -> Row {
|
||||
let mut cells = Vec::with_capacity(self.columns.len());
|
||||
|
||||
let other = self.columns.iter().map(|c| self.display(file, c, xattrs)).collect::<Vec<_>>();
|
||||
for (old_width, column) in self.widths.iter_mut().zip(other.into_iter()) {
|
||||
*old_width = max(*old_width, *column.width);
|
||||
cells.push(column);
|
||||
}
|
||||
|
||||
Row { cells }
|
||||
}
|
||||
|
||||
pub fn add_widths(&mut self, row: &Row) {
|
||||
for (old_width, cell) in self.widths.iter_mut().zip(row.cells.iter()) {
|
||||
*old_width = max(*old_width, *cell.width);
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user