mirror of
https://github.com/Llewellynvdm/exa.git
synced 2024-11-22 20:15:11 +00:00
Merge branch 'time-styles-properly'
This merges in the ability to use different time styles, such as full ISO-formatted timestamps instead of just using the default variable style. Firstly, this moved the Environment from the Table to the Columns, so it 1) would only be instantiated when a table is actually used, and 2) can be affected by command-line options. Next, it renames Columns to table::Options, in line with what the view optionses were renamed to. Finally, it adds support for more time styles, deferring timestamp formatting to an enum. Fixes #133.
This commit is contained in:
commit
690aa21ac8
@ -51,10 +51,12 @@ These options are available when running with --long (`-l`):
|
|||||||
- **-U**, **--created**: use the created timestamp field
|
- **-U**, **--created**: use the created timestamp field
|
||||||
- **-@**, **--extended**: list each file's extended attributes and sizes
|
- **-@**, **--extended**: list each file's extended attributes and sizes
|
||||||
- **--git**: list each file's Git status, if tracked
|
- **--git**: list each file's Git status, if tracked
|
||||||
|
- **--time-style**: how to format timestamps
|
||||||
|
|
||||||
- Valid **--color** options are **always**, **automatic**, and **never**.
|
- Valid **--color** options are **always**, **automatic**, and **never**.
|
||||||
- Valid sort fields are **accessed**, **created**, **extension**, **Extension**, **inode**, **modified**, **name**, **Name**, **size**, **type**, and **none**. Fields starting with a capital letter are case-sensitive.
|
- Valid sort fields are **accessed**, **created**, **extension**, **Extension**, **inode**, **modified**, **name**, **Name**, **size**, **type**, and **none**. Fields starting with a capital letter are case-sensitive.
|
||||||
- Valid time fields are **modified**, **accessed**, and **created**.
|
- Valid time fields are **modified**, **accessed**, and **created**.
|
||||||
|
- Valid time styles are **default**, **iso**, **long-iso**, and **full-iso**.
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
2
Vagrantfile
vendored
2
Vagrantfile
vendored
@ -320,6 +320,8 @@ Vagrant.configure(2) do |config|
|
|||||||
touch -t #{old} -a "#{test_dir}/dates/plum"
|
touch -t #{old} -a "#{test_dir}/dates/plum"
|
||||||
touch -t #{med} -a "#{test_dir}/dates/pear"
|
touch -t #{med} -a "#{test_dir}/dates/pear"
|
||||||
touch -t #{new} -a "#{test_dir}/dates/peach"
|
touch -t #{new} -a "#{test_dir}/dates/peach"
|
||||||
|
|
||||||
|
sudo chown #{user}:#{user} -R "#{test_dir}/dates"
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,6 +22,11 @@ _exa()
|
|||||||
COMPREPLY=( $( compgen -W 'accessed modified created --' -- $cur ) )
|
COMPREPLY=( $( compgen -W 'accessed modified created --' -- $cur ) )
|
||||||
return
|
return
|
||||||
;;
|
;;
|
||||||
|
|
||||||
|
--time-style)
|
||||||
|
COMPREPLY=( $( compgen -W 'default iso long-iso full-iso --' -- $cur ) )
|
||||||
|
return
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
case "$cur" in
|
case "$cur" in
|
||||||
|
@ -57,6 +57,12 @@ complete -c exa -s 't' -l 'time' -x -d "Which timestamp field to list" -a "
|
|||||||
"
|
"
|
||||||
complete -c exa -s 'u' -l 'accessed' -d "Use the accessed timestamp field"
|
complete -c exa -s 'u' -l 'accessed' -d "Use the accessed timestamp field"
|
||||||
complete -c exa -s 'U' -l 'created' -d "Use the created timestamp field"
|
complete -c exa -s 'U' -l 'created' -d "Use the created timestamp field"
|
||||||
|
complete -c exa -l 'time-style' -x -d "How to format timestamps" -a "
|
||||||
|
default\t'Use the default time style'
|
||||||
|
iso\t'Display brief ISO timestamps'
|
||||||
|
long-iso\t'Display longer ISO timestaps, up to the minute'
|
||||||
|
full-iso\t'Display full ISO timestamps, up to the nanosecond'
|
||||||
|
"
|
||||||
|
|
||||||
# Optional extras
|
# Optional extras
|
||||||
complete -c exa -s 'g' -l 'git' -d "List each file's Git status, if tracked"
|
complete -c exa -s 'g' -l 'git' -d "List each file's Git status, if tracked"
|
||||||
|
@ -29,6 +29,7 @@ __exa() {
|
|||||||
{-m,--modified}"[Use the modified timestamp field]" \
|
{-m,--modified}"[Use the modified timestamp field]" \
|
||||||
{-S,--blocks}"[List each file's number of filesystem blocks]" \
|
{-S,--blocks}"[List each file's number of filesystem blocks]" \
|
||||||
{-t,--time}"[Which time field to show]:(time field):(accessed created modified)" \
|
{-t,--time}"[Which time field to show]:(time field):(accessed created modified)" \
|
||||||
|
--time-style"[How to format timestamps]:(time style):(default iso long-iso full-iso)" \
|
||||||
{-u,--accessed}"[Use the accessed timestamp field]" \
|
{-u,--accessed}"[Use the accessed timestamp field]" \
|
||||||
{-U,--created}"[Use the created timestamp field]" \
|
{-U,--created}"[Use the created timestamp field]" \
|
||||||
--git"[List each file's Git status, if tracked]" \
|
--git"[List each file's Git status, if tracked]" \
|
||||||
|
@ -145,6 +145,11 @@ which timestamp field to list (modified, accessed, created)
|
|||||||
.RS
|
.RS
|
||||||
.RE
|
.RE
|
||||||
.TP
|
.TP
|
||||||
|
.B \-\-time\-style=\f[I]STYLE\f[]
|
||||||
|
how to format timestamps (default, iso, long-iso, full-iso)
|
||||||
|
.RS
|
||||||
|
.RE
|
||||||
|
.TP
|
||||||
.B \-u, \-\-accessed
|
.B \-u, \-\-accessed
|
||||||
use the accessed timestamp field
|
use the accessed timestamp field
|
||||||
.RS
|
.RS
|
||||||
|
@ -166,7 +166,11 @@ pub struct DeviceIDs {
|
|||||||
|
|
||||||
|
|
||||||
/// One of a file’s timestamps (created, accessed, or modified).
|
/// One of a file’s timestamps (created, accessed, or modified).
|
||||||
pub struct Time(pub time_t);
|
#[derive(Copy, Clone)]
|
||||||
|
pub struct Time {
|
||||||
|
pub seconds: time_t,
|
||||||
|
pub nanoseconds: time_t,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// A file’s status in a Git repository. Whether a file is in a repository or
|
/// A file’s status in a Git repository. Whether a file is in a repository or
|
||||||
|
@ -273,23 +273,35 @@ impl<'dir> File<'dir> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This file’s last modified timestamp.
|
||||||
pub fn modified_time(&self) -> f::Time {
|
pub fn modified_time(&self) -> f::Time {
|
||||||
f::Time(self.metadata.mtime())
|
f::Time {
|
||||||
|
seconds: self.metadata.mtime(),
|
||||||
|
nanoseconds: self.metadata.mtime_nsec()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This file’s created timestamp.
|
||||||
pub fn created_time(&self) -> f::Time {
|
pub fn created_time(&self) -> f::Time {
|
||||||
f::Time(self.metadata.ctime())
|
f::Time {
|
||||||
|
seconds: self.metadata.ctime(),
|
||||||
|
nanoseconds: self.metadata.ctime_nsec()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This file’s last accessed timestamp.
|
||||||
pub fn accessed_time(&self) -> f::Time {
|
pub fn accessed_time(&self) -> f::Time {
|
||||||
f::Time(self.metadata.mtime())
|
f::Time {
|
||||||
|
seconds: self.metadata.atime(),
|
||||||
|
nanoseconds: self.metadata.atime_nsec()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This file's 'type'.
|
/// This file’s ‘type’.
|
||||||
///
|
///
|
||||||
/// This is used in the leftmost column of the permissions column.
|
/// This is used a the leftmost character of the permissions column.
|
||||||
/// Although the file type can usually be guessed from the colour of the
|
/// The file type can usually be guessed from the colour of the file, but
|
||||||
/// file, `ls` puts this character there, so people will expect it.
|
/// ls puts this character there.
|
||||||
pub fn type_char(&self) -> f::Type {
|
pub fn type_char(&self) -> f::Type {
|
||||||
if self.is_file() {
|
if self.is_file() {
|
||||||
f::Type::File
|
f::Type::File
|
||||||
@ -341,7 +353,7 @@ impl<'dir> File<'dir> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether this file's extension is any of the strings that get passed in.
|
/// Whether this file’s extension is any of the strings that get passed in.
|
||||||
///
|
///
|
||||||
/// This will always return `false` if the file has no extension.
|
/// This will always return `false` if the file has no extension.
|
||||||
pub fn extension_is_one_of(&self, choices: &[&str]) -> bool {
|
pub fn extension_is_one_of(&self, choices: &[&str]) -> bool {
|
||||||
|
@ -40,7 +40,8 @@ LONG VIEW OPTIONS
|
|||||||
-S, --blocks show number of file system blocks
|
-S, --blocks show number of file system blocks
|
||||||
-t, --time FIELD which timestamp field to list (modified, accessed, created)
|
-t, --time FIELD which timestamp field to list (modified, accessed, created)
|
||||||
-u, --accessed use the accessed timestamp field
|
-u, --accessed use the accessed timestamp field
|
||||||
-U, --created use the created timestamp field"##;
|
-U, --created use the created timestamp field
|
||||||
|
--time-style how to format timestamps (default, iso, long-iso, full-iso)"##;
|
||||||
|
|
||||||
static GIT_HELP: &str = r##" --git list each file's Git status, if tracked"##;
|
static GIT_HELP: &str = r##" --git list each file's Git status, if tracked"##;
|
||||||
static EXTENDED_HELP: &str = r##" -@, --extended list each file's extended attributes and sizes"##;
|
static EXTENDED_HELP: &str = r##" -@, --extended list each file's extended attributes and sizes"##;
|
||||||
|
@ -23,7 +23,7 @@ pub use self::view::{View, Mode};
|
|||||||
|
|
||||||
/// These **options** represent a parsed, error-checked versions of the
|
/// These **options** represent a parsed, error-checked versions of the
|
||||||
/// user’s command-line options.
|
/// user’s command-line options.
|
||||||
#[derive(PartialEq, Debug, Clone)]
|
#[derive(Debug)]
|
||||||
pub struct Options {
|
pub struct Options {
|
||||||
|
|
||||||
/// The action to perform when encountering a directory rather than a
|
/// The action to perform when encountering a directory rather than a
|
||||||
@ -88,6 +88,7 @@ impl Options {
|
|||||||
opts.optopt ("t", "time", "which timestamp field to show", "WORD");
|
opts.optopt ("t", "time", "which timestamp field to show", "WORD");
|
||||||
opts.optflag("u", "accessed", "use the accessed timestamp field");
|
opts.optflag("u", "accessed", "use the accessed timestamp field");
|
||||||
opts.optflag("U", "created", "use the created timestamp field");
|
opts.optflag("U", "created", "use the created timestamp field");
|
||||||
|
opts.optopt ("", "time-style", "how to format timestamp fields", "STYLE");
|
||||||
|
|
||||||
if cfg!(feature="git") {
|
if cfg!(feature="git") {
|
||||||
opts.optflag("", "git", "list each file's git status");
|
opts.optflag("", "git", "list each file's git status");
|
||||||
@ -124,8 +125,8 @@ impl Options {
|
|||||||
/// results will end up being displayed.
|
/// results will end up being displayed.
|
||||||
pub fn should_scan_for_git(&self) -> bool {
|
pub fn should_scan_for_git(&self) -> bool {
|
||||||
match self.view.mode {
|
match self.view.mode {
|
||||||
Mode::Details(details::Options { columns: Some(cols), .. }) |
|
Mode::Details(details::Options { table: Some(ref table), .. }) |
|
||||||
Mode::GridDetails(_, details::Options { columns: Some(cols), .. }) => cols.should_scan_for_git(),
|
Mode::GridDetails(_, details::Options { table: Some(ref table), .. }) => table.should_scan_for_git(),
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -201,13 +202,13 @@ mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
fn long_across() {
|
fn long_across() {
|
||||||
let opts = Options::getopts(&[ "--long", "--across" ]);
|
let opts = Options::getopts(&[ "--long", "--across" ]);
|
||||||
assert_eq!(opts, Err(Misfire::Useless("across", true, "long")))
|
assert_eq!(opts.unwrap_err(), Misfire::Useless("across", true, "long"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn oneline_across() {
|
fn oneline_across() {
|
||||||
let opts = Options::getopts(&[ "--oneline", "--across" ]);
|
let opts = Options::getopts(&[ "--oneline", "--across" ]);
|
||||||
assert_eq!(opts, Err(Misfire::Useless("across", true, "oneline")))
|
assert_eq!(opts.unwrap_err(), Misfire::Useless("across", true, "oneline"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -4,14 +4,15 @@ use getopts;
|
|||||||
|
|
||||||
use output::Colours;
|
use output::Colours;
|
||||||
use output::{grid, details};
|
use output::{grid, details};
|
||||||
use output::column::{Columns, TimeTypes, SizeFormat};
|
use output::table::{TimeTypes, Environment, SizeFormat, Options as TableOptions};
|
||||||
use output::file_name::Classify;
|
use output::file_name::Classify;
|
||||||
|
use output::time::TimeFormat;
|
||||||
use options::Misfire;
|
use options::Misfire;
|
||||||
use fs::feature::xattr;
|
use fs::feature::xattr;
|
||||||
|
|
||||||
|
|
||||||
/// The **view** contains all information about how to format output.
|
/// The **view** contains all information about how to format output.
|
||||||
#[derive(PartialEq, Debug, Clone)]
|
#[derive(Debug)]
|
||||||
pub struct View {
|
pub struct View {
|
||||||
pub mode: Mode,
|
pub mode: Mode,
|
||||||
pub colours: Colours,
|
pub colours: Colours,
|
||||||
@ -31,7 +32,7 @@ impl View {
|
|||||||
|
|
||||||
|
|
||||||
/// The **mode** is the “type” of output.
|
/// The **mode** is the “type” of output.
|
||||||
#[derive(PartialEq, Debug, Clone)]
|
#[derive(Debug)]
|
||||||
pub enum Mode {
|
pub enum Mode {
|
||||||
Grid(grid::Options),
|
Grid(grid::Options),
|
||||||
Details(details::Options),
|
Details(details::Options),
|
||||||
@ -54,7 +55,7 @@ impl Mode {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Ok(details::Options {
|
Ok(details::Options {
|
||||||
columns: Some(Columns::deduce(matches)?),
|
table: Some(TableOptions::deduce(matches)?),
|
||||||
header: matches.opt_present("header"),
|
header: matches.opt_present("header"),
|
||||||
xattr: xattr::ENABLED && matches.opt_present("extended"),
|
xattr: xattr::ENABLED && matches.opt_present("extended"),
|
||||||
})
|
})
|
||||||
@ -94,7 +95,7 @@ impl Mode {
|
|||||||
}
|
}
|
||||||
else if matches.opt_present("tree") {
|
else if matches.opt_present("tree") {
|
||||||
let details = details::Options {
|
let details = details::Options {
|
||||||
columns: None,
|
table: None,
|
||||||
header: false,
|
header: false,
|
||||||
xattr: false,
|
xattr: false,
|
||||||
};
|
};
|
||||||
@ -117,7 +118,7 @@ impl Mode {
|
|||||||
|
|
||||||
if matches.opt_present("tree") {
|
if matches.opt_present("tree") {
|
||||||
let details = details::Options {
|
let details = details::Options {
|
||||||
columns: None,
|
table: None,
|
||||||
header: false,
|
header: false,
|
||||||
xattr: false,
|
xattr: false,
|
||||||
};
|
};
|
||||||
@ -194,9 +195,11 @@ impl TerminalWidth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl Columns {
|
impl TableOptions {
|
||||||
fn deduce(matches: &getopts::Matches) -> Result<Columns, Misfire> {
|
fn deduce(matches: &getopts::Matches) -> Result<Self, Misfire> {
|
||||||
Ok(Columns {
|
Ok(TableOptions {
|
||||||
|
env: Environment::load_all(),
|
||||||
|
time_format: TimeFormat::deduce(matches)?,
|
||||||
size_format: SizeFormat::deduce(matches)?,
|
size_format: SizeFormat::deduce(matches)?,
|
||||||
time_types: TimeTypes::deduce(matches)?,
|
time_types: TimeTypes::deduce(matches)?,
|
||||||
inode: matches.opt_present("inode"),
|
inode: matches.opt_present("inode"),
|
||||||
@ -233,6 +236,29 @@ impl SizeFormat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl TimeFormat {
|
||||||
|
|
||||||
|
/// Determine how time should be formatted in timestamp columns.
|
||||||
|
fn deduce(matches: &getopts::Matches) -> Result<TimeFormat, Misfire> {
|
||||||
|
pub use output::time::{DefaultFormat, ISOFormat};
|
||||||
|
const STYLES: &[&str] = &["default", "long-iso", "full-iso", "iso"];
|
||||||
|
|
||||||
|
if let Some(word) = matches.opt_str("time-style") {
|
||||||
|
match &*word {
|
||||||
|
"default" => Ok(TimeFormat::DefaultFormat(DefaultFormat::new())),
|
||||||
|
"iso" => Ok(TimeFormat::ISOFormat(ISOFormat::new())),
|
||||||
|
"long-iso" => Ok(TimeFormat::LongISO),
|
||||||
|
"full-iso" => Ok(TimeFormat::FullISO),
|
||||||
|
otherwise => Err(Misfire::bad_argument("time-style", otherwise, STYLES)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Ok(TimeFormat::DefaultFormat(DefaultFormat::new()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
impl TimeTypes {
|
impl TimeTypes {
|
||||||
|
|
||||||
/// Determine which of a file’s time fields should be displayed for it
|
/// Determine which of a file’s time fields should be displayed for it
|
||||||
|
@ -1,193 +0,0 @@
|
|||||||
use fs::Dir;
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
|
||||||
pub enum Column {
|
|
||||||
Permissions,
|
|
||||||
FileSize(SizeFormat),
|
|
||||||
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 {
|
|
||||||
Column::FileSize(_)
|
|
||||||
| 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",
|
|
||||||
Column::FileSize(_) => "Size",
|
|
||||||
Column::Timestamp(t) => t.header(),
|
|
||||||
Column::Blocks => "Blocks",
|
|
||||||
Column::User => "User",
|
|
||||||
Column::Group => "Group",
|
|
||||||
Column::HardLinks => "Links",
|
|
||||||
Column::Inode => "inode",
|
|
||||||
Column::GitStatus => "Git",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(PartialEq, Copy, Clone, Debug, Default)]
|
|
||||||
pub struct Columns {
|
|
||||||
pub size_format: SizeFormat,
|
|
||||||
pub time_types: TimeTypes,
|
|
||||||
pub inode: bool,
|
|
||||||
pub links: bool,
|
|
||||||
pub blocks: bool,
|
|
||||||
pub group: bool,
|
|
||||||
pub git: bool
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Columns {
|
|
||||||
pub fn should_scan_for_git(&self) -> bool {
|
|
||||||
self.git
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn for_dir(&self, dir: Option<&Dir>) -> Vec<Column> {
|
|
||||||
let mut columns = vec![];
|
|
||||||
|
|
||||||
if self.inode {
|
|
||||||
columns.push(Column::Inode);
|
|
||||||
}
|
|
||||||
|
|
||||||
columns.push(Column::Permissions);
|
|
||||||
|
|
||||||
if self.links {
|
|
||||||
columns.push(Column::HardLinks);
|
|
||||||
}
|
|
||||||
|
|
||||||
columns.push(Column::FileSize(self.size_format));
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg!(feature="git") {
|
|
||||||
if let Some(d) = dir {
|
|
||||||
if self.should_scan_for_git() && d.has_git_repo() {
|
|
||||||
columns.push(Column::GitStatus);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
columns
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// 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 }
|
|
||||||
}
|
|
||||||
}
|
|
@ -68,11 +68,10 @@ use fs::{Dir, File};
|
|||||||
use fs::feature::xattr::{Attribute, FileAttributes};
|
use fs::feature::xattr::{Attribute, FileAttributes};
|
||||||
use options::{FileFilter, RecurseOptions};
|
use options::{FileFilter, RecurseOptions};
|
||||||
use output::colours::Colours;
|
use output::colours::Colours;
|
||||||
use output::column::Columns;
|
|
||||||
use output::cell::TextCell;
|
use output::cell::TextCell;
|
||||||
use output::tree::{TreeTrunk, TreeParams, TreeDepth};
|
use output::tree::{TreeTrunk, TreeParams, TreeDepth};
|
||||||
use output::file_name::{FileName, LinkStyle, Classify};
|
use output::file_name::{FileName, LinkStyle, Classify};
|
||||||
use output::table::{Table, Environment, Row as TableRow};
|
use output::table::{Table, Options as TableOptions, Row as TableRow};
|
||||||
|
|
||||||
|
|
||||||
/// With the **Details** view, the output gets formatted into columns, with
|
/// With the **Details** view, the output gets formatted into columns, with
|
||||||
@ -86,13 +85,14 @@ use output::table::{Table, Environment, Row as TableRow};
|
|||||||
///
|
///
|
||||||
/// Almost all the heavy lifting is done in a Table object, which handles the
|
/// Almost all the heavy lifting is done in a Table object, which handles the
|
||||||
/// columns for each row.
|
/// columns for each row.
|
||||||
#[derive(PartialEq, Debug, Clone, Default)]
|
#[derive(Debug)]
|
||||||
pub struct Options {
|
pub struct Options {
|
||||||
|
|
||||||
/// A Columns object that says which columns should be included in the
|
/// Options specific to drawing a table.
|
||||||
/// output in the general case. Directories themselves can pick which
|
///
|
||||||
/// columns are *added* to this list, such as the Git column.
|
/// Directories themselves can pick which columns are *added* to this
|
||||||
pub columns: Option<Columns>,
|
/// list, such as the Git column.
|
||||||
|
pub table: Option<TableOptions>,
|
||||||
|
|
||||||
/// Whether to show a header line or not.
|
/// Whether to show a header line or not.
|
||||||
pub header: bool,
|
pub header: bool,
|
||||||
@ -139,10 +139,8 @@ 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();
|
let mut rows = Vec::new();
|
||||||
|
|
||||||
if let Some(columns) = self.opts.columns {
|
if let Some(ref table) = self.opts.table {
|
||||||
let env = Environment::default();
|
let mut table = Table::new(&table, self.dir, &self.colours);
|
||||||
let colz = columns.for_dir(self.dir);
|
|
||||||
let mut table = Table::new(&colz, &self.colours, &env);
|
|
||||||
|
|
||||||
if self.opts.header {
|
if self.opts.header {
|
||||||
let header = table.header_row();
|
let header = table.header_row();
|
||||||
|
@ -8,12 +8,11 @@ use fs::feature::xattr::FileAttributes;
|
|||||||
|
|
||||||
use options::FileFilter;
|
use options::FileFilter;
|
||||||
use output::cell::TextCell;
|
use output::cell::TextCell;
|
||||||
use output::column::Column;
|
|
||||||
use output::colours::Colours;
|
use output::colours::Colours;
|
||||||
use output::details::{Options as DetailsOptions, Row as DetailsRow, Render as DetailsRender};
|
use output::details::{Options as DetailsOptions, Row as DetailsRow, Render as DetailsRender};
|
||||||
use output::grid::Options as GridOptions;
|
use output::grid::Options as GridOptions;
|
||||||
use output::file_name::{FileName, LinkStyle, Classify};
|
use output::file_name::{FileName, LinkStyle, Classify};
|
||||||
use output::table::{Table, Environment, Row as TableRow};
|
use output::table::{Table, Row as TableRow, Options as TableOptions};
|
||||||
use output::tree::{TreeParams, TreeDepth};
|
use output::tree::{TreeParams, TreeDepth};
|
||||||
|
|
||||||
|
|
||||||
@ -42,16 +41,11 @@ 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 columns_for_dir = match self.details.columns {
|
let options = self.details.table.as_ref().expect("Details table options not given!");
|
||||||
Some(cols) => cols.for_dir(self.dir),
|
|
||||||
None => Vec::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let env = Environment::default();
|
|
||||||
|
|
||||||
let drender = self.clone().details();
|
let drender = self.clone().details();
|
||||||
|
|
||||||
let (first_table, _) = self.make_table(&env, &columns_for_dir, &drender);
|
let (first_table, _) = self.make_table(options, &drender);
|
||||||
|
|
||||||
let rows = self.files.iter()
|
let rows = self.files.iter()
|
||||||
.map(|file| first_table.row_for_file(file, file_has_xattrs(file)))
|
.map(|file| first_table.row_for_file(file, file_has_xattrs(file)))
|
||||||
@ -61,10 +55,10 @@ impl<'a> Render<'a> {
|
|||||||
.map(|file| FileName::new(file, LinkStyle::JustFilenames, self.classify, self.colours).paint().promote())
|
.map(|file| FileName::new(file, LinkStyle::JustFilenames, self.classify, self.colours).paint().promote())
|
||||||
.collect::<Vec<TextCell>>();
|
.collect::<Vec<TextCell>>();
|
||||||
|
|
||||||
let mut last_working_table = self.make_grid(&env, 1, &columns_for_dir, &file_names, rows.clone(), &drender);
|
let mut last_working_table = self.make_grid(1, options, &file_names, rows.clone(), &drender);
|
||||||
|
|
||||||
for column_count in 2.. {
|
for column_count in 2.. {
|
||||||
let grid = self.make_grid(&env, column_count, &columns_for_dir, &file_names, rows.clone(), &drender);
|
let grid = self.make_grid(column_count, options, &file_names, rows.clone(), &drender);
|
||||||
|
|
||||||
let the_grid_fits = {
|
let the_grid_fits = {
|
||||||
let d = grid.fit_into_columns(column_count);
|
let d = grid.fit_into_columns(column_count);
|
||||||
@ -82,8 +76,8 @@ impl<'a> Render<'a> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_table<'t>(&'a self, env: &'a Environment, columns_for_dir: &'a [Column], drender: &DetailsRender) -> (Table<'a>, Vec<DetailsRow>) {
|
fn make_table<'t>(&'a self, options: &'a TableOptions, drender: &DetailsRender) -> (Table<'a>, Vec<DetailsRow>) {
|
||||||
let mut table = Table::new(columns_for_dir, self.colours, env);
|
let mut table = Table::new(options, self.dir, self.colours);
|
||||||
let mut rows = Vec::new();
|
let mut rows = Vec::new();
|
||||||
|
|
||||||
if self.details.header {
|
if self.details.header {
|
||||||
@ -95,11 +89,11 @@ impl<'a> Render<'a> {
|
|||||||
(table, rows)
|
(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 {
|
fn make_grid(&'a self, column_count: usize, options: &'a TableOptions, file_names: &[TextCell], rows: Vec<TableRow>, drender: &DetailsRender) -> grid::Grid {
|
||||||
|
|
||||||
let mut tables = Vec::new();
|
let mut tables = Vec::new();
|
||||||
for _ in 0 .. column_count {
|
for _ in 0 .. column_count {
|
||||||
tables.push(self.make_table(env.clone(), columns_for_dir, drender));
|
tables.push(self.make_table(options, drender));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut num_cells = rows.len();
|
let mut num_cells = rows.len();
|
||||||
|
@ -2,12 +2,12 @@ pub use self::cell::{TextCell, TextCellContents, DisplayWidth};
|
|||||||
pub use self::colours::Colours;
|
pub use self::colours::Colours;
|
||||||
pub use self::escape::escape;
|
pub use self::escape::escape;
|
||||||
|
|
||||||
pub mod column;
|
|
||||||
pub mod details;
|
pub mod details;
|
||||||
pub mod file_name;
|
pub mod file_name;
|
||||||
pub mod grid_details;
|
pub mod grid_details;
|
||||||
pub mod grid;
|
pub mod grid;
|
||||||
pub mod lines;
|
pub mod lines;
|
||||||
|
pub mod table;
|
||||||
pub mod time;
|
pub mod time;
|
||||||
|
|
||||||
mod cell;
|
mod cell;
|
||||||
@ -15,4 +15,3 @@ mod colours;
|
|||||||
mod escape;
|
mod escape;
|
||||||
mod render;
|
mod render;
|
||||||
mod tree;
|
mod tree;
|
||||||
mod table;
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use fs::fields as f;
|
use fs::fields as f;
|
||||||
use output::column::SizeFormat;
|
|
||||||
use output::cell::{TextCell, DisplayWidth};
|
use output::cell::{TextCell, DisplayWidth};
|
||||||
use output::colours::Colours;
|
use output::colours::Colours;
|
||||||
|
use output::table::SizeFormat;
|
||||||
use locale;
|
use locale;
|
||||||
|
|
||||||
|
|
||||||
@ -68,8 +68,8 @@ impl f::DeviceIDs {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod test {
|
pub mod test {
|
||||||
use output::colours::Colours;
|
use output::colours::Colours;
|
||||||
use output::column::SizeFormat;
|
|
||||||
use output::cell::{TextCell, DisplayWidth};
|
use output::cell::{TextCell, DisplayWidth};
|
||||||
|
use output::table::SizeFormat;
|
||||||
use fs::fields as f;
|
use fs::fields as f;
|
||||||
|
|
||||||
use locale;
|
use locale;
|
||||||
|
@ -6,18 +6,17 @@ use output::colours::Colours;
|
|||||||
use output::time::TimeFormat;
|
use output::time::TimeFormat;
|
||||||
|
|
||||||
|
|
||||||
#[allow(trivial_numeric_casts)]
|
|
||||||
impl f::Time {
|
impl f::Time {
|
||||||
pub fn render(&self, colours: &Colours,
|
pub fn render(self, colours: &Colours,
|
||||||
tz: &Option<TimeZone>,
|
tz: &Option<TimeZone>,
|
||||||
style: &TimeFormat) -> TextCell {
|
style: &TimeFormat) -> TextCell {
|
||||||
|
|
||||||
if let Some(ref tz) = *tz {
|
if let Some(ref tz) = *tz {
|
||||||
let datestamp = style.format_zoned(self.0 as i64, tz);
|
let datestamp = style.format_zoned(self, tz);
|
||||||
TextCell::paint(colours.date, datestamp)
|
TextCell::paint(colours.date, datestamp)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
let datestamp = style.format_local(self.0 as i64);
|
let datestamp = style.format_local(self);
|
||||||
TextCell::paint(colours.date, datestamp)
|
TextCell::paint(colours.date, datestamp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use std::cmp::max;
|
use std::cmp::max;
|
||||||
|
use std::fmt;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::sync::{Mutex, MutexGuard};
|
use std::sync::{Mutex, MutexGuard};
|
||||||
|
|
||||||
@ -11,10 +12,214 @@ use users::UsersCache;
|
|||||||
|
|
||||||
use output::cell::TextCell;
|
use output::cell::TextCell;
|
||||||
use output::colours::Colours;
|
use output::colours::Colours;
|
||||||
use output::column::{Alignment, Column};
|
|
||||||
use output::time::TimeFormat;
|
use output::time::TimeFormat;
|
||||||
|
|
||||||
use fs::{File, fields as f};
|
use fs::{File, Dir, fields as f};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Options for displaying a table.
|
||||||
|
pub struct Options {
|
||||||
|
pub env: Environment,
|
||||||
|
pub size_format: SizeFormat,
|
||||||
|
pub time_format: TimeFormat,
|
||||||
|
pub time_types: TimeTypes,
|
||||||
|
pub inode: bool,
|
||||||
|
pub links: bool,
|
||||||
|
pub blocks: bool,
|
||||||
|
pub group: bool,
|
||||||
|
pub git: bool
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for Options {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
|
// I had to make other types derive Debug,
|
||||||
|
// and Mutex<UsersCache> is not that!
|
||||||
|
writeln!(f, "<table options>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Options {
|
||||||
|
pub fn should_scan_for_git(&self) -> bool {
|
||||||
|
self.git
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn for_dir(&self, dir: Option<&Dir>) -> Vec<Column> {
|
||||||
|
let mut columns = vec![];
|
||||||
|
|
||||||
|
if self.inode {
|
||||||
|
columns.push(Column::Inode);
|
||||||
|
}
|
||||||
|
|
||||||
|
columns.push(Column::Permissions);
|
||||||
|
|
||||||
|
if self.links {
|
||||||
|
columns.push(Column::HardLinks);
|
||||||
|
}
|
||||||
|
|
||||||
|
columns.push(Column::FileSize(self.size_format));
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg!(feature="git") {
|
||||||
|
if let Some(d) = dir {
|
||||||
|
if self.should_scan_for_git() && d.has_git_repo() {
|
||||||
|
columns.push(Column::GitStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
columns
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// A table contains these.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Column {
|
||||||
|
Permissions,
|
||||||
|
FileSize(SizeFormat),
|
||||||
|
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 {
|
||||||
|
Column::FileSize(_)
|
||||||
|
| 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",
|
||||||
|
Column::FileSize(_) => "Size",
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// The **environment** struct contains any data that could change between
|
/// The **environment** struct contains any data that could change between
|
||||||
@ -26,9 +231,6 @@ pub struct Environment {
|
|||||||
/// Localisation rules for formatting numbers.
|
/// Localisation rules for formatting numbers.
|
||||||
numeric: locale::Numeric,
|
numeric: locale::Numeric,
|
||||||
|
|
||||||
/// Rules for formatting timestamps.
|
|
||||||
time_format: TimeFormat,
|
|
||||||
|
|
||||||
/// The computer's current time zone. This gets used to determine how to
|
/// The computer's current time zone. This gets used to determine how to
|
||||||
/// offset files' timestamps.
|
/// offset files' timestamps.
|
||||||
tz: Option<TimeZone>,
|
tz: Option<TimeZone>,
|
||||||
@ -41,10 +243,8 @@ impl Environment {
|
|||||||
pub fn lock_users(&self) -> MutexGuard<UsersCache> {
|
pub fn lock_users(&self) -> MutexGuard<UsersCache> {
|
||||||
self.users.lock().unwrap()
|
self.users.lock().unwrap()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Environment {
|
pub fn load_all() -> Self {
|
||||||
fn default() -> Self {
|
|
||||||
let tz = match determine_time_zone() {
|
let tz = match determine_time_zone() {
|
||||||
Ok(t) => Some(t),
|
Ok(t) => Some(t),
|
||||||
Err(ref e) => {
|
Err(ref e) => {
|
||||||
@ -53,14 +253,12 @@ impl Default for Environment {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let time_format = TimeFormat::deduce();
|
|
||||||
|
|
||||||
let numeric = locale::Numeric::load_user_locale()
|
let numeric = locale::Numeric::load_user_locale()
|
||||||
.unwrap_or_else(|_| locale::Numeric::english());
|
.unwrap_or_else(|_| locale::Numeric::english());
|
||||||
|
|
||||||
let users = Mutex::new(UsersCache::new());
|
let users = Mutex::new(UsersCache::new());
|
||||||
|
|
||||||
Environment { tz, time_format, numeric, users }
|
Environment { tz, numeric, users }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,10 +271,11 @@ fn determine_time_zone() -> TZResult<TimeZone> {
|
|||||||
|
|
||||||
|
|
||||||
pub struct Table<'a> {
|
pub struct Table<'a> {
|
||||||
columns: &'a [Column],
|
columns: Vec<Column>,
|
||||||
colours: &'a Colours,
|
colours: &'a Colours,
|
||||||
env: &'a Environment,
|
env: &'a Environment,
|
||||||
widths: TableWidths,
|
widths: TableWidths,
|
||||||
|
time_format: &'a TimeFormat,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -85,9 +284,10 @@ pub struct Row {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'f> Table<'a> {
|
impl<'a, 'f> Table<'a> {
|
||||||
pub fn new(columns: &'a [Column], colours: &'a Colours, env: &'a Environment) -> Table<'a> {
|
pub fn new(options: &'a Options, dir: Option<&'a Dir>, colours: &'a Colours) -> Table<'a> {
|
||||||
let widths = TableWidths::zero(columns.len());
|
let colz = options.for_dir(dir);
|
||||||
Table { columns, colours, env, widths }
|
let widths = TableWidths::zero(colz.len());
|
||||||
|
Table { columns: colz, colours, env: &options.env, widths, time_format: &options.time_format }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn widths(&self) -> &TableWidths {
|
pub fn widths(&self) -> &TableWidths {
|
||||||
@ -123,7 +323,7 @@ impl<'a, 'f> Table<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn display(&self, file: &File, column: &Column, xattrs: bool) -> TextCell {
|
fn display(&self, file: &File, column: &Column, xattrs: bool) -> TextCell {
|
||||||
use output::column::TimeType::*;
|
use output::table::TimeType::*;
|
||||||
|
|
||||||
match *column {
|
match *column {
|
||||||
Column::Permissions => self.permissions_plus(file, xattrs).render(&self.colours),
|
Column::Permissions => self.permissions_plus(file, xattrs).render(&self.colours),
|
||||||
@ -135,9 +335,9 @@ impl<'a, 'f> Table<'a> {
|
|||||||
Column::Group => file.group().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::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(Modified) => file.modified_time().render(&self.colours, &self.env.tz, &self.time_format),
|
||||||
Column::Timestamp(Created) => file.created_time().render( &self.colours, &self.env.tz, &self.env.time_format),
|
Column::Timestamp(Created) => file.created_time().render( &self.colours, &self.env.tz, &self.time_format),
|
||||||
Column::Timestamp(Accessed) => file.accessed_time().render(&self.colours, &self.env.tz, &self.env.time_format),
|
Column::Timestamp(Accessed) => file.accessed_time().render(&self.colours, &self.env.tz, &self.time_format),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,40 @@
|
|||||||
use datetime::{LocalDateTime, TimeZone, DatePiece};
|
use datetime::{LocalDateTime, TimeZone, DatePiece, TimePiece};
|
||||||
use datetime::fmt::DateFormat;
|
use datetime::fmt::DateFormat;
|
||||||
use locale;
|
use locale;
|
||||||
|
|
||||||
use fs::fields::time_t;
|
use fs::fields::Time;
|
||||||
|
|
||||||
|
|
||||||
|
pub enum TimeFormat {
|
||||||
|
DefaultFormat(DefaultFormat),
|
||||||
|
ISOFormat(ISOFormat),
|
||||||
|
LongISO,
|
||||||
|
FullISO,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimeFormat {
|
||||||
|
pub fn format_local(&self, time: Time) -> String {
|
||||||
|
match *self {
|
||||||
|
TimeFormat::DefaultFormat(ref fmt) => fmt.format_local(time),
|
||||||
|
TimeFormat::ISOFormat(ref iso) => iso.format_local(time),
|
||||||
|
TimeFormat::LongISO => long_local(time),
|
||||||
|
TimeFormat::FullISO => full_local(time),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_zoned(&self, time: Time, zone: &TimeZone) -> String {
|
||||||
|
match *self {
|
||||||
|
TimeFormat::DefaultFormat(ref fmt) => fmt.format_zoned(time, zone),
|
||||||
|
TimeFormat::ISOFormat(ref iso) => iso.format_zoned(time, zone),
|
||||||
|
TimeFormat::LongISO => long_zoned(time, zone),
|
||||||
|
TimeFormat::FullISO => full_zoned(time, zone),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct TimeFormat {
|
pub struct DefaultFormat {
|
||||||
|
|
||||||
/// The year of the current time. This gets used to determine which date
|
/// The year of the current time. This gets used to determine which date
|
||||||
/// format to use.
|
/// format to use.
|
||||||
@ -22,36 +50,8 @@ pub struct TimeFormat {
|
|||||||
pub date_and_year: DateFormat<'static>,
|
pub date_and_year: DateFormat<'static>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TimeFormat {
|
impl DefaultFormat {
|
||||||
fn is_recent(&self, date: LocalDateTime) -> bool {
|
pub fn new() -> DefaultFormat {
|
||||||
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;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
let locale = locale::Time::load_user_locale()
|
let locale = locale::Time::load_user_locale()
|
||||||
@ -74,6 +74,124 @@ impl TimeFormat {
|
|||||||
_ => DateFormat::parse("{2>:D} {:M} {5>:Y}").unwrap()
|
_ => DateFormat::parse("{2>:D} {:M} {5>:Y}").unwrap()
|
||||||
};
|
};
|
||||||
|
|
||||||
TimeFormat { current_year, locale, date_and_time, date_and_year }
|
DefaultFormat { current_year, locale, date_and_time, date_and_year }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_recent(&self, date: LocalDateTime) -> bool {
|
||||||
|
date.year() == self.current_year
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(trivial_numeric_casts)]
|
||||||
|
fn format_local(&self, time: Time) -> String {
|
||||||
|
let date = LocalDateTime::at(time.seconds 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)]
|
||||||
|
fn format_zoned(&self, time: Time, zone: &TimeZone) -> String {
|
||||||
|
let date = zone.to_zoned(LocalDateTime::at(time.seconds 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)]
|
||||||
|
fn long_local(time: Time) -> String {
|
||||||
|
let date = LocalDateTime::at(time.seconds as i64);
|
||||||
|
format!("{:04}-{:02}-{:02} {:02}:{:02}",
|
||||||
|
date.year(), date.month() as usize, date.day(),
|
||||||
|
date.hour(), date.minute())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(trivial_numeric_casts)]
|
||||||
|
fn long_zoned(time: Time, zone: &TimeZone) -> String {
|
||||||
|
let date = zone.to_zoned(LocalDateTime::at(time.seconds as i64));
|
||||||
|
format!("{:04}-{:02}-{:02} {:02}:{:02}",
|
||||||
|
date.year(), date.month() as usize, date.day(),
|
||||||
|
date.hour(), date.minute())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[allow(trivial_numeric_casts)]
|
||||||
|
fn full_local(time: Time) -> String {
|
||||||
|
let date = LocalDateTime::at(time.seconds as i64);
|
||||||
|
format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:09}",
|
||||||
|
date.year(), date.month() as usize, date.day(),
|
||||||
|
date.hour(), date.minute(), date.second(), time.nanoseconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(trivial_numeric_casts)]
|
||||||
|
fn full_zoned(time: Time, zone: &TimeZone) -> String {
|
||||||
|
use datetime::Offset;
|
||||||
|
|
||||||
|
let local = LocalDateTime::at(time.seconds as i64);
|
||||||
|
let date = zone.to_zoned(local);
|
||||||
|
let offset = Offset::of_seconds(zone.offset(local) as i32).expect("Offset out of range");
|
||||||
|
format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:09} {:+03}{:02}",
|
||||||
|
date.year(), date.month() as usize, date.day(),
|
||||||
|
date.hour(), date.minute(), date.second(), time.nanoseconds,
|
||||||
|
offset.hours(), offset.minutes().abs())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ISOFormat {
|
||||||
|
|
||||||
|
/// The year of the current time. This gets used to determine which date
|
||||||
|
/// format to use.
|
||||||
|
pub current_year: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ISOFormat {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let current_year = LocalDateTime::now().year();
|
||||||
|
ISOFormat { current_year }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_recent(&self, date: LocalDateTime) -> bool {
|
||||||
|
date.year() == self.current_year
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(trivial_numeric_casts)]
|
||||||
|
fn format_local(&self, time: Time) -> String {
|
||||||
|
let date = LocalDateTime::at(time.seconds as i64);
|
||||||
|
|
||||||
|
if self.is_recent(date) {
|
||||||
|
format!("{:04}-{:02}-{:02}",
|
||||||
|
date.year(), date.month() as usize, date.day())
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
format!("{:02}-{:02} {:02}:{:02}",
|
||||||
|
date.month() as usize, date.day(),
|
||||||
|
date.hour(), date.minute())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(trivial_numeric_casts)]
|
||||||
|
fn format_zoned(&self, time: Time, zone: &TimeZone) -> String {
|
||||||
|
let date = zone.to_zoned(LocalDateTime::at(time.seconds as i64));
|
||||||
|
|
||||||
|
if self.is_recent(date) {
|
||||||
|
format!("{:04}-{:02}-{:02}",
|
||||||
|
date.year(), date.month() as usize, date.day())
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
format!("{:02}-{:02} {:02}:{:02}",
|
||||||
|
date.month() as usize, date.day(),
|
||||||
|
date.hour(), date.minute())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
4
xtests/dates_accessed
Normal file
4
xtests/dates_accessed
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[4mPermissions[0m [4mSize[0m [4mUser[0m [4mDate Accessed[0m [4mName[0m
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 3 Mar 2003[0m plum
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m15 Jun 2006[0m pear
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m22 Jul 2009[0m peach
|
3
xtests/dates_full_iso
Normal file
3
xtests/dates_full_iso
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m2006-06-15 23:14:29.000000000 +0000[0m peach
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m2003-03-03 00:00:00.000000000 +0000[0m pear
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m2009-07-22 10:38:53.000000000 +0000[0m plum
|
3
xtests/dates_iso
Normal file
3
xtests/dates_iso
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m06-15 23:14[0m peach
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m03-03 00:00[0m pear
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m07-22 10:38[0m plum
|
3
xtests/dates_long_iso
Normal file
3
xtests/dates_long_iso
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m2006-06-15 23:14[0m peach
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m2003-03-03 00:00[0m pear
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m2009-07-22 10:38[0m plum
|
4
xtests/dates_modified
Normal file
4
xtests/dates_modified
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[4mPermissions[0m [4mSize[0m [4mUser[0m [4mDate Modified[0m [4mName[0m
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 3 Mar 2003[0m pear
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m15 Jun 2006[0m peach
|
||||||
|
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m22 Jul 2009[0m plum
|
@ -38,5 +38,6 @@ LONG VIEW OPTIONS
|
|||||||
-t, --time FIELD which timestamp field to list (modified, accessed, created)
|
-t, --time FIELD which timestamp field to list (modified, accessed, created)
|
||||||
-u, --accessed use the accessed timestamp field
|
-u, --accessed use the accessed timestamp field
|
||||||
-U, --created use the created timestamp field
|
-U, --created use the created timestamp field
|
||||||
|
--time-style how to format timestamps (default, iso, long-iso, full-iso)
|
||||||
--git list each file's Git status, if tracked
|
--git list each file's Git status, if tracked
|
||||||
-@, --extended list each file's extended attributes and sizes
|
-@, --extended list each file's extended attributes and sizes
|
||||||
|
@ -14,5 +14,6 @@ LONG VIEW OPTIONS
|
|||||||
-t, --time FIELD which timestamp field to list (modified, accessed, created)
|
-t, --time FIELD which timestamp field to list (modified, accessed, created)
|
||||||
-u, --accessed use the accessed timestamp field
|
-u, --accessed use the accessed timestamp field
|
||||||
-U, --created use the created timestamp field
|
-U, --created use the created timestamp field
|
||||||
|
--time-style how to format timestamps (default, iso, long-iso, full-iso)
|
||||||
--git list each file's Git status, if tracked
|
--git list each file's Git status, if tracked
|
||||||
-@, --extended list each file's extended attributes and sizes
|
-@, --extended list each file's extended attributes and sizes
|
||||||
|
@ -107,6 +107,14 @@ $exa $testcases/file-names-exts/music.* -I "*.OGG" -1 2>&1 | diff -q - $re
|
|||||||
$exa $testcases/file-names-exts/music.* -I "*.OGG|*.mp3" -1 2>&1 | diff -q - $results/empty || exit 1
|
$exa $testcases/file-names-exts/music.* -I "*.OGG|*.mp3" -1 2>&1 | diff -q - $results/empty || exit 1
|
||||||
|
|
||||||
|
|
||||||
|
# Dates and times
|
||||||
|
$exa $testcases/dates -lh --accessed --sort=accessed 2>&1 | diff -q - $results/dates_accessed || exit 1
|
||||||
|
$exa $testcases/dates -lh --sort=modified 2>&1 | diff -q - $results/dates_modified || exit 1
|
||||||
|
$exa $testcases/dates -l --time-style=long-iso 2>&1 | diff -q - $results/dates_long_iso || exit 1
|
||||||
|
$exa $testcases/dates -l --time-style=full-iso 2>&1 | diff -q - $results/dates_full_iso || exit 1
|
||||||
|
$exa $testcases/dates -l --time-style=iso 2>&1 | diff -q - $results/dates_iso || exit 1
|
||||||
|
|
||||||
|
|
||||||
# Paths and directories
|
# Paths and directories
|
||||||
# These directories are created in the VM user’s home directory (the default
|
# These directories are created in the VM user’s home directory (the default
|
||||||
# location) when a Cargo build is done.
|
# location) when a Cargo build is done.
|
||||||
|
Loading…
Reference in New Issue
Block a user