Ver Fonte

Update screenshot to show off awesome new grid view functionality

Ben S há 11 anos atrás
pai
commit
d15529301f
7 ficheiros alterados com 168 adições e 41 exclusões
  1. 4 3
      README.md
  2. BIN
      screenshot.png
  3. 57 10
      src/exa.rs
  4. 13 9
      src/file.rs
  5. 34 18
      src/options.rs
  6. 57 0
      src/term.rs
  7. 3 1
      src/unix.rs

+ 4 - 3
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.

BIN
screenshot.png


+ 57 - 10
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<Column>, 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<Vec<String>> = 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<uint> = range(0, options.columns.len())
-        .map(|n| lengths.iter().map(|row| *row.get(n)).max().unwrap())
+    let column_widths: Vec<uint> = 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));
             }
         }

+ 13 - 9
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<File<'a>> {
         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()
     }
 

+ 34 - 18
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<String>,
-    pub columns: Vec<Column>,
-    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<Column>),
+    Grid(bool),
+}
+
+pub struct Options {
+    pub show_invisibles: bool,
+    pub sort_field: SortField,
+    pub reverse: bool,
+    pub dirs: Vec<String>,
+    pub view: View,
+    pub header: bool,
+}
+
+
 impl Options {
     pub fn getopts(args: Vec<String>) -> Result<Options, getopts::Fail_> {
         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<Column> {
         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)
             }),
         }
 

+ 57 - 0
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))
+    }
+}

+ 3 - 1
src/unix.rs

@@ -39,6 +39,7 @@ mod c {
         pub fn getuid() -> libc::c_int;
     }
 }
+
 pub struct Unix {
     user_names:    HashMap<u32, Option<String>>,  // mapping of user IDs to user names
     group_names:   HashMap<u32, Option<String>>,  // 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) };