diff --git a/README.md b/README.md index 7f6ec2a..799e2f4 100644 --- a/README.md +++ b/README.md @@ -17,14 +17,15 @@ Options - **-b**, **--binary**: use binary (power of two) file sizes - **-g**, **--group**: show group as well as user - **-h**, **--header**: show a header row +- **-H**, **--links**: show number of hard links column - **-i**, **--inode**: show inode number column -- **-l**, **--links**: show number of hard links column +- **-l**, **--long**: display extended details and attributes - **-r**, **--reverse**: reverse sort order - **-s**, **--sort=(name, size, ext)**: field to sort by - **-S**, **--blocks**: show number of file system blocks - +- **-x**, **--across**: sort multi-column view entries across Installation ------------ -exa is written in [Rust](http://www.rust-lang.org). It compiles with Rust 0.11, the latest version - 0.10 will not do, as there have been too many breaking changes since. You will also need [Cargo](http://crates.io), the Rust package manager. Once you have them both set up, a simple `cargo build` will pull in all the dependencies and compile exa. +exa is written in [Rust](http://www.rust-lang.org). You'll have to use the nightly -- I try to keep it up to date with the latest version when possible. You will also need [Cargo](http://crates.io), the Rust package manager. Once you have them both set up, a simple `cargo build` will pull in all the dependencies and compile exa. diff --git a/screenshot.png b/screenshot.png index 535fe16..2514c8b 100644 Binary files a/screenshot.png and b/screenshot.png differ diff --git a/src/exa.rs b/src/exa.rs index 37a6ebd..5621174 100644 --- a/src/exa.rs +++ b/src/exa.rs @@ -2,12 +2,14 @@ extern crate regex; #[phase(plugin)] extern crate regex_macros; extern crate ansi_term; +extern crate unicode; use std::os; use file::File; use dir::Dir; -use options::Options; +use column::{Column, Left}; +use options::{Options, Lines, Grid}; use unix::Unix; use ansi_term::{Paint, Plain, strip_formatting}; @@ -20,6 +22,7 @@ pub mod filetype; pub mod unix; pub mod options; pub mod sort; +pub mod term; fn main() { let args = os::args(); @@ -48,7 +51,10 @@ fn exa(opts: &Options) { match Dir::readdir(Path::new(dir_name.clone())) { Ok(dir) => { if print_dir_names { println!("{}:", dir_name); } - lines_view(opts, dir); + match opts.view { + Lines(ref cols) => lines_view(opts, cols, dir), + Grid(bool) => grid_view(opts, bool, dir), + } } Err(e) => { println!("{}: {}", dir_name, e); @@ -58,7 +64,48 @@ fn exa(opts: &Options) { } } -fn lines_view(options: &Options, dir: Dir) { +fn grid_view(options: &Options, across: bool, dir: Dir) { + let unsorted_files = dir.files(); + let files: Vec<&File> = options.transform_files(&unsorted_files); + + let max_column_length = files.iter().map(|f| f.file_name_width()).max().unwrap(); + let (console_width, _) = term::dimensions().unwrap_or((80, 24)); + let num_columns = (console_width + 1) / (max_column_length + 1); + let count = files.len(); + + let mut num_rows = count / num_columns; + if count % num_columns != 0 { + num_rows += 1; + } + + for y in range(0, num_rows) { + for x in range(0, num_columns) { + let num = if across { + y * num_columns + x + } + else { + y + num_rows * x + }; + + if num >= count { + continue; + } + + let file = files[num]; + let file_name = file.name.clone(); + let styled_name = file.file_colour().paint(file_name.as_slice()); + if x == num_columns - 1 { + print!("{}", styled_name); + } + else { + print!("{}", Left.pad_string(&styled_name, max_column_length - file_name.len() + 1)); + } + } + print!("\n"); + } +} + +fn lines_view(options: &Options, columns: &Vec, dir: Dir) { let unsorted_files = dir.files(); let files: Vec<&File> = options.transform_files(&unsorted_files); @@ -71,11 +118,11 @@ fn lines_view(options: &Options, dir: Dir) { let mut cache = Unix::empty_cache(); let mut table: Vec> = files.iter() - .map(|f| options.columns.iter().map(|c| f.display(c, &mut cache)).collect()) + .map(|f| columns.iter().map(|c| f.display(c, &mut cache)).collect()) .collect(); if options.header { - table.unshift(options.columns.iter().map(|c| Plain.underline().paint(c.header())).collect()); + table.unshift(columns.iter().map(|c| Plain.underline().paint(c.header())).collect()); } // Each column needs to have its invisible colour-formatting @@ -88,21 +135,21 @@ fn lines_view(options: &Options, dir: Dir) { .map(|row| row.iter().map(|col| strip_formatting(col.clone()).len()).collect()) .collect(); - let column_widths: Vec = range(0, options.columns.len()) - .map(|n| lengths.iter().map(|row| *row.get(n)).max().unwrap()) + let column_widths: Vec = range(0, columns.len()) + .map(|n| lengths.iter().map(|row| row[n]).max().unwrap()) .collect(); for (field_widths, row) in lengths.iter().zip(table.iter()) { - for (num, column) in options.columns.iter().enumerate() { + for (num, column) in columns.iter().enumerate() { if num != 0 { print!(" "); } - if num == options.columns.len() - 1 { + if num == columns.len() - 1 { print!("{}", row.get(num)); } else { - let padding = *column_widths.get(num) - *field_widths.get(num); + let padding = column_widths[num] - field_widths[num]; print!("{}", column.alignment().pad_string(row.get(num), padding)); } } diff --git a/src/file.rs b/src/file.rs index e7d12c2..0de2655 100644 --- a/src/file.rs +++ b/src/file.rs @@ -1,6 +1,6 @@ use std::io::{fs, IoResult}; use std::io; -use std::str::from_utf8_lossy; +use unicode::str::UnicodeStrSlice; use ansi_term::{Paint, Colour, Plain, Style, Red, Green, Yellow, Blue, Purple, Cyan, Fixed}; @@ -32,7 +32,7 @@ pub struct File<'a> { impl<'a> File<'a> { pub fn from_path(path: &'a Path, parent: &'a Dir) -> IoResult> { let v = path.filename().unwrap(); // fails if / or . or .. - let filename = from_utf8_lossy(v).to_string(); + let filename = String::from_utf8_lossy(v).to_string(); // Use lstat here instead of file.stat(), as it doesn't follow // symbolic links. Otherwise, the stat() call will fail if it @@ -109,13 +109,13 @@ impl<'a> File<'a> { // the time. HardLinks => { let style = if self.stat.kind == io::TypeFile && self.stat.unstable.nlink > 1 { Red.on(Yellow) } else { Red.normal() }; - style.paint(self.stat.unstable.nlink.to_str().as_slice()) + style.paint(self.stat.unstable.nlink.to_string().as_slice()) }, - Inode => Purple.paint(self.stat.unstable.inode.to_str().as_slice()), + Inode => Purple.paint(self.stat.unstable.inode.to_string().as_slice()), Blocks => { if self.stat.kind == io::TypeFile || self.stat.kind == io::TypeSymlink { - Cyan.paint(self.stat.unstable.blocks.to_str().as_slice()) + Cyan.paint(self.stat.unstable.blocks.to_string().as_slice()) } else { Grey.paint("-") @@ -128,13 +128,13 @@ impl<'a> File<'a> { let uid = self.stat.unstable.uid as u32; unix.load_user(uid); let style = if unix.uid == uid { Yellow.bold() } else { Plain }; - let string = unix.get_user_name(uid).unwrap_or(uid.to_str()); + let string = unix.get_user_name(uid).unwrap_or(uid.to_string()); style.paint(string.as_slice()) }, Group => { let gid = self.stat.unstable.gid as u32; unix.load_group(gid); - let name = unix.get_group_name(gid).unwrap_or(gid.to_str()); + let name = unix.get_group_name(gid).unwrap_or(gid.to_string()); let style = if unix.is_group_member(gid) { Yellow.normal() } else { Plain }; style.paint(name.as_slice()) }, @@ -158,9 +158,13 @@ impl<'a> File<'a> { } } + pub fn file_name_width(&self) -> uint { + self.name.as_slice().width(false) + } + fn target_file_name_and_arrow(&self, target_path: Path) -> String { let v = target_path.filename().unwrap(); - let filename = from_utf8_lossy(v).to_string(); + let filename = String::from_utf8_lossy(v).to_string(); let link_target = fs::stat(&target_path).map(|stat| File { path: &target_path, @@ -210,7 +214,7 @@ impl<'a> File<'a> { } } - fn file_colour(&self) -> Style { + pub fn file_colour(&self) -> Style { self.get_type().style() } diff --git a/src/options.rs b/src/options.rs index 3afa8b9..750c423 100644 --- a/src/options.rs +++ b/src/options.rs @@ -1,7 +1,6 @@ extern crate getopts; use file::File; -use std::cmp::lexical_ordering; use column::{Column, Permissions, FileName, FileSize, User, Group, HardLinks, Inode, Blocks}; use std::ascii::StrAsciiExt; @@ -9,15 +8,6 @@ pub enum SortField { Name, Extension, Size } -pub struct Options { - pub showInvisibles: bool, - pub sortField: SortField, - pub reverse: bool, - pub dirs: Vec, - pub columns: Vec, - pub header: bool, -} - impl SortField { fn from_word(word: String) -> SortField { match word.as_slice() { @@ -29,6 +19,21 @@ impl SortField { } } +pub enum View { + Lines(Vec), + Grid(bool), +} + +pub struct Options { + pub show_invisibles: bool, + pub sort_field: SortField, + pub reverse: bool, + pub dirs: Vec, + pub view: View, + pub header: bool, +} + + impl Options { pub fn getopts(args: Vec) -> Result { let opts = [ @@ -36,26 +41,37 @@ impl Options { getopts::optflag("b", "binary", "use binary prefixes in file sizes"), getopts::optflag("g", "group", "show group as well as user"), getopts::optflag("h", "header", "show a header row at the top"), + getopts::optflag("H", "links", "show number of hard links"), + getopts::optflag("l", "long", "display extended details and attributes"), getopts::optflag("i", "inode", "show each file's inode number"), - getopts::optflag("l", "links", "show number of hard links"), getopts::optflag("r", "reverse", "reverse order of files"), getopts::optopt("s", "sort", "field to sort by", "WORD"), getopts::optflag("S", "blocks", "show number of file system blocks"), + getopts::optflag("x", "across", "sort multi-column view entries across"), ]; match getopts::getopts(args.tail(), opts) { Err(f) => Err(f), Ok(matches) => Ok(Options { - showInvisibles: matches.opt_present("all"), + show_invisibles: matches.opt_present("all"), reverse: matches.opt_present("reverse"), header: matches.opt_present("header"), - sortField: matches.opt_str("sort").map(|word| SortField::from_word(word)).unwrap_or(Name), + sort_field: matches.opt_str("sort").map(|word| SortField::from_word(word)).unwrap_or(Name), dirs: if matches.free.is_empty() { vec![ ".".to_string() ] } else { matches.free.clone() }, - columns: Options::columns(matches), + view: Options::view(matches), }) } } - + + fn view(matches: getopts::Matches) -> View { + if matches.opt_present("long") { + Lines(Options::columns(matches)) + } + else { + Grid(matches.opt_present("across")) + } + } + fn columns(matches: getopts::Matches) -> Vec { let mut columns = vec![]; @@ -87,7 +103,7 @@ impl Options { } fn should_display(&self, f: &File) -> bool { - if self.showInvisibles { + if self.show_invisibles { true } else { !f.name.as_slice().starts_with(".") @@ -99,13 +115,13 @@ impl Options { .filter(|&f| self.should_display(f)) .collect(); - match self.sortField { + match self.sort_field { Name => files.sort_by(|a, b| a.parts.cmp(&b.parts)), Size => files.sort_by(|a, b| a.stat.size.cmp(&b.stat.size)), Extension => files.sort_by(|a, b| { let exts = a.ext.clone().map(|e| e.as_slice().to_ascii_lower()).cmp(&b.ext.clone().map(|e| e.as_slice().to_ascii_lower())); let names = a.name.as_slice().to_ascii_lower().cmp(&b.name.as_slice().to_ascii_lower()); - lexical_ordering(exts, names) + exts.cmp(&names) }), } diff --git a/src/term.rs b/src/term.rs new file mode 100644 index 0000000..9e88adf --- /dev/null +++ b/src/term.rs @@ -0,0 +1,57 @@ + +mod c { + #![allow(non_camel_case_types)] + extern crate libc; + pub use self::libc::{ + c_int, + c_ushort, + c_ulong, + STDOUT_FILENO, + }; + use std::mem::zeroed; + + // Getting the terminal size is done using an ioctl command that + // takes the file handle to the terminal (which in our case is + // stdout), and populates a structure with the values. + + pub struct winsize { + pub ws_row: c_ushort, + pub ws_col: c_ushort, + } + + // Unfortunately the actual command is not standardised... + + #[cfg(target_os = "linux")] + #[cfg(target_os = "android")] + static TIOCGWINSZ: c_ulong = 0x5413; + + #[cfg(target_os = "freebsd")] + #[cfg(target_os = "macos")] + static TIOCGWINSZ: c_ulong = 0x40087468; + + extern { + pub fn ioctl(fd: c_int, request: c_ulong, ...) -> c_int; + } + + pub fn dimensions() -> winsize { + unsafe { + let mut window: winsize = zeroed(); + ioctl(STDOUT_FILENO, TIOCGWINSZ, &mut window as *mut winsize); + window + } + } +} + +pub fn dimensions() -> Option<(uint, uint)> { + let w = c::dimensions(); + + // If either of the dimensions is 0 then the command failed, + // usually because output isn't to a terminal (instead to a file + // or pipe or something) + if w.ws_col == 0 || w.ws_row == 0 { + None + } + else { + Some((w.ws_col as uint, w.ws_row as uint)) + } +} diff --git a/src/unix.rs b/src/unix.rs index 4d6d567..b1257ec 100644 --- a/src/unix.rs +++ b/src/unix.rs @@ -39,6 +39,7 @@ mod c { pub fn getuid() -> libc::c_int; } } + pub struct Unix { user_names: HashMap>, // mapping of user IDs to user names group_names: HashMap>, // mapping of groups IDs to group names @@ -50,7 +51,8 @@ pub struct Unix { impl Unix { pub fn empty_cache() -> Unix { let uid = unsafe { c::getuid() }; - let info = unsafe { c::getpwuid(uid as i32).to_option().unwrap() }; // the user has to have a name + let infoptr = unsafe { c::getpwuid(uid as i32) }; + let info = unsafe { infoptr.to_option().unwrap() }; // the user has to have a name let username = unsafe { from_c_str(info.pw_name) };