diff --git a/Cargo.lock b/Cargo.lock index e0370bf..17e25e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,10 +3,13 @@ name = "exa" version = "0.1.0" dependencies = [ "ansi_term 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "datetime 0.1.2 (git+https://github.com/ogham/rust-datetime.git)", + "datetime_macros 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "getopts 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "git2 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", "natord 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "number_prefix 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "pad 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "users 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -21,7 +24,41 @@ dependencies = [ [[package]] name = "bitflags" -version = "0.1.0" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "datetime" +version = "0.1.2" +source = "git+https://github.com/ogham/rust-datetime.git#c108628eb3519535821bc7907f2305643465acf5" +dependencies = [ + "pad 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", + "regex_macros 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "datetime" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "pad 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", + "regex_macros 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "datetime_macros" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "datetime 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "pad 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "gcc" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -29,7 +66,7 @@ name = "getopts" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "log 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -37,7 +74,7 @@ name = "git2" version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "bitflags 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "libgit2-sys 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", "url 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -54,7 +91,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "libssh2-sys 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "libz-sys 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "openssl-sys 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", "pkg-config 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -72,7 +109,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "libz-sys 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "openssl-sys 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", "pkg-config 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -86,7 +123,7 @@ dependencies = [ [[package]] name = "log" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -106,18 +143,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "openssl-sys" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ + "gcc 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", "libressl-pnacl-sys 2.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "pkg-config 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "pad" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "pkg-config" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "pkg-config" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "pnacl-build-helper" version = "1.3.2" diff --git a/Cargo.toml b/Cargo.toml index f7029bd..dc5e154 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,9 +8,11 @@ name = "exa" [dependencies] ansi_term = "0.4.5" +datetime_macros = "0.1.2" getopts = "0.2.1" natord = "1.0.7" number_prefix = "0.2.3" +pad = "0.1.1" users = "0.2.3" [features] @@ -19,4 +21,7 @@ git = [ "git2" ] [dependencies.git2] version = "0.1.13" -optional = true \ No newline at end of file +optional = true + +[dependencies.datetime] +git = "https://github.com/ogham/rust-datetime.git" diff --git a/README.md b/README.md index 436a6dd..8164823 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,8 @@ exa is a replacement for `ls` written in Rust. - **-R**, **--recurse**: recurse into subdirectories - **-s**, **--sort=(field)**: field to sort by - **-S**, **--blocks**: show number of file system blocks +- **-t**, **--time**: which timestamp to show for a file +- **-T**, **--tree**: recurse into subdirectories in a tree view - **-x**, **--across**: sort multi-column view entries across You can sort by **name**, **size**, **ext**, **inode**, or **none**. diff --git a/src/column.rs b/src/column.rs index 030570a..11aae59 100644 --- a/src/column.rs +++ b/src/column.rs @@ -2,12 +2,13 @@ use std::iter::repeat; use ansi_term::Style; -use options::SizeFormat; +use options::{SizeFormat, TimeType}; #[derive(PartialEq, Debug, Copy)] pub enum Column { Permissions, FileSize(SizeFormat), + Timestamp(TimeType, i64), Blocks, User, Group, @@ -42,14 +43,15 @@ impl Column { /// to have a header row printed. pub fn header(&self) -> &'static str { match *self { - Column::Permissions => "Permissions", - Column::FileSize(_) => "Size", - Column::Blocks => "Blocks", - Column::User => "User", - Column::Group => "Group", - Column::HardLinks => "Links", - Column::Inode => "inode", - Column::GitStatus => "Git", + 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", } } } diff --git a/src/file.rs b/src/file.rs index bc968c7..92732fb 100644 --- a/src/file.rs +++ b/src/file.rs @@ -8,13 +8,18 @@ use ansi_term::Colour::{Red, Green, Yellow, Blue, Purple, Cyan, Fixed}; use users::Users; +use pad::Alignment; + use number_prefix::{binary_prefix, decimal_prefix, Prefixed, Standalone, PrefixNames}; +use datetime; +use datetime::local::{LocalDateTime, DatePiece}; + use column::{Column, Cell}; use column::Column::*; use dir::Dir; use filetype::HasType; -use options::SizeFormat; +use options::{SizeFormat, TimeType}; /// This grey value is directly in between white and black, so it's guaranteed /// to show up on either backgrounded terminal. @@ -94,14 +99,15 @@ impl<'a> File<'a> { /// Get the data for a column, formatted as a coloured string. pub fn display(&self, column: &Column, users_cache: &mut U) -> Cell { match *column { - Permissions => self.permissions_string(), - FileSize(f) => self.file_size(f), - HardLinks => self.hard_links(), - Inode => self.inode(), - Blocks => self.blocks(), - User => self.user(users_cache), - Group => self.group(users_cache), - GitStatus => self.git_status(), + Permissions => self.permissions_string(), + FileSize(f) => self.file_size(f), + Timestamp(t, y) => self.timestamp(t, y), + HardLinks => self.hard_links(), + Inode => self.inode(), + Blocks => self.blocks(), + User => self.user(users_cache), + Group => self.group(users_cache), + GitStatus => self.git_status(), } } @@ -297,6 +303,27 @@ impl<'a> File<'a> { } } + fn timestamp(&self, time_type: TimeType, current_year: i64) -> Cell { + + // Need to convert these values from milliseconds into seconds. + let time_in_seconds = match time_type { + TimeType::FileAccessed => self.stat.accessed, + TimeType::FileModified => self.stat.modified, + TimeType::FileCreated => self.stat.created, + } as i64 / 1000; + + let date = LocalDateTime::at(time_in_seconds); + + let format = if date.year() == current_year { + date_format!("{2>:D} {:M} {2>:h}:{02>:m}") + } + else { + date_format!("{2>:D} {:M} {4>:Y}") + }; + + Cell::paint(Blue.normal(), format.format(date).as_slice()) + } + /// This file's type, represented by a coloured character. /// /// Although the file type can usually be guessed from the colour of the diff --git a/src/main.rs b/src/main.rs index 2e384ff..97c51ce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,14 @@ -#![feature(collections, core, env, io, libc, os, path, std_misc)] +#![feature(collections, core, env, io, libc, os, path, plugin, std_misc)] + +#[plugin] #[no_link] +extern crate datetime_macros; extern crate ansi_term; +extern crate datetime; extern crate getopts; extern crate natord; extern crate number_prefix; +extern crate pad; extern crate users; #[cfg(feature="git")] diff --git a/src/options.rs b/src/options.rs index 005b284..e12b349 100644 --- a/src/options.rs +++ b/src/options.rs @@ -12,6 +12,8 @@ use std::fmt; use getopts; use natord; +use datetime::local::{LocalDateTime, DatePiece}; + use self::Misfire::*; /// The *Options* struct represents a parsed version of the user's @@ -50,13 +52,17 @@ impl Options { opts.optflag("g", "group", "show group as well as user"); opts.optflag("h", "header", "show a header row at the top"); opts.optflag("H", "links", "show number of hard links"); - opts.optflag("l", "long", "display extended details and attributes"); opts.optflag("i", "inode", "show each file's inode number"); + opts.optflag("l", "long", "display extended details and attributes"); + opts.optflag("m", "modified", "display timestamp of most recent modification"); opts.optflag("r", "reverse", "reverse order of files"); opts.optflag("R", "recurse", "recurse into directories"); opts.optopt ("s", "sort", "field to sort by", "WORD"); opts.optflag("S", "blocks", "show number of file system blocks"); + opts.optopt ("t", "time", "which timestamp to show for a file", "WORD"); opts.optflag("T", "tree", "recurse into subdirectories in a tree view"); + opts.optflag("u", "accessed", "display timestamp of last access for a file"); + opts.optflag("U", "created", "display timestamp of creation for a file"); opts.optflag("x", "across", "sort multi-column view entries across"); opts.optflag("?", "help", "show list of command-line options"); @@ -231,6 +237,9 @@ impl View { else if matches.opt_present("blocks") { Err(Misfire::Useless("blocks", false, "long")) } + else if matches.opt_present("time") { + Err(Misfire::Useless("time", false, "long")) + } else if matches.opt_present("oneline") { if matches.opt_present("across") { Err(Misfire::Useless("across", true, "oneline")) @@ -279,6 +288,73 @@ impl SizeFormat { } } +#[derive(PartialEq, Debug, Copy)] +pub enum TimeType { + FileAccessed, + FileModified, + FileCreated, +} + +impl TimeType { + pub fn header(&self) -> &'static str { + match *self { + TimeType::FileAccessed => "Date Accessed", + TimeType::FileModified => "Date Modified", + TimeType::FileCreated => "Date Created", + } + } +} + +#[derive(PartialEq, Debug, Copy)] +pub struct TimeTypes { + accessed: bool, + modified: bool, + created: bool, +} + +impl TimeTypes { + + /// Find which field to use based on a user-supplied word. + fn deduce(matches: &getopts::Matches) -> Result { + let possible_word = matches.opt_str("time"); + let modified = matches.opt_present("modified"); + let created = matches.opt_present("created"); + let accessed = matches.opt_present("accessed"); + + if let Some(word) = possible_word { + if modified { + return Err(Misfire::Useless("modified", true, "time")); + } + else if created { + return Err(Misfire::Useless("created", true, "time")); + } + else if accessed { + return Err(Misfire::Useless("accessed", true, "time")); + } + + match word.as_slice() { + "mod" | "modified" => Ok(TimeTypes { accessed: false, modified: true, created: false }), + "acc" | "accessed" => Ok(TimeTypes { accessed: true, modified: false, created: false }), + "cr" | "created" => Ok(TimeTypes { accessed: false, modified: false, created: true }), + field => Err(TimeTypes::none(field)), + } + } + else { + if modified || created || accessed { + Ok(TimeTypes { accessed: accessed, modified: modified, created: created }) + } + else { + Ok(TimeTypes { accessed: false, modified: true, created: false }) + } + } + } + + /// How to display an error when the word didn't match with anything. + fn none(field: &str) -> Misfire { + Misfire::InvalidOptions(getopts::Fail::UnrecognizedOption(format!("--time {}", field))) + } +} + /// What to do when encountering a directory? #[derive(PartialEq, Debug, Copy)] pub enum DirAction { @@ -304,6 +380,7 @@ impl DirAction { #[derive(PartialEq, Copy, Debug)] pub struct Columns { size_format: SizeFormat, + time_types: TimeTypes, inode: bool, links: bool, blocks: bool, @@ -314,6 +391,7 @@ impl Columns { pub fn deduce(matches: &getopts::Matches) -> Result { Ok(Columns { size_format: try!(SizeFormat::deduce(matches)), + time_types: try!(TimeTypes::deduce(matches)), inode: matches.opt_present("inode"), links: matches.opt_present("links"), blocks: matches.opt_present("blocks"), @@ -346,6 +424,20 @@ impl Columns { columns.push(Group); } + let current_year = LocalDateTime::now().year(); + + if self.time_types.modified { + columns.push(Timestamp(TimeType::FileModified, current_year)); + } + + if self.time_types.created { + columns.push(Timestamp(TimeType::FileCreated, current_year)); + } + + if self.time_types.accessed { + columns.push(Timestamp(TimeType::FileAccessed, current_year)); + } + if cfg!(feature="git") { if let Some(d) = dir { if d.has_git_repo() {