diff --git a/Cargo.toml b/Cargo.toml index 9bac973..ec548fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,11 @@ authors = [ "ogham@bsago.me" ] [[bin]] name = "exa" +path = "src/bin/main.rs" + +[lib] +name = "exa" +path = "src/exa.rs" [dependencies] ansi_term = "0.7.1" diff --git a/src/bin/main.rs b/src/bin/main.rs new file mode 100644 index 0000000..491d793 --- /dev/null +++ b/src/bin/main.rs @@ -0,0 +1,19 @@ +extern crate exa; +use exa::Exa; + +use std::env::args; +use std::io::stdout; +use std::process::exit; + +fn main() { + let args: Vec = args().skip(1).collect(); + let mut stdout = stdout(); + + match Exa::new(&args, &mut stdout) { + Ok(mut exa) => exa.run().expect("IO error"), + Err(e) => { + println!("{}", e); + exit(e.error_code()); + }, + }; +} diff --git a/src/main.rs b/src/exa.rs similarity index 77% rename from src/main.rs rename to src/exa.rs index 325b04f..be3bd85 100644 --- a/src/main.rs +++ b/src/exa.rs @@ -19,13 +19,13 @@ extern crate zoneinfo_compiled; #[cfg(feature="git")] extern crate git2; #[macro_use] extern crate lazy_static; -use std::env; -use std::io::{Write, stdout, Result as IOResult}; +use std::ffi::OsStr; +use std::io::{Write, Result as IOResult}; use std::path::{Component, Path}; -use std::process; use fs::{Dir, File}; use options::{Options, View}; +pub use options::Misfire; mod fs; mod info; @@ -35,27 +35,41 @@ mod term; /// The main program wrapper. -struct Exa<'w, W: Write + 'w> { +pub struct Exa<'w, W: Write + 'w> { /// List of command-line options, having been successfully parsed. - options: Options, + pub options: Options, /// The output handle that we write to. When running the program normally, /// this will be `std::io::Stdout`, but it can accept any struct that’s /// `Write` so we can write into, say, a vector for testing. - writer: &'w mut W, + pub writer: &'w mut W, + + /// List of the free command-line arguments that should correspond to file + /// names (anything that isn’t an option). + pub args: Vec, } impl<'w, W: Write + 'w> Exa<'w, W> { - fn run(&mut self, mut args_file_names: Vec) -> IOResult<()> { + pub fn new(args: &[S], writer: &'w mut W) -> Result, Misfire> + where S: AsRef { + Options::getopts(args).map(move |(opts, args)| Exa { + options: opts, + writer: writer, + args: args, + }) + } + + pub fn run(&mut self) -> IOResult<()> { let mut files = Vec::new(); let mut dirs = Vec::new(); - if args_file_names.is_empty() { - args_file_names.push(".".to_owned()); + // List the current directory by default, like ls. + if self.args.is_empty() { + self.args.push(".".to_owned()); } - for file_name in args_file_names.iter() { + for file_name in self.args.iter() { match File::from_path(Path::new(&file_name), None) { Err(e) => { try!(writeln!(self.writer, "{}: {}", file_name, e)); @@ -74,6 +88,10 @@ impl<'w, W: Write + 'w> Exa<'w, W> { } } + // We want to print a directory’s name before we list it, *except* in + // the case where it’s the only directory, *except* if there are any + // files to print as well. (It’s a double negative) + let no_files = files.is_empty(); let is_only_dir = dirs.len() == 1 && no_files; @@ -87,8 +105,8 @@ impl<'w, W: Write + 'w> Exa<'w, W> { fn print_dirs(&mut self, dir_files: Vec, mut first: bool, is_only_dir: bool) -> IOResult<()> { for dir in dir_files { - // Put a gap between directories, or between the list of files and the - // first directory. + // Put a gap between directories, or between the list of files and + // the first directory. if first { first = false; } @@ -139,6 +157,9 @@ impl<'w, W: Write + 'w> Exa<'w, W> { Ok(()) } + /// Prints the list of files using whichever view is selected. + /// For various annoying logistical reasons, each one handles + /// printing differently... fn print_files(&mut self, dir: Option<&Dir>, files: Vec) -> IOResult<()> { match self.options.view { View::Grid(g) => g.view(&files, self.writer), @@ -148,20 +169,3 @@ impl<'w, W: Write + 'w> Exa<'w, W> { } } } - - -fn main() { - let args: Vec = env::args().skip(1).collect(); - - match Options::getopts(&args) { - Ok((options, paths)) => { - let mut stdout = stdout(); - let mut exa = Exa { options: options, writer: &mut stdout }; - exa.run(paths).expect("IO error"); - }, - Err(e) => { - println!("{}", e); - process::exit(e.error_code()); - }, - }; -} diff --git a/src/output/details.rs b/src/output/details.rs index a6ebb65..f6837c1 100644 --- a/src/output/details.rs +++ b/src/output/details.rs @@ -12,6 +12,7 @@ //! You will probably recognise it from the `ls --long` command. It looks like //! this: //! +//! ```text //! .rw-r--r-- 9.6k ben 29 Jun 16:16 Cargo.lock //! .rw-r--r-- 547 ben 23 Jun 10:54 Cargo.toml //! .rw-r--r-- 1.1k ben 23 Nov 2014 LICENCE @@ -19,6 +20,7 @@ //! .rw-r--r-- 382k ben 8 Jun 21:00 screenshot.png //! drwxr-xr-x - ben 29 Jun 14:50 src //! drwxr-xr-x - ben 28 Jun 19:53 target +//! ``` //! //! The table is constructed by creating a `Table` value, which produces a `Row` //! value for each file. These rows can contain a vector of `Cell`s, or they can @@ -41,6 +43,7 @@ //! //! To illustrate the above: //! +//! ```text //! ┌─────────────────────────────────────────────────────────────────────────┐ //! │ columns: [ Permissions, Size, User, Date(Modified) ] │ //! ├─────────────────────────────────────────────────────────────────────────┤ @@ -50,6 +53,7 @@ //! │ row 3: [ "drwxr-xr-x", "-", "ben", "29 Jun 14:50" ] src │ //! │ row 4: [ "drwxr-xr-x", "-", "ben", "28 Jun 19:53" ] target │ //! └─────────────────────────────────────────────────────────────────────────┘ +//! ``` //! //! Each column in the table needs to be resized to fit its widest argument. This //! means that we must wait until every row has been added to the table before it @@ -61,11 +65,13 @@ //! Finally, files' extended attributes and any errors that occur while statting //! them can also be displayed as their children. It looks like this: //! +//! ```text //! .rw-r--r-- 0 ben 3 Sep 13:26 forbidden //! └── //! .rw-r--r--@ 0 ben 3 Sep 13:26 file_with_xattrs //! ├── another_greeting (len 2) //! └── greeting (len 5) +//! ``` //! //! These lines also have `None` cells, and the error string or attribute details //! are used in place of the filename. diff --git a/src/output/tree.rs b/src/output/tree.rs index 1cac117..db01cff 100644 --- a/src/output/tree.rs +++ b/src/output/tree.rs @@ -12,6 +12,7 @@ //! affect the file’s information; it’s just used to display a different set of //! Unicode tree characters! The resulting table looks like this: //! +//! ```text //! ┌───────┬───────┬───────────────────────┐ //! │ Depth │ Last │ Output │ //! ├───────┼───────┼───────────────────────┤ @@ -28,6 +29,7 @@ //! │ 2 │ false │ ├── library.png │ //! │ 2 │ true │ └── space.tiff │ //! └───────┴───────┴───────────────────────┘ +//! ``` //! //! Creating the table like this means that each file has to be tested to see //! if it’s the last one in the group. This is usually done by putting all the diff --git a/tests/basic.rs b/tests/basic.rs new file mode 100644 index 0000000..a1162fa --- /dev/null +++ b/tests/basic.rs @@ -0,0 +1,36 @@ +extern crate exa; +use exa::Exa; + +/// --------------------------------------------------------------------------- +/// These tests assume that the ‘generate annoying testcases’ script has been +/// run first. Otherwise, they will break! +/// --------------------------------------------------------------------------- + + +static DIRECTORIES: &'static str = concat!( + "\x1B[1;34m", "attributes", "\x1B[0m", '\n', + "\x1B[1;34m", "links", "\x1B[0m", '\n', + "\x1B[1;34m", "passwd", "\x1B[0m", '\n', + "\x1B[1;34m", "permissions", "\x1B[0m", '\n', +); + +#[test] +fn directories() { + let mut output = Vec::::new(); + Exa::new( &[ "-1", "testcases" ], &mut output).unwrap().run().unwrap(); + assert_eq!(output, DIRECTORIES.as_bytes()); +} + + +static PERMISSIONS: &'static str = concat!( + "\x1B[1;32m", "all-permissions", "\x1B[0m", '\n', + "\x1B[1;34m", "forbidden-directory", "\x1B[0m", '\n', + "no-permissions", '\n', +); + +#[test] +fn permissions() { + let mut output = Vec::::new(); + Exa::new( &[ "-1", "testcases/permissions" ], &mut output).unwrap().run().unwrap(); + assert_eq!(output, PERMISSIONS.as_bytes()); +}