2017-07-02 00:02:17 +00:00
|
|
|
|
use std::cmp::max;
|
2017-07-05 20:01:01 +00:00
|
|
|
|
use std::fmt;
|
2017-07-03 16:40:05 +00:00
|
|
|
|
use std::ops::Deref;
|
2017-07-02 00:02:17 +00:00
|
|
|
|
use std::sync::{Mutex, MutexGuard};
|
|
|
|
|
|
|
|
|
|
use datetime::TimeZone;
|
|
|
|
|
use zoneinfo_compiled::{CompiledData, Result as TZResult};
|
|
|
|
|
|
|
|
|
|
use locale;
|
|
|
|
|
|
|
|
|
|
use users::UsersCache;
|
|
|
|
|
|
2017-08-26 20:40:37 +00:00
|
|
|
|
use style::Colours;
|
2017-07-02 00:02:17 +00:00
|
|
|
|
use output::cell::TextCell;
|
2017-07-03 07:45:14 +00:00
|
|
|
|
use output::time::TimeFormat;
|
2017-07-05 19:16:04 +00:00
|
|
|
|
use fs::{File, Dir, fields as f};
|
2017-09-01 18:13:47 +00:00
|
|
|
|
use fs::feature::git::GitCache;
|
2017-07-05 19:16:04 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Options for displaying a table.
|
|
|
|
|
pub struct Options {
|
2017-07-05 20:01:01 +00:00
|
|
|
|
pub env: Environment,
|
2017-07-05 19:16:04 +00:00
|
|
|
|
pub size_format: SizeFormat,
|
2017-07-05 20:54:43 +00:00
|
|
|
|
pub time_format: TimeFormat,
|
2017-08-09 21:25:16 +00:00
|
|
|
|
pub extra_columns: Columns,
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-01 18:13:47 +00:00
|
|
|
|
// I had to make other types derive Debug,
|
|
|
|
|
// and Mutex<UsersCache> is not that!
|
|
|
|
|
impl fmt::Debug for Options {
|
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
|
|
|
|
write!(f, "<table options>")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-09 21:25:16 +00:00
|
|
|
|
/// Extra columns to display in the table.
|
|
|
|
|
#[derive(PartialEq, Debug)]
|
|
|
|
|
pub struct Columns {
|
|
|
|
|
|
|
|
|
|
/// At least one of these timestamps will be shown.
|
2017-07-05 19:16:04 +00:00
|
|
|
|
pub time_types: TimeTypes,
|
2017-08-09 21:25:16 +00:00
|
|
|
|
|
|
|
|
|
// The rest are just on/off
|
2017-07-05 19:16:04 +00:00
|
|
|
|
pub inode: bool,
|
|
|
|
|
pub links: bool,
|
|
|
|
|
pub blocks: bool,
|
|
|
|
|
pub group: bool,
|
2017-09-01 18:13:47 +00:00
|
|
|
|
pub git: bool,
|
2017-07-05 20:01:01 +00:00
|
|
|
|
}
|
|
|
|
|
|
2017-08-09 21:25:16 +00:00
|
|
|
|
impl Columns {
|
2017-09-01 18:13:47 +00:00
|
|
|
|
pub fn collect(&self, actually_enable_git: bool) -> Vec<Column> {
|
|
|
|
|
let mut columns = Vec::with_capacity(4);
|
2017-07-05 19:16:04 +00:00
|
|
|
|
|
|
|
|
|
if self.inode {
|
|
|
|
|
columns.push(Column::Inode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
columns.push(Column::Permissions);
|
|
|
|
|
|
|
|
|
|
if self.links {
|
|
|
|
|
columns.push(Column::HardLinks);
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-09 20:47:51 +00:00
|
|
|
|
columns.push(Column::FileSize);
|
2017-07-05 19:16:04 +00:00
|
|
|
|
|
|
|
|
|
if self.blocks {
|
|
|
|
|
columns.push(Column::Blocks);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
columns.push(Column::User);
|
|
|
|
|
|
|
|
|
|
if self.group {
|
|
|
|
|
columns.push(Column::Group);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.time_types.modified {
|
|
|
|
|
columns.push(Column::Timestamp(TimeType::Modified));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.time_types.created {
|
|
|
|
|
columns.push(Column::Timestamp(TimeType::Created));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.time_types.accessed {
|
|
|
|
|
columns.push(Column::Timestamp(TimeType::Accessed));
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-01 18:13:47 +00:00
|
|
|
|
if cfg!(feature="git") && self.git && actually_enable_git {
|
|
|
|
|
columns.push(Column::GitStatus);
|
2017-07-05 19:16:04 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
columns
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// A table contains these.
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub enum Column {
|
|
|
|
|
Permissions,
|
2017-08-09 20:47:51 +00:00
|
|
|
|
FileSize,
|
2017-07-05 19:16:04 +00:00
|
|
|
|
Timestamp(TimeType),
|
|
|
|
|
Blocks,
|
|
|
|
|
User,
|
|
|
|
|
Group,
|
|
|
|
|
HardLinks,
|
|
|
|
|
Inode,
|
|
|
|
|
GitStatus,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Each column can pick its own **Alignment**. Usually, numbers are
|
|
|
|
|
/// right-aligned, and text is left-aligned.
|
|
|
|
|
#[derive(Copy, Clone)]
|
|
|
|
|
pub enum Alignment {
|
|
|
|
|
Left, Right,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Column {
|
|
|
|
|
|
|
|
|
|
/// Get the alignment this column should use.
|
|
|
|
|
pub fn alignment(&self) -> Alignment {
|
|
|
|
|
match *self {
|
2017-08-09 20:47:51 +00:00
|
|
|
|
Column::FileSize
|
2017-07-05 19:16:04 +00:00
|
|
|
|
| Column::HardLinks
|
|
|
|
|
| Column::Inode
|
|
|
|
|
| Column::Blocks
|
|
|
|
|
| Column::GitStatus => Alignment::Right,
|
|
|
|
|
_ => Alignment::Left,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get the text that should be printed at the top, when the user elects
|
|
|
|
|
/// to have a header row printed.
|
|
|
|
|
pub fn header(&self) -> &'static str {
|
|
|
|
|
match *self {
|
|
|
|
|
Column::Permissions => "Permissions",
|
2017-08-09 20:47:51 +00:00
|
|
|
|
Column::FileSize => "Size",
|
2017-07-05 19:16:04 +00:00
|
|
|
|
Column::Timestamp(t) => t.header(),
|
|
|
|
|
Column::Blocks => "Blocks",
|
|
|
|
|
Column::User => "User",
|
|
|
|
|
Column::Group => "Group",
|
|
|
|
|
Column::HardLinks => "Links",
|
|
|
|
|
Column::Inode => "inode",
|
|
|
|
|
Column::GitStatus => "Git",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Formatting options for file sizes.
|
|
|
|
|
#[derive(PartialEq, Debug, Copy, Clone)]
|
|
|
|
|
pub enum SizeFormat {
|
|
|
|
|
|
|
|
|
|
/// Format the file size using **decimal** prefixes, such as “kilo”,
|
|
|
|
|
/// “mega”, or “giga”.
|
|
|
|
|
DecimalBytes,
|
|
|
|
|
|
|
|
|
|
/// Format the file size using **binary** prefixes, such as “kibi”,
|
|
|
|
|
/// “mebi”, or “gibi”.
|
|
|
|
|
BinaryBytes,
|
|
|
|
|
|
|
|
|
|
/// Do no formatting and just display the size as a number of bytes.
|
|
|
|
|
JustBytes,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for SizeFormat {
|
|
|
|
|
fn default() -> SizeFormat {
|
|
|
|
|
SizeFormat::DecimalBytes
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// The types of a file’s time fields. These three fields are standard
|
|
|
|
|
/// across most (all?) operating systems.
|
|
|
|
|
#[derive(PartialEq, Debug, Copy, Clone)]
|
|
|
|
|
pub enum TimeType {
|
|
|
|
|
|
|
|
|
|
/// The file’s accessed time (`st_atime`).
|
|
|
|
|
Accessed,
|
|
|
|
|
|
|
|
|
|
/// The file’s modified time (`st_mtime`).
|
|
|
|
|
Modified,
|
|
|
|
|
|
|
|
|
|
/// The file’s creation time (`st_ctime`).
|
|
|
|
|
Created,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl TimeType {
|
|
|
|
|
|
|
|
|
|
/// Returns the text to use for a column’s heading in the columns output.
|
|
|
|
|
pub fn header(&self) -> &'static str {
|
|
|
|
|
match *self {
|
|
|
|
|
TimeType::Accessed => "Date Accessed",
|
|
|
|
|
TimeType::Modified => "Date Modified",
|
|
|
|
|
TimeType::Created => "Date Created",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Fields for which of a file’s time fields should be displayed in the
|
|
|
|
|
/// columns output.
|
|
|
|
|
///
|
|
|
|
|
/// There should always be at least one of these--there's no way to disable
|
|
|
|
|
/// the time columns entirely (yet).
|
|
|
|
|
#[derive(PartialEq, Debug, Copy, Clone)]
|
|
|
|
|
pub struct TimeTypes {
|
|
|
|
|
pub accessed: bool,
|
|
|
|
|
pub modified: bool,
|
|
|
|
|
pub created: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for TimeTypes {
|
|
|
|
|
|
|
|
|
|
/// By default, display just the ‘modified’ time. This is the most
|
|
|
|
|
/// common option, which is why it has this shorthand.
|
|
|
|
|
fn default() -> TimeTypes {
|
|
|
|
|
TimeTypes { accessed: false, modified: true, created: false }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2017-07-02 00:02:17 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// 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,
|
|
|
|
|
|
|
|
|
|
/// 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()
|
|
|
|
|
}
|
|
|
|
|
|
2017-07-05 07:21:24 +00:00
|
|
|
|
pub fn load_all() -> Self {
|
2017-07-03 07:45:14 +00:00
|
|
|
|
let tz = match determine_time_zone() {
|
|
|
|
|
Ok(t) => Some(t),
|
|
|
|
|
Err(ref e) => {
|
|
|
|
|
println!("Unable to determine time zone: {}", e);
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
};
|
2017-07-02 00:02:17 +00:00
|
|
|
|
|
|
|
|
|
let numeric = locale::Numeric::load_user_locale()
|
|
|
|
|
.unwrap_or_else(|_| locale::Numeric::english());
|
|
|
|
|
|
2017-07-03 07:45:14 +00:00
|
|
|
|
let users = Mutex::new(UsersCache::new());
|
2017-07-02 00:02:17 +00:00
|
|
|
|
|
2017-07-05 20:54:43 +00:00
|
|
|
|
Environment { tz, numeric, users }
|
2017-07-02 00:02:17 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn determine_time_zone() -> TZResult<TimeZone> {
|
|
|
|
|
TimeZone::from_file("/etc/localtime")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pub struct Table<'a> {
|
2017-07-05 20:01:01 +00:00
|
|
|
|
columns: Vec<Column>,
|
2017-07-02 00:02:17 +00:00
|
|
|
|
colours: &'a Colours,
|
|
|
|
|
env: &'a Environment,
|
2017-07-03 16:40:05 +00:00
|
|
|
|
widths: TableWidths,
|
2017-07-05 20:54:43 +00:00
|
|
|
|
time_format: &'a TimeFormat,
|
2017-08-09 20:47:51 +00:00
|
|
|
|
size_format: SizeFormat,
|
2017-09-01 18:13:47 +00:00
|
|
|
|
git: Option<&'a GitCache>,
|
2017-07-02 00:02:17 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub struct Row {
|
|
|
|
|
cells: Vec<TextCell>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<'a, 'f> Table<'a> {
|
2017-09-01 18:13:47 +00:00
|
|
|
|
pub fn new(options: &'a Options, dir: Option<&'a Dir>, git: Option<&'a GitCache>, colours: &'a Colours) -> Table<'a> {
|
|
|
|
|
let has_git = if let (Some(g), Some(d)) = (git, dir) { g.has_anything_for(&d.path) } else { false };
|
|
|
|
|
let columns = options.extra_columns.collect(has_git);
|
|
|
|
|
let widths = TableWidths::zero(columns.len());
|
|
|
|
|
|
|
|
|
|
Table {
|
|
|
|
|
colours, widths, columns, git,
|
2017-08-09 20:47:51 +00:00
|
|
|
|
env: &options.env,
|
|
|
|
|
time_format: &options.time_format,
|
|
|
|
|
size_format: options.size_format,
|
|
|
|
|
}
|
2017-07-02 00:02:17 +00:00
|
|
|
|
}
|
|
|
|
|
|
2017-07-03 19:21:33 +00:00
|
|
|
|
pub fn widths(&self) -> &TableWidths {
|
|
|
|
|
&self.widths
|
2017-07-02 00:02:17 +00:00
|
|
|
|
}
|
|
|
|
|
|
2017-07-03 16:40:05 +00:00
|
|
|
|
pub fn header_row(&self) -> Row {
|
|
|
|
|
let cells = self.columns.iter()
|
|
|
|
|
.map(|c| TextCell::paint_str(self.colours.header, c.header()))
|
|
|
|
|
.collect();
|
2017-07-02 00:02:17 +00:00
|
|
|
|
|
|
|
|
|
Row { cells }
|
|
|
|
|
}
|
|
|
|
|
|
Only get an Env if one’s being used, also mutexes
This commit ties a table’s Environment to the fact that it contains columns.
Previously, the Details view would get its Environment, and then use those fields to actually display the details in the table: except for the case where we’re only displaying a tree, when it would just be ignored, instead.
This was caused by the “no columns” case using a Vec of no Columns behind the scenes, rather than disabling the table entirely; much like how a tap isn’t a zero-length swipe, the code should have been updated to reflect this. Now, the Environment is only created if it’s going to be used.
Also, fix a double-mutex-lock: the mutable Table had to be accessed under a lock, but the table contained a UsersCache, which *also* had to be accessed under a lock. This was changed so that the table is only updated *after* the threads have all been joined, so there’s no need for any lock at all. May fix #141, but not sure.
2017-07-03 16:04:37 +00:00
|
|
|
|
pub fn row_for_file(&self, file: &File, xattrs: bool) -> Row {
|
|
|
|
|
let cells = self.columns.iter()
|
|
|
|
|
.map(|c| self.display(file, c, xattrs))
|
|
|
|
|
.collect();
|
2017-07-02 00:02:17 +00:00
|
|
|
|
|
|
|
|
|
Row { cells }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn add_widths(&mut self, row: &Row) {
|
2017-07-03 16:40:05 +00:00
|
|
|
|
self.widths.add_widths(row)
|
2017-07-02 00:02:17 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 {
|
2017-07-05 19:16:04 +00:00
|
|
|
|
use output::table::TimeType::*;
|
2017-07-02 00:02:17 +00:00
|
|
|
|
|
|
|
|
|
match *column {
|
2017-08-20 19:29:23 +00:00
|
|
|
|
Column::Permissions => self.permissions_plus(file, xattrs).render(self.colours),
|
|
|
|
|
Column::FileSize => file.size().render(self.colours, self.size_format, &self.env.numeric),
|
|
|
|
|
Column::HardLinks => file.links().render(self.colours, &self.env.numeric),
|
|
|
|
|
Column::Inode => file.inode().render(self.colours.inode),
|
|
|
|
|
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()),
|
2017-09-01 18:13:47 +00:00
|
|
|
|
Column::GitStatus => self.git_status(file).render(self.colours),
|
2017-08-20 19:29:23 +00:00
|
|
|
|
|
|
|
|
|
Column::Timestamp(Modified) => file.modified_time().render(self.colours.date, &self.env.tz, &self.time_format),
|
|
|
|
|
Column::Timestamp(Created) => file.created_time() .render(self.colours.date, &self.env.tz, &self.time_format),
|
|
|
|
|
Column::Timestamp(Accessed) => file.accessed_time().render(self.colours.date, &self.env.tz, &self.time_format),
|
2017-07-02 00:02:17 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-01 18:13:47 +00:00
|
|
|
|
fn git_status(&self, file: &File) -> f::Git {
|
|
|
|
|
debug!("Getting Git status for file {:?}", file.path);
|
|
|
|
|
self.git
|
|
|
|
|
.map(|g| g.get(&file.path, file.is_directory()))
|
|
|
|
|
.unwrap_or_default()
|
|
|
|
|
}
|
|
|
|
|
|
2017-07-02 00:02:17 +00:00
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-07-03 16:40:05 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-07-03 19:21:33 +00:00
|
|
|
|
|
|
|
|
|
pub fn total(&self) -> usize {
|
|
|
|
|
self.0.len() + self.0.iter().sum::<usize>()
|
|
|
|
|
}
|
2017-07-03 16:40:05 +00:00
|
|
|
|
}
|