diff --git a/Cargo.lock b/Cargo.lock index 9bc9b76..7981e14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,9 +52,16 @@ dependencies = [ "ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "size 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", + "users 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "num-traits" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "redox_syscall" version = "0.1.40" @@ -68,6 +75,14 @@ dependencies = [ "redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "size" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "strsim" version = "0.7.0" @@ -106,6 +121,14 @@ name = "unicode-width" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "users" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "vec_map" version = "0.8.1" @@ -137,13 +160,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b957d88f4b6a63b9d70d5f454ac8011819c6efa7727858f458ab71c756ce2d3e" "checksum lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a374c89b9db55895453a74c1e38861d9deec0b01b405a82516e9d5de4820dea1" "checksum libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)" = "76e3a3ef172f1a0b9a9ff0dd1491ae5e6c948b94479a3021819ba7d860c8645d" +"checksum num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0b3a5d7cc97d6d30d8b9bc8fa19bf45349ffe46241e8816f50f62f6d6aaabee1" "checksum redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "c214e91d3ecf43e9a4e41e578973adeb14b474f2bee858742d127af75a0112b1" "checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" +"checksum size 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "baf1794b187cb1fd42cbe074cafc027be10b275c68aa7d039dcbef41e94d01d4" "checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550" "checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" "checksum textwrap 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "307686869c93e71f94da64286f9a9524c0f308a9e1c87a583de8e9c9039ad3f6" "checksum time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "d825be0eb33fda1a7e68012d51e9c7f451dc1a69391e7fdc197060bb8c56667b" "checksum unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526" +"checksum users 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb3c80625ae5e77e1b402f8a0fa89afbd50622a6cae65128844720bd4e26b657" "checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" "checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0" "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" diff --git a/Cargo.toml b/Cargo.toml index b697fda..12e1bbb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,4 +7,6 @@ version = "0.1.0" ansi_term = "0.11.0" clap = "2.32.0" lazy_static = "1.2.0" +size = "0.1.1" time = "0.1.40" +users = "0.8.0" diff --git a/src/core.rs b/src/core.rs new file mode 100644 index 0000000..4ee82e0 --- /dev/null +++ b/src/core.rs @@ -0,0 +1,206 @@ +use formatter::*; +use size::*; +use std::cmp::Ordering; +use std::fs::{read_link, Metadata}; +use std::os::unix::fs::MetadataExt; +use std::path::{Path, PathBuf}; +use users::{get_group_by_gid, get_user_by_uid}; +use Options; + +pub struct Core<'a> { + formatter: Formatter, + options: &'a Options, +} + +#[derive(Debug)] +pub struct Meta { + pub path: PathBuf, + pub metadata: Metadata, + pub group: String, + pub user: String, + pub symlink: Option, + pub size_value: String, + pub size_unit: String, +} + +impl<'a> Core<'a> { + pub fn new(options: &'a Options) -> Core<'a> { + Core { + options: options, + formatter: Formatter::new(), + } + } + + pub fn print(&self, inputs: Vec<&str>) { + let print_folder_name: bool = inputs.len() > 1; + + let mut dirs = Vec::new(); + let mut files = Vec::new(); + + for input in inputs { + let path = Path::new(input); + + if path.is_dir() { + dirs.push(path); + } else { + files.push(self.path_to_meta(&path).unwrap()); + } + } + + files.sort_unstable_by(sort_by_meta); + dirs.sort_unstable(); + + if files.len() > 0 { + self.print_long(&mut files); + } + + for dir in dirs { + let folder_metas = self.list_folder_content(dir); + if print_folder_name { + println!("\n{}:", dir.display()) + } + self.print_long(&folder_metas); + } + } + + pub fn list_folder_content(&self, folder: &Path) -> Vec { + let mut content: Vec = Vec::new(); + + for entry in folder.read_dir().expect("read_dir call failed") { + if let Ok(entry) = entry { + if let Some(meta) = self.path_to_meta(entry.path().as_path()) { + content.push(meta); + } + } + } + + content.sort_unstable_by(sort_by_meta); + + content + } + + pub fn path_to_meta(&self, path: &Path) -> Option { + let mut name: Option<&str> = None; + if let Some(os_str_name) = path.file_name() { + if let Some(name_str) = os_str_name.to_str() { + name = Some(name_str); + } + } + + if name.is_none() { + println!("failed to retrieve file name for {}", path.display()); + return None; + } + + // Skip the hidden files if the 'display_all' option is not set. + if name.unwrap().starts_with(".") && !self.options.display_all { + return None; + } + + let meta; + let mut symlink = None; + if let Ok(res) = read_link(path) { + meta = path.symlink_metadata().unwrap(); + symlink = Some(res.to_str().unwrap().to_string()); + } else { + meta = path.metadata().unwrap(); + } + + let user = get_user_by_uid(meta.uid()) + .unwrap() + .name() + .to_str() + .unwrap() + .to_string(); + + let group = get_group_by_gid(meta.gid()) + .unwrap() + .name() + .to_str() + .unwrap() + .to_string(); + + let size = Size::Bytes(meta.len()).to_string(Base::Base10, Style::Abbreviated); + let size_parts: Vec<&str> = size.split(' ').collect(); + + Some(Meta { + path: path.to_path_buf(), + metadata: meta, + user: user, + group: group, + symlink: symlink, + size_value: size_parts[0].to_string(), + size_unit: size_parts[1].to_string(), + }) + } + + fn print_long(&self, metas: &Vec) { + let max_user_length = self.detect_user_lenght(metas); + let max_group_length = self.detect_group_lenght(metas); + let (max_size_value_length, max_size_unit_length) = self.detect_size_lenghts(metas); + + for meta in metas { + print!( + " {} {} {} {} {} {}\n", + self.formatter.format_permissions(&meta), + self.formatter.format_user(&meta.user, max_user_length), + self.formatter.format_group(&meta.group, max_group_length), + self.formatter + .format_size(&meta, max_size_value_length, max_size_unit_length), + self.formatter.format_date(&meta), + self.formatter.format_name(&meta), + ); + } + } + + fn detect_user_lenght(&self, paths: &Vec) -> usize { + let mut max: usize = 0; + + for path in paths { + if path.user.len() > max { + max = path.user.len(); + } + } + + max + } + + fn detect_group_lenght(&self, paths: &Vec) -> usize { + let mut max: usize = 0; + + for path in paths { + if path.group.len() > max { + max = path.group.len(); + } + } + + max + } + + fn detect_size_lenghts(&self, paths: &Vec) -> (usize, usize) { + let mut max_value_length: usize = 0; + let mut max_unit_size: usize = 0; + + for path in paths { + if path.size_value.len() > max_value_length { + max_value_length = path.size_value.len(); + } + + if path.size_unit.len() > max_unit_size { + max_unit_size = path.size_unit.len(); + } + } + + (max_value_length, max_unit_size) + } +} + +fn sort_by_meta(a: &Meta, b: &Meta) -> Ordering { + if a.path.is_dir() == b.path.is_dir() { + a.path.cmp(&b.path) + } else if a.path.is_dir() && b.path.is_file() { + Ordering::Less + } else { + Ordering::Greater + } +} diff --git a/src/formatter.rs b/src/formatter.rs index 51d70ec..e34d8b8 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -1,5 +1,5 @@ use ansi_term::Colour; -use path_lister::Path; +use core::Meta; use std::collections::HashMap; use std::os::unix::fs::PermissionsExt; use std::time::{Duration, SystemTime, UNIX_EPOCH}; @@ -8,70 +8,72 @@ use time::Timespec; const HOURE: u64 = 3600; // 1 HOURE == 3600 seconds const DAY: u64 = HOURE * 24; // 1 DAY == 25 HOURE -#[derive(Hash, Debug, Eq, PartialEq, Copy, Clone)] -pub enum PathKind { - UnrecognizedFile, - RecognizedFile, - Dir, -} - -lazy_static! { - pub static ref PathKindColor: HashMap = { - let mut m = HashMap::new(); - - m.insert(PathKind::UnrecognizedFile, (0xFF, 0xFF, 0x04)); // gold - m.insert(PathKind::RecognizedFile, (0x04, 0xFF, 0x04)); // limon - m.insert(PathKind::Dir, (0x00, 0xAF, 0xFF)); // dodgerblue - - m - }; -} - -#[derive(Hash, Debug, Eq, PartialEq, Copy, Clone)] -pub enum LastTimeModified { - DayOld, - HourOld, - Older, -} - -lazy_static! { - pub static ref LastTimeModifiedColor: HashMap = { - let mut m = HashMap::new(); - - m.insert(LastTimeModified::HourOld, (0x2C, 0xFF, 0x2C)); - m.insert(LastTimeModified::DayOld, (0x1C, 0xFF, 0xB7)); - m.insert(LastTimeModified::Older, (0x63, 0xB1, 0x8A)); - - m - }; -} - #[allow(dead_code)] #[derive(Hash, Debug, Eq, PartialEq, Copy, Clone)] pub enum Elem { + /// Path Kind + UnrecognizedFile, + RecognizedFile, + Dir, + + /// Permissions + Read, + Write, + Exec, + NoAccess, + + /// Last Time Modified + DayOld, + HourOld, + Older, + /// Link DeadLink, Link, + /// User / Group Name + User, + Group, + /// File Size FileLarge, FileMedium, FileSmall, +} - /// Random - Report, - User, - Tree, - Empty, - Error, - Normal, +lazy_static! { + pub static ref Colors: HashMap = { + let mut m = HashMap::new(); + // User / Group + m.insert(Elem::User, Colour::RGB(0xFF, 0xFF, 0xD8)); + m.insert(Elem::Group, Colour::RGB(0xD9, 0xD9, 0x8F)); - /// Git - Addition, - Modification, - Deletion, - Untracked, - Unchanged, + // Permissions + m.insert(Elem::Read, Colour::RGB(0x5f, 0xD7, 0x5F)); + m.insert(Elem::Write, Colour::RGB(0xD7, 0xD7, 0x87)); + m.insert(Elem::Exec, Colour::RGB(0xCD, 0x3A, 0x3A)); + m.insert(Elem::NoAccess, Colour::RGB(0xD7, 0x89, 0x89)); + + // Path Kind + m.insert(Elem::UnrecognizedFile, Colour::RGB(0xFF, 0xFF, 0x04)); // gold + m.insert(Elem::RecognizedFile, Colour::RGB(0x04, 0xFF, 0x04)); // limon + m.insert(Elem::Dir, Colour::RGB(0x00, 0xAF, 0xFF)); // dodgerblue + + // Last Time Modified + m.insert(Elem::HourOld, Colour::RGB(0x2C, 0xFF, 0x2C)); + m.insert(Elem::DayOld, Colour::RGB(0x1C, 0xFF, 0xB7)); + m.insert(Elem::Older, Colour::RGB(0x63, 0xB1, 0x8A)); + + // Last Time Modified + m.insert(Elem::FileSmall, Colour::RGB(0xFF, 0xFF, 0xD9)); + m.insert(Elem::FileMedium, Colour::RGB(0x1C, 0xFF, 0xB7)); + m.insert(Elem::FileLarge, Colour::RGB(0xFF, 0xB0, 0x00)); + + // Link + m.insert(Elem::Link, Colour::RGB(0x3B, 0xCE, 0xCe)); + + m + }; } pub struct Formatter {} @@ -81,29 +83,38 @@ impl Formatter { Formatter {} } - pub fn format_path(&self, content: &str, path: &Path) -> String { - let color = match path.metadata.is_dir() { - true => PathKindColor[&PathKind::Dir], - false => PathKindColor[&PathKind::UnrecognizedFile], + pub fn format_name(&self, meta: &Meta) -> String { + let mut content = String::new(); + + let color = match meta.metadata.is_dir() { + true => Colors[&Elem::Dir], + false => Colors[&Elem::UnrecognizedFile], }; - Colour::RGB(color.0, color.1, color.2) - .paint(content) - .to_string() + let file_name = meta.path.file_name().unwrap().to_str().unwrap(); + content = content + &color.paint(file_name).to_string(); + + let color = Colors[&Elem::Link]; + if let Some(ref link) = meta.symlink { + content = + content + &color.paint(String::from(" ⇒ ") + &color.paint(link).to_string()); + } + + content } - pub fn format_date(&self, path: &Path) -> String { - let modified_time = path.metadata.modified().unwrap(); + pub fn format_date(&self, meta: &Meta) -> String { + let modified_time = meta.metadata.modified().unwrap(); let now = SystemTime::now(); let color; if modified_time > now - Duration::new(HOURE, 0) { - color = LastTimeModifiedColor[&LastTimeModified::HourOld]; + color = Colors[&Elem::HourOld]; } else if modified_time > now - Duration::new(DAY, 0) { - color = LastTimeModifiedColor[&LastTimeModified::DayOld]; + color = Colors[&Elem::DayOld]; } else { - color = LastTimeModifiedColor[&LastTimeModified::Older]; + color = Colors[&Elem::Older]; } let modified_time_since_epoch = modified_time.duration_since(UNIX_EPOCH).unwrap(); @@ -112,28 +123,18 @@ impl Formatter { modified_time_since_epoch.subsec_nanos() as i32, )); - Colour::RGB(color.0, color.1, color.2) - .paint(time.ctime().to_string()) - .to_string() + color.paint(time.ctime().to_string()).to_string() } - pub fn format_permissions(&self, path: &Path) -> String { + pub fn format_permissions(&self, meta: &Meta) -> String { let mut res = String::with_capacity(10); - let mode = path.metadata.permissions().mode(); + let mode = meta.metadata.permissions().mode(); - let read_perm = Colour::RGB(0x5f, 0xD7, 0x5F) - .paint(String::from("r")) - .to_string(); - let write_perm = Colour::RGB(0xD7, 0xD7, 0x87) - .paint(String::from("w")) - .to_string(); - let exec_perm = Colour::RGB(0xCD, 0x3A, 0x3A) - .paint(String::from("x")) - .to_string(); - let no_access = Colour::RGB(0xD7, 0x89, 0x89) - .paint(String::from("-")) - .to_string(); + let read_perm = Colors[&Elem::Read].paint(String::from("r")).to_string(); + let write_perm = Colors[&Elem::Write].paint(String::from("w")).to_string(); + let exec_perm = Colors[&Elem::Exec].paint(String::from("x")).to_string(); + let no_access = Colors[&Elem::NoAccess].paint(String::from("-")).to_string(); // User Read Permisssions match mode & 0o400 { @@ -191,4 +192,64 @@ impl Formatter { res.to_string() } + + pub fn format_user(&self, user_name: &String, max_user_size: usize) -> String { + if user_name.len() == max_user_size { + return Colors[&Elem::User].paint(user_name).to_string(); + } + + let mut content = String::with_capacity(max_user_size); + + content = content + user_name; + + for _ in 0..(max_user_size - user_name.len()) { + content.push(' '); + } + + content + } + + pub fn format_group(&self, group_name: &String, max_group_size: usize) -> String { + if group_name.len() == max_group_size { + return Colors[&Elem::Group].paint(group_name).to_string(); + } + + let mut content = String::with_capacity(max_group_size); + content = content + group_name; + + for _ in 0..(max_group_size - group_name.len()) { + content.push(' '); + } + + content + } + + pub fn format_size( + &self, + meta: &Meta, + max_value_length: usize, + max_unit_size: usize, + ) -> String { + let mut content = String::with_capacity(max_value_length + max_unit_size + 1); + + for _ in 0..(max_value_length - meta.size_value.len()) { + content.push(' '); + } + + content = content + meta.size_value.as_str(); + content.push(' '); + content = content + meta.size_unit.as_str(); + + for _ in 0..(max_unit_size - meta.size_unit.len()) { + content.push(' '); + } + + if meta.metadata.len() < 10 * 1044 * 1024 { + return Colors[&Elem::FileSmall].paint(content).to_string(); + } else if meta.metadata.len() < 100 * 1044 * 1024 { + return Colors[&Elem::FileMedium].paint(content).to_string(); + } else { + return Colors[&Elem::FileLarge].paint(content).to_string(); + } + } } diff --git a/src/main.rs b/src/main.rs index 9a3a84c..17f954d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,18 +2,17 @@ extern crate clap; #[macro_use] extern crate lazy_static; extern crate ansi_term; +extern crate size; extern crate time; +extern crate users; +mod core; mod formatter; -mod path_lister; -mod presenter; use clap::{App, Arg}; -use path_lister::PathLister; -use presenter::Presenter; +use core::Core; pub struct Options { - pub display_long: bool, pub display_all: bool, } @@ -21,20 +20,16 @@ fn main() { let matches = App::new("lsd") .about("A ls comment with a lot of pretty colors and some other stuff.") .arg(Arg::with_name("FILE").multiple(true).default_value(".")) - .arg(Arg::with_name("long").short("l")) .arg(Arg::with_name("all").short("a")) .get_matches(); let options = Options { - display_long: matches.is_present("long"), display_all: matches.is_present("all"), }; let inputs: Vec<&str> = matches.values_of("FILE").unwrap().collect(); - let path_lister = PathLister::new(&options); + let core = Core::new(&options); - let presenter = Presenter::new(&options); - - presenter.print(path_lister.list_paths_to_print(inputs)); + core.print(inputs); } diff --git a/src/path_lister.rs b/src/path_lister.rs deleted file mode 100644 index 86c8cd5..0000000 --- a/src/path_lister.rs +++ /dev/null @@ -1,80 +0,0 @@ -use std::fs::Metadata; -use std::io::ErrorKind; -use std::path::{self, PathBuf}; -use Options; - -pub struct Path { - pub path: PathBuf, - pub metadata: Metadata, -} - -pub struct PathLister<'a> { - options: &'a Options, -} - -impl<'a> PathLister<'a> { - pub fn new(options: &'a Options) -> PathLister<'a> { - PathLister { options: options } - } - - pub fn list_paths_to_print(&self, inputs: Vec<&'a str>) -> Vec { - let mut res = Vec::new(); - - for input in inputs.iter() { - let path = path::Path::new(input); - - let res_meta = path.metadata(); - - if let Err(err) = res_meta { - match err.kind() { - ErrorKind::NotFound => println!("Specified path \"{}\" doesn't exists.", input), - ErrorKind::PermissionDenied => { - println!("Cannot open \"{}\": Permission denied", input) - } - _ => println!("Cannot open \"{}\": {}", input, err), - } - - continue; - } - - if let Ok(meta) = res_meta { - let file_type = meta.file_type(); - - if file_type.is_file() { - self.add_path_to_list(&mut res, path.to_path_buf(), meta); - continue; - } - - if file_type.is_dir() { - self.read_paths_from_dir(&mut res, path.to_path_buf()); - continue; - } - - if file_type.is_symlink() {} - } - } - - res - } - - fn read_paths_from_dir(&self, path_list: &mut Vec, path: PathBuf) { - for entry in path.read_dir().expect("read_dir call failed") { - if let Ok(entry) = entry { - self.add_path_to_list(path_list, entry.path(), entry.metadata().unwrap()); - } - } - } - - fn add_path_to_list(&self, path_list: &mut Vec, path: PathBuf, meta: Metadata) { - // Skip the hidden files if the 'display_all' option is not set. - if path.file_name().unwrap().to_str().unwrap().starts_with(".") && !self.options.display_all - { - return; - } - - path_list.push(Path { - path: path.to_path_buf(), - metadata: meta, - }); - } -} diff --git a/src/presenter.rs b/src/presenter.rs deleted file mode 100644 index 248ee19..0000000 --- a/src/presenter.rs +++ /dev/null @@ -1,48 +0,0 @@ -use formatter::*; -use path_lister::Path; -use Options; - -pub struct Presenter<'a> { - formatter: Formatter, - options: &'a Options, -} - -impl<'a> Presenter<'a> { - pub fn new(options: &'a Options) -> Presenter<'a> { - Presenter { - options: options, - formatter: Formatter::new(), - } - } - - pub fn print(&self, paths: Vec) { - if self.options.display_long { - self.print_long(&paths); - return; - } - - self.print_simple(&paths) - } - - fn print_long(&self, paths: &Vec) { - for path in paths { - print!( - " {} {} {}\n", - self.formatter.format_permissions(path), - self.formatter.format_date(path), - self.formatter - .format_path(path.path.file_name().unwrap().to_str().unwrap(), &path) - ); - } - } - - fn print_simple(&self, paths: &Vec) { - for path in paths { - print!( - "{}\n", - self.formatter - .format_path(path.path.file_name().unwrap().to_str().unwrap(), &path) - ); - } - } -}