mirror of
https://github.com/Llewellynvdm/exa.git
synced 2024-11-22 20:15:11 +00:00
Update screenshot to show off awesome new grid view functionality
This commit is contained in:
commit
d15529301f
@ -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
BIN
screenshot.png
Binary file not shown.
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 47 KiB |
67
src/exa.rs
67
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));
|
||||
}
|
||||
}
|
||||
|
22
src/file.rs
22
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()
|
||||
}
|
||||
|
||||
|
@ -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
57
src/term.rs
Normal 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))
|
||||
}
|
||||
}
|
@ -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) };
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user