2014-05-04 20:33:14 +00:00
|
|
|
use std::io::fs;
|
|
|
|
use std::io;
|
|
|
|
|
2014-06-16 11:43:34 +00:00
|
|
|
use colours::{Plain, Style, Black, Red, Green, Yellow, Blue, Purple, Cyan, Fixed};
|
2014-05-05 10:29:50 +00:00
|
|
|
use column::{Column, Permissions, FileName, FileSize, User, Group};
|
2014-05-26 19:24:51 +00:00
|
|
|
use format::{format_metric_bytes, format_IEC_bytes};
|
2014-05-05 10:29:50 +00:00
|
|
|
use unix::{get_user_name, get_group_name};
|
2014-06-01 10:54:31 +00:00
|
|
|
use sort::SortPart;
|
2014-06-16 23:27:05 +00:00
|
|
|
use dir::Dir;
|
2014-05-04 20:33:14 +00:00
|
|
|
|
2014-06-16 22:34:22 +00:00
|
|
|
static IMAGE_TYPES: &'static [&'static str] = &[
|
2014-05-24 19:40:31 +00:00
|
|
|
"png", "jpeg", "jpg", "gif", "bmp", "tiff", "tif",
|
|
|
|
"ppm", "pgm", "pbm", "pnm", "webp", "raw", "arw",
|
2014-06-16 22:34:22 +00:00
|
|
|
"svg", "pdf", "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" ];
|
2014-05-24 19:40:31 +00:00
|
|
|
|
|
|
|
static COMPRESSED_TYPES: &'static [&'static str] = &[
|
|
|
|
"zip", "tar", "Z", "gz", "bz2", "a", "ar", "7z",
|
|
|
|
"iso", "dmg", "tc", "rar", "par" ];
|
|
|
|
|
2014-06-16 22:34:22 +00:00
|
|
|
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" ];
|
|
|
|
|
2014-06-16 23:27:05 +00:00
|
|
|
|
2014-05-26 19:24:51 +00:00
|
|
|
// Instead of working with Rust's Paths, we have our own File object
|
|
|
|
// that holds the Path and various cached information. Each file is
|
|
|
|
// definitely going to have its filename used at least once, its stat
|
|
|
|
// information queried at least once, and its file extension extracted
|
|
|
|
// at least once, so we may as well carry around that information with
|
|
|
|
// the actual path.
|
|
|
|
|
2014-05-04 20:33:14 +00:00
|
|
|
pub struct File<'a> {
|
2014-05-24 01:17:43 +00:00
|
|
|
pub name: &'a str,
|
2014-06-16 23:27:05 +00:00
|
|
|
pub dir: &'a Dir<'a>,
|
2014-05-24 01:36:00 +00:00
|
|
|
pub ext: Option<&'a str>,
|
2014-05-24 01:17:43 +00:00
|
|
|
pub path: &'a Path,
|
|
|
|
pub stat: io::FileStat,
|
2014-06-01 15:30:18 +00:00
|
|
|
pub parts: Vec<SortPart>,
|
2014-05-04 20:33:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> File<'a> {
|
2014-06-16 23:27:05 +00:00
|
|
|
pub fn from_path(path: &'a Path, parent: &'a Dir) -> File<'a> {
|
2014-05-26 19:24:51 +00:00
|
|
|
// Getting the string from a filename fails whenever it's not
|
|
|
|
// UTF-8 representable - just assume it is for now.
|
2014-05-04 20:33:14 +00:00
|
|
|
let filename: &str = path.filename_str().unwrap();
|
|
|
|
|
2014-05-26 19:24:51 +00:00
|
|
|
// Use lstat here instead of file.stat(), as it doesn't follow
|
|
|
|
// symbolic links. Otherwise, the stat() call will fail if it
|
|
|
|
// encounters a link that's target is non-existent.
|
2014-05-04 20:33:14 +00:00
|
|
|
let stat: io::FileStat = match fs::lstat(path) {
|
|
|
|
Ok(stat) => stat,
|
|
|
|
Err(e) => fail!("Couldn't stat {}: {}", filename, e),
|
|
|
|
};
|
|
|
|
|
2014-05-24 01:36:00 +00:00
|
|
|
return File {
|
2014-06-01 10:54:31 +00:00
|
|
|
path: path,
|
2014-06-16 23:27:05 +00:00
|
|
|
dir: parent,
|
2014-06-01 10:54:31 +00:00
|
|
|
stat: stat,
|
|
|
|
name: filename,
|
|
|
|
ext: File::ext(filename),
|
|
|
|
parts: SortPart::split_into_parts(filename),
|
2014-05-24 01:36:00 +00:00
|
|
|
};
|
2014-05-04 20:33:14 +00:00
|
|
|
}
|
|
|
|
|
2014-05-24 01:36:00 +00:00
|
|
|
fn ext(name: &'a str) -> Option<&'a str> {
|
2014-05-26 19:24:51 +00:00
|
|
|
// The extension is the series of characters after a dot at
|
|
|
|
// the end of a filename. This deliberately also counts
|
|
|
|
// dotfiles - the ".git" folder has the extension "git".
|
|
|
|
let re = regex!(r"\.([^.]+)$");
|
2014-05-24 01:36:00 +00:00
|
|
|
re.captures(name).map(|caps| caps.at(1))
|
2014-05-24 01:32:57 +00:00
|
|
|
}
|
|
|
|
|
2014-05-05 09:51:24 +00:00
|
|
|
pub fn is_dotfile(&self) -> bool {
|
|
|
|
self.name.starts_with(".")
|
|
|
|
}
|
|
|
|
|
2014-06-16 11:43:34 +00:00
|
|
|
fn is_tmpfile(&self) -> bool {
|
|
|
|
self.name.ends_with("~") || (self.name.starts_with("#") && self.name.ends_with("#"))
|
|
|
|
}
|
2014-06-16 23:27:05 +00:00
|
|
|
|
2014-06-16 22:34:22 +00:00
|
|
|
// 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.
|
|
|
|
|
2014-06-16 23:27:05 +00:00
|
|
|
fn get_source_files(&self) -> Vec<Path> {
|
2014-06-16 20:20:09 +00:00
|
|
|
match self.ext {
|
2014-06-16 23:27:05 +00:00
|
|
|
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
|
2014-06-16 20:20:09 +00:00
|
|
|
_ => vec![],
|
|
|
|
}
|
|
|
|
}
|
2014-06-16 22:34:22 +00:00
|
|
|
|
2014-06-16 23:27:05 +00:00
|
|
|
fn get_source_files_usual(&self) -> Vec<Path> {
|
2014-06-16 22:34:22 +00:00
|
|
|
match self.ext {
|
2014-06-16 23:27:05 +00:00
|
|
|
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
|
2014-06-16 22:34:22 +00:00
|
|
|
|
2014-06-16 23:27:05 +00:00
|
|
|
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
|
2014-06-16 11:43:34 +00:00
|
|
|
|
2014-06-16 22:34:22 +00:00
|
|
|
_ => vec![],
|
|
|
|
}
|
|
|
|
}
|
2014-05-26 10:08:33 +00:00
|
|
|
pub fn display(&self, column: &Column) -> String {
|
2014-05-04 20:33:14 +00:00
|
|
|
match *column {
|
2014-05-26 19:24:51 +00:00
|
|
|
Permissions => self.permissions_string(),
|
2014-06-01 10:54:31 +00:00
|
|
|
FileName => self.file_colour().paint(self.name),
|
2014-05-26 19:24:51 +00:00
|
|
|
FileSize(use_iec) => self.file_size(use_iec),
|
|
|
|
|
|
|
|
// Display the ID if the user/group doesn't exist, which
|
|
|
|
// usually means it was deleted but its files weren't.
|
2014-05-27 18:05:15 +00:00
|
|
|
User(uid) => {
|
|
|
|
let style = if uid == self.stat.unstable.uid { Yellow.bold() } else { Plain };
|
|
|
|
let string = get_user_name(self.stat.unstable.uid as i32).unwrap_or(self.stat.unstable.uid.to_str());
|
|
|
|
return style.paint(string.as_slice());
|
|
|
|
},
|
2014-05-22 00:18:39 +00:00
|
|
|
Group => get_group_name(self.stat.unstable.gid as u32).unwrap_or(self.stat.unstable.gid.to_str()),
|
2014-05-04 20:33:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-05-26 19:24:51 +00:00
|
|
|
fn file_size(&self, use_iec_prefixes: bool) -> String {
|
2014-05-25 14:52:36 +00:00
|
|
|
// Don't report file sizes for directories. I've never looked
|
|
|
|
// at one of those numbers and gained any information from it.
|
|
|
|
if self.stat.kind == io::TypeDirectory {
|
2014-06-03 20:14:35 +00:00
|
|
|
Black.bold().paint("-")
|
2014-05-04 20:33:14 +00:00
|
|
|
} else {
|
2014-06-03 20:20:17 +00:00
|
|
|
let (size, suffix) = if use_iec_prefixes {
|
2014-05-26 19:24:51 +00:00
|
|
|
format_IEC_bytes(self.stat.size)
|
2014-05-25 14:52:36 +00:00
|
|
|
} else {
|
2014-05-26 19:24:51 +00:00
|
|
|
format_metric_bytes(self.stat.size)
|
2014-05-25 14:52:36 +00:00
|
|
|
};
|
2014-05-04 20:33:14 +00:00
|
|
|
|
2014-06-03 20:20:17 +00:00
|
|
|
return format!("{}{}", Green.bold().paint(size.as_slice()), Green.paint(suffix.as_slice()));
|
2014-05-25 14:52:36 +00:00
|
|
|
}
|
2014-05-04 20:33:14 +00:00
|
|
|
}
|
|
|
|
|
2014-05-26 10:08:33 +00:00
|
|
|
fn type_char(&self) -> String {
|
2014-05-04 20:33:14 +00:00
|
|
|
return match self.stat.kind {
|
2014-06-02 20:02:06 +00:00
|
|
|
io::TypeFile => ".".to_string(),
|
2014-05-26 19:24:51 +00:00
|
|
|
io::TypeDirectory => Blue.paint("d"),
|
|
|
|
io::TypeNamedPipe => Yellow.paint("|"),
|
2014-05-04 20:33:14 +00:00
|
|
|
io::TypeBlockSpecial => Purple.paint("s"),
|
2014-05-26 19:24:51 +00:00
|
|
|
io::TypeSymlink => Cyan.paint("l"),
|
2014-06-02 20:02:06 +00:00
|
|
|
_ => "?".to_string(),
|
2014-05-04 20:33:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn file_colour(&self) -> Style {
|
|
|
|
if self.stat.kind == io::TypeDirectory {
|
2014-06-16 11:43:34 +00:00
|
|
|
Blue.bold()
|
2014-05-24 19:40:31 +00:00
|
|
|
}
|
|
|
|
else if self.stat.perm.contains(io::UserExecute) {
|
|
|
|
Green.bold()
|
|
|
|
}
|
2014-06-16 11:43:34 +00:00
|
|
|
else if self.is_tmpfile() {
|
|
|
|
Fixed(244).normal() // midway between white and black - should show up as grey on all terminals
|
2014-05-24 19:40:31 +00:00
|
|
|
}
|
|
|
|
else if self.name.starts_with("README") {
|
|
|
|
Yellow.bold().underline()
|
|
|
|
}
|
2014-06-16 22:34:22 +00:00
|
|
|
else if self.ext.is_some() && IMAGE_TYPES.iter().any(|&s| s == self.ext.unwrap()) {
|
|
|
|
Fixed(133).normal()
|
|
|
|
}
|
|
|
|
else if self.ext.is_some() && VIDEO_TYPES.iter().any(|&s| s == self.ext.unwrap()) {
|
|
|
|
Fixed(135).normal()
|
|
|
|
}
|
|
|
|
else if self.ext.is_some() && MUSIC_TYPES.iter().any(|&s| s == self.ext.unwrap()) {
|
|
|
|
Fixed(92).normal()
|
|
|
|
}
|
|
|
|
else if self.ext.is_some() && MUSIC_LOSSLESS.iter().any(|&s| s == self.ext.unwrap()) {
|
|
|
|
Fixed(93).normal()
|
|
|
|
}
|
|
|
|
else if self.ext.is_some() && CRYPTO_TYPES.iter().any(|&s| s == self.ext.unwrap()) {
|
|
|
|
Fixed(109).normal()
|
|
|
|
}
|
|
|
|
else if self.ext.is_some() && DOCUMENT_TYPES.iter().any(|&s| s == self.ext.unwrap()) {
|
|
|
|
Fixed(105).normal()
|
2014-05-24 19:40:31 +00:00
|
|
|
}
|
|
|
|
else if self.ext.is_some() && COMPRESSED_TYPES.iter().any(|&s| s == self.ext.unwrap()) {
|
|
|
|
Red.normal()
|
|
|
|
}
|
2014-06-16 22:34:22 +00:00
|
|
|
else if self.ext.is_some() && TEMP_TYPES.iter().any(|&s| s == self.ext.unwrap()) {
|
|
|
|
Fixed(244).normal()
|
|
|
|
}
|
2014-05-24 19:40:31 +00:00
|
|
|
else {
|
2014-06-16 20:20:09 +00:00
|
|
|
let source_files = self.get_source_files();
|
|
|
|
if source_files.len() == 0 {
|
2014-06-16 22:34:22 +00:00
|
|
|
let source_files_usual = self.get_source_files_usual();
|
2014-06-16 23:27:05 +00:00
|
|
|
if source_files_usual.iter().any(|path| self.dir.contains(path)) {
|
2014-06-16 22:34:22 +00:00
|
|
|
Fixed(244).normal()
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
Plain
|
|
|
|
}
|
2014-06-16 20:20:09 +00:00
|
|
|
}
|
2014-06-16 23:27:05 +00:00
|
|
|
else if source_files.iter().any(|path| self.dir.contains(path)) {
|
2014-06-16 20:20:09 +00:00
|
|
|
Fixed(244).normal()
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
Fixed(137).normal()
|
|
|
|
}
|
2014-05-04 20:33:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-05-26 19:24:51 +00:00
|
|
|
fn permissions_string(&self) -> String {
|
2014-05-04 20:33:14 +00:00
|
|
|
let bits = self.stat.perm;
|
|
|
|
return format!("{}{}{}{}{}{}{}{}{}{}",
|
|
|
|
self.type_char(),
|
2014-05-26 19:24:51 +00:00
|
|
|
|
|
|
|
// The first three are bold because they're the ones used
|
|
|
|
// most often.
|
|
|
|
File::permission_bit(bits, io::UserRead, "r", Yellow.bold()),
|
|
|
|
File::permission_bit(bits, io::UserWrite, "w", Red.bold()),
|
|
|
|
File::permission_bit(bits, io::UserExecute, "x", Green.bold().underline()),
|
|
|
|
File::permission_bit(bits, io::GroupRead, "r", Yellow.normal()),
|
|
|
|
File::permission_bit(bits, io::GroupWrite, "w", Red.normal()),
|
|
|
|
File::permission_bit(bits, io::GroupExecute, "x", Green.normal()),
|
|
|
|
File::permission_bit(bits, io::OtherRead, "r", Yellow.normal()),
|
|
|
|
File::permission_bit(bits, io::OtherWrite, "w", Red.normal()),
|
|
|
|
File::permission_bit(bits, io::OtherExecute, "x", Green.normal()),
|
2014-05-04 20:33:14 +00:00
|
|
|
);
|
|
|
|
}
|
2014-05-26 10:50:46 +00:00
|
|
|
|
2014-05-26 19:24:51 +00:00
|
|
|
fn permission_bit(bits: io::FilePermission, bit: io::FilePermission, character: &'static str, style: Style) -> String {
|
2014-05-26 10:50:46 +00:00
|
|
|
if bits.contains(bit) {
|
2014-05-26 19:24:51 +00:00
|
|
|
style.paint(character.as_slice())
|
2014-05-26 10:50:46 +00:00
|
|
|
} else {
|
|
|
|
Black.bold().paint("-".as_slice())
|
|
|
|
}
|
|
|
|
}
|
2014-05-04 20:33:14 +00:00
|
|
|
}
|