Update screenshot to show off awesome new grid view functionality

This commit is contained in:
Ben S 2014-07-22 15:50:41 +01:00
commit d15529301f
7 changed files with 168 additions and 41 deletions

View File

@ -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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -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));
}
}

View File

@ -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()
}

View File

@ -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
src/term.rs Normal file
View File

@ -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))
}
}

View File

@ -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) };