diff --git a/dir.rs b/dir.rs new file mode 100644 index 0000000..5b4770b --- /dev/null +++ b/dir.rs @@ -0,0 +1,33 @@ +use std::io::fs; +use file::File; + +// The purpose of a Dir is to provide a cached list of the file paths +// in the directory being searched for. This object is then passed to +// the Files themselves, which can then check the status of their +// surrounding files, such as whether it needs to be coloured +// differently if a certain other file exists. + +pub struct Dir<'a> { + contents: Vec, +} + +impl<'a> Dir<'a> { + pub fn readdir(path: Path) -> Dir<'a> { + match fs::readdir(&path) { + Ok(paths) => Dir { + contents: paths, + }, + Err(e) => fail!("readdir: {}", e), + } + } + + pub fn files(&'a self) -> Vec> { + self.contents.iter().map(|path| File::from_path(path, self)).collect() + } + + pub fn contains(&self, path: &Path) -> bool { + self.contents.contains(path) + } +} + + diff --git a/exa.rs b/exa.rs index 8f9838f..d25b0c7 100644 --- a/exa.rs +++ b/exa.rs @@ -3,15 +3,17 @@ extern crate regex; #[phase(syntax)] extern crate regex_macros; use std::os; -use std::io::fs; use file::File; +use dir::Dir; use options::Options; pub mod colours; pub mod column; +pub mod dir; pub mod format; pub mod file; +pub mod filetype; pub mod unix; pub mod options; pub mod sort; @@ -40,13 +42,9 @@ fn main() { } fn exa(options: &Options, path: Path) { - let paths = match fs::readdir(&path) { - Ok(paths) => paths, - Err(e) => fail!("readdir: {}", e), - }; - - let unordered_files: Vec = paths.iter().map(|path| File::from_path(path)).collect(); - let files: Vec<&File> = options.transform_files(&unordered_files); + let dir = Dir::readdir(path); + let unsorted_files = dir.files(); + let files: Vec<&File> = options.transform_files(&unsorted_files); // The output gets formatted into columns, which looks nicer. To // do this, we have to write the results into a table, instead of diff --git a/file.rs b/file.rs index d5c09e5..6ed3139 100644 --- a/file.rs +++ b/file.rs @@ -1,20 +1,13 @@ +use colours::{Plain, Style, Black, Red, Green, Yellow, Blue, Purple, Cyan, Fixed}; use std::io::fs; use std::io; -use colours::{Plain, Style, Black, Red, Green, Yellow, Blue, Purple, Cyan, Fixed}; use column::{Column, Permissions, FileName, FileSize, User, Group}; use format::{format_metric_bytes, format_IEC_bytes}; use unix::{get_user_name, get_group_name}; use sort::SortPart; - -static MEDIA_TYPES: &'static [&'static str] = &[ - "png", "jpeg", "jpg", "gif", "bmp", "tiff", "tif", - "ppm", "pgm", "pbm", "pnm", "webp", "raw", "arw", - "svg", "pdf", "stl", "eps", "dvi", "ps" ]; - -static COMPRESSED_TYPES: &'static [&'static str] = &[ - "zip", "tar", "Z", "gz", "bz2", "a", "ar", "7z", - "iso", "dmg", "tc", "rar", "par" ]; +use dir::Dir; +use filetype::HasType; // Instead of working with Rust's Paths, we have our own File object // that holds the Path and various cached information. Each file is @@ -25,6 +18,7 @@ static COMPRESSED_TYPES: &'static [&'static str] = &[ pub struct File<'a> { pub name: &'a str, + pub dir: &'a Dir<'a>, pub ext: Option<&'a str>, pub path: &'a Path, pub stat: io::FileStat, @@ -32,7 +26,7 @@ pub struct File<'a> { } impl<'a> File<'a> { - pub fn from_path(path: &'a Path) -> File<'a> { + pub fn from_path(path: &'a Path, parent: &'a Dir) -> File<'a> { // Getting the string from a filename fails whenever it's not // UTF-8 representable - just assume it is for now. let filename: &str = path.filename_str().unwrap(); @@ -47,6 +41,7 @@ impl<'a> File<'a> { return File { path: path, + dir: parent, stat: stat, name: filename, ext: File::ext(filename), @@ -66,25 +61,37 @@ impl<'a> File<'a> { self.name.starts_with(".") } - fn is_tmpfile(&self) -> bool { + pub fn is_tmpfile(&self) -> bool { self.name.ends_with("~") || (self.name.starts_with("#") && self.name.ends_with("#")) } + + // Highlight the compiled versions of files. Some of them, like .o, + // get special highlighting when they're alone because there's no + // point in existing without their source. Others can be perfectly + // content without their source files, such as how .js is valid + // without a .coffee. - fn with_extension(&self, newext: &'static str) -> String { - format!("{}.{}", self.path.filestem_str().unwrap(), newext) - } - - fn get_source_files(&self) -> Vec { + pub fn get_source_files(&self) -> Vec { match self.ext { - Some("class") => vec![self.with_extension("java")], // Java - Some("elc") => vec![self.name.chop()], // Emacs Lisp - Some("hi") => vec![self.with_extension("hs")], // Haskell - Some("o") => vec![self.with_extension("c"), self.with_extension("cpp")], // C, C++ - Some("pyc") => vec![self.name.chop()], // Python + Some("class") => vec![self.path.with_extension("java")], // Java + Some("elc") => vec![self.path.with_extension("el")], // Emacs Lisp + Some("hi") => vec![self.path.with_extension("hs")], // Haskell + Some("o") => vec![self.path.with_extension("c"), self.path.with_extension("cpp")], // C, C++ + Some("pyc") => vec![self.path.with_extension("py")], // Python + Some("js") => vec![self.path.with_extension("coffee"), self.path.with_extension("ts")], // CoffeeScript, TypeScript + Some("css") => vec![self.path.with_extension("sass"), self.path.with_extension("less")], // SASS, Less + + Some("aux") => vec![self.path.with_extension("tex")], // TeX: auxiliary file + Some("bbl") => vec![self.path.with_extension("tex")], // BibTeX bibliography file + Some("blg") => vec![self.path.with_extension("tex")], // BibTeX log file + Some("lof") => vec![self.path.with_extension("tex")], // list of figures + Some("log") => vec![self.path.with_extension("tex")], // TeX log file + Some("lot") => vec![self.path.with_extension("tex")], // list of tables + Some("toc") => vec![self.path.with_extension("tex")], // table of contents + _ => vec![], } } - pub fn display(&self, column: &Column) -> String { match *column { Permissions => self.permissions_string(), @@ -130,36 +137,7 @@ impl<'a> File<'a> { } fn file_colour(&self) -> Style { - if self.stat.kind == io::TypeDirectory { - Blue.bold() - } - else if self.stat.perm.contains(io::UserExecute) { - Green.bold() - } - else if self.is_tmpfile() { - Fixed(244).normal() // midway between white and black - should show up as grey on all terminals - } - else if self.name.starts_with("README") { - Yellow.bold().underline() - } - else if self.ext.is_some() && MEDIA_TYPES.iter().any(|&s| s == self.ext.unwrap()) { - Purple.normal() - } - else if self.ext.is_some() && COMPRESSED_TYPES.iter().any(|&s| s == self.ext.unwrap()) { - Red.normal() - } - else { - let source_files = self.get_source_files(); - if source_files.len() == 0 { - Plain - } - else if source_files.iter().any(|filename| Path::new(format!("{}/{}", self.path.dirname_str().unwrap(), filename)).exists()) { - Fixed(244).normal() - } - else { - Fixed(137).normal() - } - } + self.get_type().style() } fn permissions_string(&self) -> String { @@ -189,13 +167,3 @@ impl<'a> File<'a> { } } } - -trait Chop { - fn chop(&self) -> String; -} - -impl<'a> Chop for &'a str { - fn chop(&self) -> String { - self.slice_to(self.len() - 1).to_string() - } -} \ No newline at end of file diff --git a/filetype.rs b/filetype.rs new file mode 100644 index 0000000..c65f2be --- /dev/null +++ b/filetype.rs @@ -0,0 +1,124 @@ +use colours::{Plain, Style, Black, Red, Green, Yellow, Blue, Purple, Cyan, Fixed}; +use file::File; +use std::io; + +pub enum FileType { + Normal, Directory, Executable, Immediate, Compiled, + Image, Video, Music, Lossless, Compressed, Document, Temp, Crypto, +} + +static IMAGE_TYPES: &'static [&'static str] = &[ + "png", "jpeg", "jpg", "gif", "bmp", "tiff", "tif", + "ppm", "pgm", "pbm", "pnm", "webp", "raw", "arw", + "svg", "stl", "eps", "dvi", "ps", "cbr", + "cbz", "xpm", "ico" ]; + +static VIDEO_TYPES: &'static [&'static str] = &[ + "avi", "flv", "m2v", "mkv", "mov", "mp4", "mpeg", + "mpg", "ogm", "ogv", "vob", "wmv" ]; + +static MUSIC_TYPES: &'static [&'static str] = &[ + "aac", "m4a", "mp3", "ogg" ]; + +static MUSIC_LOSSLESS: &'static [&'static str] = &[ + "alac", "ape", "flac", "wav" ]; + +static COMPRESSED_TYPES: &'static [&'static str] = &[ + "zip", "tar", "Z", "gz", "bz2", "a", "ar", "7z", + "iso", "dmg", "tc", "rar", "par" ]; + +static DOCUMENT_TYPES: &'static [&'static str] = &[ + "djvu", "doc", "docx", "eml", "eps", "odp", "ods", + "odt", "pdf", "ppt", "pptx", "xls", "xlsx" ]; + +static TEMP_TYPES: &'static [&'static str] = &[ + "tmp", "swp", "swo", "swn", "bak" ]; + +static CRYPTO_TYPES: &'static [&'static str] = &[ + "asc", "gpg", "sig", "signature", "pgp" ]; + +static COMPILED_TYPES: &'static [&'static str] = &[ + "class", "elc", "hi", "o", "pyc" ]; + +impl FileType { + pub fn style(&self) -> Style { + match *self { + Normal => Plain, + Directory => Blue.bold(), + Executable => Green.bold(), + Image => Fixed(133).normal(), + Video => Fixed(135).normal(), + Music => Fixed(92).normal(), + Lossless => Fixed(93).normal(), + Crypto => Fixed(109).normal(), + Document => Fixed(105).normal(), + Compressed => Red.normal(), + Temp => Fixed(244).normal(), + Immediate => Yellow.bold().underline(), + Compiled => Fixed(137).normal(), + } + } +} + +pub trait HasType { + fn get_type(&self) -> FileType; +} + +impl<'a> HasType for File<'a> { + fn get_type(&self) -> FileType { + if self.stat.kind == io::TypeDirectory { + return Directory; + } + else if self.stat.perm.contains(io::UserExecute) { + return Executable; + } + else if self.name.starts_with("README") { + return Immediate; + } + else if self.ext.is_some() { + let ext = self.ext.unwrap(); + if IMAGE_TYPES.iter().any(|&s| s == ext) { + return Image; + } + else if VIDEO_TYPES.iter().any(|&s| s == ext) { + return Video; + } + else if MUSIC_TYPES.iter().any(|&s| s == ext) { + return Music; + } + else if MUSIC_LOSSLESS.iter().any(|&s| s == ext) { + return Lossless; + } + else if CRYPTO_TYPES.iter().any(|&s| s == ext) { + return Crypto; + } + else if DOCUMENT_TYPES.iter().any(|&s| s == ext) { + return Document; + } + else if COMPRESSED_TYPES.iter().any(|&s| s == ext) { + return Compressed; + } + else if self.is_tmpfile() || TEMP_TYPES.iter().any(|&s| s == ext) { + return Temp; + } + + let source_files = self.get_source_files(); + if source_files.len() == 0 { + return Normal; + } + else if source_files.iter().any(|path| self.dir.contains(path)) { + return Temp; + } + else { + if COMPILED_TYPES.iter().any(|&s| s == ext) { + return Compiled; + } + else { + return Normal; + } + } + } + return Normal; // no filetype + } +} +