exa/file.rs

164 lines
6.1 KiB
Rust
Raw Normal View History

use std::io::fs;
use std::io;
use colours::{Plain, Style, Black, Red, Green, Yellow, Blue, Purple, Cyan};
use column::{Column, Permissions, FileName, FileSize, User, Group};
2014-05-26 19:24:51 +00:00
use format::{format_metric_bytes, format_IEC_bytes};
use unix::{get_user_name, get_group_name};
2014-06-01 10:54:31 +00:00
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" ];
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.
pub struct File<'a> {
pub name: &'a str,
pub ext: Option<&'a str>,
pub path: &'a Path,
pub stat: io::FileStat,
2014-06-01 10:54:31 +00:00
pub parts: Vec<SortPart<'a>>,
}
impl<'a> File<'a> {
pub fn from_path(path: &'a Path) -> 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.
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.
let stat: io::FileStat = match fs::lstat(path) {
Ok(stat) => stat,
Err(e) => fail!("Couldn't stat {}: {}", filename, e),
};
return File {
2014-06-01 10:54:31 +00:00
path: path,
stat: stat,
name: filename,
ext: File::ext(filename),
parts: SortPart::split_into_parts(filename),
};
}
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"\.([^.]+)$");
re.captures(name).map(|caps| caps.at(1))
2014-05-24 01:32:57 +00:00
}
pub fn is_dotfile(&self) -> bool {
self.name.starts_with(".")
}
2014-05-26 10:08:33 +00:00
pub fn display(&self, column: &Column) -> String {
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());
},
Group => get_group_name(self.stat.unstable.gid as u32).unwrap_or(self.stat.unstable.gid.to_str()),
}
}
2014-05-26 19:24:51 +00:00
fn file_size(&self, use_iec_prefixes: bool) -> String {
// 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 {
Black.bold().paint("---")
} else {
2014-05-26 19:24:51 +00:00
let size_str = if use_iec_prefixes {
format_IEC_bytes(self.stat.size)
} else {
2014-05-26 19:24:51 +00:00
format_metric_bytes(self.stat.size)
};
2014-05-26 19:24:51 +00:00
return Green.bold().paint(size_str.as_slice());
}
}
2014-05-26 10:08:33 +00:00
fn type_char(&self) -> String {
return match self.stat.kind {
2014-05-26 19:24:51 +00:00
io::TypeFile => ".".to_strbuf(),
io::TypeDirectory => Blue.paint("d"),
io::TypeNamedPipe => Yellow.paint("|"),
io::TypeBlockSpecial => Purple.paint("s"),
2014-05-26 19:24:51 +00:00
io::TypeSymlink => Cyan.paint("l"),
_ => "?".to_owned(),
}
}
fn file_colour(&self) -> Style {
if self.stat.kind == io::TypeDirectory {
Blue.normal()
}
else if self.stat.perm.contains(io::UserExecute) {
Green.bold()
}
else if self.name.ends_with("~") {
Black.bold()
}
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 {
Plain
}
}
2014-05-26 19:24:51 +00:00
fn permissions_string(&self) -> String {
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-26 19:24:51 +00:00
fn permission_bit(bits: io::FilePermission, bit: io::FilePermission, character: &'static str, style: Style) -> String {
if bits.contains(bit) {
2014-05-26 19:24:51 +00:00
style.paint(character.as_slice())
} else {
Black.bold().paint("-".as_slice())
}
}
}