2014-06-20 20:07:53 +00:00
|
|
|
use std::io::{fs, IoResult};
|
2014-05-04 20:33:14 +00:00
|
|
|
use std::io;
|
2014-12-12 14:06:48 +00:00
|
|
|
use std::str::CowString;
|
2014-05-04 20:33:14 +00:00
|
|
|
|
2014-12-02 14:20:28 +00:00
|
|
|
use ansi_term::{ANSIString, Colour, Style};
|
|
|
|
use ansi_term::Style::Plain;
|
|
|
|
use ansi_term::Colour::{Red, Green, Yellow, Blue, Purple, Cyan, Fixed};
|
2014-07-01 18:00:36 +00:00
|
|
|
|
2014-12-12 14:13:08 +00:00
|
|
|
use users::Users;
|
2014-12-12 11:17:55 +00:00
|
|
|
|
2014-12-18 07:00:31 +00:00
|
|
|
use number_prefix::{binary_prefix, decimal_prefix, Prefixed, Standalone, PrefixNames};
|
|
|
|
|
|
|
|
use column::{Column, SizeFormat};
|
2014-11-23 21:29:11 +00:00
|
|
|
use column::Column::*;
|
2014-06-16 23:27:05 +00:00
|
|
|
use dir::Dir;
|
2014-06-17 21:17:22 +00:00
|
|
|
use filetype::HasType;
|
2014-06-16 23:27:05 +00:00
|
|
|
|
2014-11-23 21:29:11 +00:00
|
|
|
pub static GREY: Colour = Fixed(244);
|
2014-07-01 18:00:36 +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-06-29 20:02:14 +00:00
|
|
|
pub name: String,
|
2014-11-26 07:40:52 +00:00
|
|
|
pub dir: Option<&'a Dir>,
|
2014-06-29 20:02:14 +00:00
|
|
|
pub ext: Option<String>,
|
2014-11-24 17:03:36 +00:00
|
|
|
pub path: Path,
|
2014-06-21 09:11:50 +00:00
|
|
|
pub stat: io::FileStat,
|
2014-05-04 20:33:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> File<'a> {
|
2014-12-12 14:06:48 +00:00
|
|
|
pub fn from_path(path: &Path, parent: Option<&'a Dir>) -> IoResult<File<'a>> {
|
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-12-12 14:06:48 +00:00
|
|
|
fs::lstat(path).map(|stat| File::with_stat(stat, path, parent))
|
2014-11-25 01:27:26 +00:00
|
|
|
}
|
2014-11-25 20:50:23 +00:00
|
|
|
|
2014-12-12 14:06:48 +00:00
|
|
|
pub fn with_stat(stat: io::FileStat, path: &Path, parent: Option<&'a Dir>) -> File<'a> {
|
2014-12-12 11:17:55 +00:00
|
|
|
let v = path.filename().unwrap(); // fails if / or . or ..
|
2014-12-12 14:06:48 +00:00
|
|
|
let filename = String::from_utf8_lossy(v);
|
2014-06-21 18:39:27 +00:00
|
|
|
|
2014-12-12 11:17:55 +00:00
|
|
|
File {
|
2014-11-24 17:03:36 +00:00
|
|
|
path: path.clone(),
|
2014-06-16 23:27:05 +00:00
|
|
|
dir: parent,
|
2014-06-01 10:54:31 +00:00
|
|
|
stat: stat,
|
2014-12-12 14:06:48 +00:00
|
|
|
name: filename.to_string(),
|
|
|
|
ext: File::ext(filename),
|
2014-12-12 11:17:55 +00:00
|
|
|
}
|
2014-05-04 20:33:14 +00:00
|
|
|
}
|
|
|
|
|
2014-12-12 14:06:48 +00:00
|
|
|
fn ext(name: CowString) -> Option<String> {
|
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-06-29 20:02:14 +00:00
|
|
|
re.captures(name.as_slice()).map(|caps| caps.at(1).to_string())
|
2014-05-24 01:32:57 +00:00
|
|
|
}
|
|
|
|
|
2014-05-05 09:51:24 +00:00
|
|
|
pub fn is_dotfile(&self) -> bool {
|
2014-06-29 20:02:14 +00:00
|
|
|
self.name.as_slice().starts_with(".")
|
2014-05-05 09:51:24 +00:00
|
|
|
}
|
|
|
|
|
2014-06-17 08:35:40 +00:00
|
|
|
pub fn is_tmpfile(&self) -> bool {
|
2014-06-29 20:02:14 +00:00
|
|
|
let name = self.name.as_slice();
|
|
|
|
name.ends_with("~") || (name.starts_with("#") && name.ends_with("#"))
|
2014-06-16 11:43:34 +00:00
|
|
|
}
|
2014-06-21 18:39:27 +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-21 18:39:27 +00:00
|
|
|
|
2014-06-17 08:35:40 +00:00
|
|
|
pub fn get_source_files(&self) -> Vec<Path> {
|
2014-12-12 14:06:48 +00:00
|
|
|
if let Some(ref ext) = self.ext {
|
|
|
|
let ext = ext.as_slice();
|
|
|
|
match ext {
|
|
|
|
"class" => vec![self.path.with_extension("java")], // Java
|
|
|
|
"css" => vec![self.path.with_extension("sass"), self.path.with_extension("less")], // SASS, Less
|
|
|
|
"elc" => vec![self.path.with_extension("el")], // Emacs Lisp
|
|
|
|
"hi" => vec![self.path.with_extension("hs")], // Haskell
|
|
|
|
"js" => vec![self.path.with_extension("coffee"), self.path.with_extension("ts")], // CoffeeScript, TypeScript
|
|
|
|
"o" => vec![self.path.with_extension("c"), self.path.with_extension("cpp")], // C, C++
|
|
|
|
"pyc" => vec![self.path.with_extension("py")], // Python
|
|
|
|
|
|
|
|
"aux" => vec![self.path.with_extension("tex")], // TeX: auxiliary file
|
|
|
|
"bbl" => vec![self.path.with_extension("tex")], // BibTeX bibliography file
|
|
|
|
"blg" => vec![self.path.with_extension("tex")], // BibTeX log file
|
|
|
|
"lof" => vec![self.path.with_extension("tex")], // TeX list of figures
|
|
|
|
"log" => vec![self.path.with_extension("tex")], // TeX log file
|
|
|
|
"lot" => vec![self.path.with_extension("tex")], // TeX list of tables
|
|
|
|
"toc" => vec![self.path.with_extension("tex")], // TeX table of contents
|
|
|
|
|
|
|
|
_ => vec![], // No source files if none of the above
|
|
|
|
}
|
2014-06-30 01:00:05 +00:00
|
|
|
}
|
2014-12-12 14:06:48 +00:00
|
|
|
else {
|
|
|
|
vec![] // No source files if there's no extension, either!
|
2014-06-16 22:34:22 +00:00
|
|
|
}
|
|
|
|
}
|
2014-06-21 18:39:27 +00:00
|
|
|
|
2014-12-12 14:13:08 +00:00
|
|
|
pub fn display<U: Users>(&self, column: &Column, users_cache: &mut U) -> String {
|
2014-05-04 20:33:14 +00:00
|
|
|
match *column {
|
2014-07-22 21:27:37 +00:00
|
|
|
Permissions => {
|
|
|
|
self.permissions_string()
|
|
|
|
},
|
2014-11-25 20:50:23 +00:00
|
|
|
|
2014-07-22 21:27:37 +00:00
|
|
|
FileName => {
|
|
|
|
self.file_name()
|
|
|
|
},
|
2014-11-25 20:50:23 +00:00
|
|
|
|
2014-07-22 21:27:37 +00:00
|
|
|
FileSize(use_iec) => {
|
|
|
|
self.file_size(use_iec)
|
|
|
|
},
|
2014-06-26 11:46:40 +00:00
|
|
|
|
|
|
|
// A file with multiple links is interesting, but
|
|
|
|
// directories and suchlike can have multiple links all
|
|
|
|
// the time.
|
|
|
|
HardLinks => {
|
2014-07-22 21:27:37 +00:00
|
|
|
let style = if self.has_multiple_links() { Red.on(Yellow) } else { Red.normal() };
|
2014-11-26 07:36:09 +00:00
|
|
|
style.paint(self.stat.unstable.nlink.to_string().as_slice()).to_string()
|
2014-06-26 11:46:40 +00:00
|
|
|
},
|
|
|
|
|
2014-07-22 21:27:37 +00:00
|
|
|
Inode => {
|
2014-11-26 07:36:09 +00:00
|
|
|
Purple.paint(self.stat.unstable.inode.to_string().as_slice()).to_string()
|
2014-07-22 21:27:37 +00:00
|
|
|
},
|
2014-11-25 20:50:23 +00:00
|
|
|
|
2014-06-22 07:09:16 +00:00
|
|
|
Blocks => {
|
2014-12-02 14:20:28 +00:00
|
|
|
if self.stat.kind == io::FileType::RegularFile || self.stat.kind == io::FileType::Symlink {
|
2014-11-26 07:36:09 +00:00
|
|
|
Cyan.paint(self.stat.unstable.blocks.to_string().as_slice()).to_string()
|
2014-06-22 07:09:16 +00:00
|
|
|
}
|
|
|
|
else {
|
2014-11-26 07:36:09 +00:00
|
|
|
GREY.paint("-").to_string()
|
2014-06-22 07:09:16 +00:00
|
|
|
}
|
|
|
|
},
|
2014-05-26 19:24:51 +00:00
|
|
|
|
|
|
|
// Display the ID if the user/group doesn't exist, which
|
|
|
|
// usually means it was deleted but its files weren't.
|
2014-06-26 22:26:27 +00:00
|
|
|
User => {
|
2014-12-12 11:17:55 +00:00
|
|
|
let uid = self.stat.unstable.uid as i32;
|
|
|
|
|
|
|
|
let user_name = match users_cache.get_user_by_uid(uid) {
|
|
|
|
Some(user) => user.name,
|
|
|
|
None => uid.to_string(),
|
|
|
|
};
|
|
|
|
|
|
|
|
let style = if users_cache.get_current_uid() == uid { Yellow.bold() } else { Plain };
|
2014-11-26 07:36:09 +00:00
|
|
|
style.paint(user_name.as_slice()).to_string()
|
2014-06-26 22:26:27 +00:00
|
|
|
},
|
2014-11-25 20:50:23 +00:00
|
|
|
|
2014-06-26 22:26:27 +00:00
|
|
|
Group => {
|
2014-06-27 18:51:04 +00:00
|
|
|
let gid = self.stat.unstable.gid as u32;
|
2014-12-12 11:17:55 +00:00
|
|
|
let mut style = Plain;
|
|
|
|
|
|
|
|
let group_name = match users_cache.get_group_by_gid(gid) {
|
|
|
|
Some(group) => {
|
|
|
|
let current_uid = users_cache.get_current_uid();
|
|
|
|
if let Some(current_user) = users_cache.get_user_by_uid(current_uid) {
|
|
|
|
if current_user.primary_group == group.gid || group.members.contains(¤t_user.name) {
|
|
|
|
style = Yellow.bold();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
group.name
|
|
|
|
},
|
|
|
|
None => gid.to_string(),
|
|
|
|
};
|
|
|
|
|
2014-11-26 07:36:09 +00:00
|
|
|
style.paint(group_name.as_slice()).to_string()
|
2014-05-27 18:05:15 +00:00
|
|
|
},
|
2014-05-04 20:33:14 +00:00
|
|
|
}
|
|
|
|
}
|
2014-06-21 18:39:27 +00:00
|
|
|
|
2014-07-22 19:47:30 +00:00
|
|
|
pub fn file_name(&self) -> String {
|
2014-06-29 20:02:14 +00:00
|
|
|
let name = self.name.as_slice();
|
|
|
|
let displayed_name = self.file_colour().paint(name);
|
2014-12-02 14:20:28 +00:00
|
|
|
if self.stat.kind == io::FileType::Symlink {
|
2014-11-24 17:03:36 +00:00
|
|
|
match fs::readlink(&self.path) {
|
2014-06-21 18:39:27 +00:00
|
|
|
Ok(path) => {
|
2014-12-12 11:17:55 +00:00
|
|
|
let target_path = match self.dir {
|
|
|
|
Some(dir) => dir.path.join(path),
|
|
|
|
None => path,
|
|
|
|
};
|
2014-12-12 14:06:48 +00:00
|
|
|
format!("{} {}", displayed_name, self.target_file_name_and_arrow(&target_path))
|
2014-06-21 18:39:27 +00:00
|
|
|
}
|
2014-11-26 07:36:09 +00:00
|
|
|
Err(_) => displayed_name.to_string(),
|
2014-06-21 09:11:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
2014-11-26 07:36:09 +00:00
|
|
|
displayed_name.to_string()
|
2014-06-21 09:11:50 +00:00
|
|
|
}
|
|
|
|
}
|
2014-06-21 18:39:27 +00:00
|
|
|
|
2014-07-22 14:41:20 +00:00
|
|
|
pub fn file_name_width(&self) -> uint {
|
|
|
|
self.name.as_slice().width(false)
|
|
|
|
}
|
|
|
|
|
2014-12-12 14:06:48 +00:00
|
|
|
fn target_file_name_and_arrow(&self, target_path: &Path) -> String {
|
2014-06-29 20:02:14 +00:00
|
|
|
let v = target_path.filename().unwrap();
|
2014-12-12 14:06:48 +00:00
|
|
|
let filename = String::from_utf8_lossy(v);
|
2014-11-25 20:50:23 +00:00
|
|
|
|
|
|
|
// Use stat instead of lstat - we *want* to follow links.
|
2014-12-12 14:06:48 +00:00
|
|
|
let link_target = fs::stat(target_path).map(|stat| File {
|
2014-11-24 17:03:36 +00:00
|
|
|
path: target_path.clone(),
|
2014-06-21 18:39:27 +00:00
|
|
|
dir: self.dir,
|
|
|
|
stat: stat,
|
2014-12-12 14:06:48 +00:00
|
|
|
name: filename.to_string(),
|
2014-06-29 20:02:14 +00:00
|
|
|
ext: File::ext(filename.clone()),
|
2014-06-21 18:39:27 +00:00
|
|
|
});
|
|
|
|
|
2014-07-22 21:27:37 +00:00
|
|
|
// Statting a path usually fails because the file at the
|
|
|
|
// other end doesn't exist. Show this by highlighting the
|
|
|
|
// target file in red instead of displaying an error, because
|
|
|
|
// the error would be shown out of context (before the
|
|
|
|
// results, not right by the file) and it's almost always for
|
2014-06-21 18:39:27 +00:00
|
|
|
// that reason anyway.
|
|
|
|
|
|
|
|
match link_target {
|
2014-11-24 01:48:51 +00:00
|
|
|
Ok(file) => format!("{} {}{}{}", GREY.paint("=>"), Cyan.paint(target_path.dirname_str().unwrap()), Cyan.paint("/"), file.file_colour().paint(filename.as_slice())),
|
|
|
|
Err(_) => format!("{} {}", Red.paint("=>"), Red.underline().paint(filename.as_slice())),
|
2014-06-21 18:39:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-12-18 07:00:31 +00:00
|
|
|
fn file_size(&self, size_format: SizeFormat) -> 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.
|
2014-12-02 14:20:28 +00:00
|
|
|
if self.stat.kind == io::FileType::Directory {
|
2014-11-26 07:36:09 +00:00
|
|
|
GREY.paint("-").to_string()
|
2014-11-25 20:50:23 +00:00
|
|
|
}
|
|
|
|
else {
|
2014-12-18 07:00:31 +00:00
|
|
|
let result = match size_format {
|
|
|
|
SizeFormat::DecimalBytes => decimal_prefix(self.stat.size as f64),
|
|
|
|
SizeFormat::BinaryBytes => binary_prefix(self.stat.size as f64),
|
|
|
|
};
|
|
|
|
|
|
|
|
match result {
|
|
|
|
Standalone(bytes) => Green.bold().paint(bytes.to_string().as_slice()).to_string(),
|
|
|
|
Prefixed(prefix, n) => {
|
|
|
|
let number = if n < 10f64 { format!("{:.1}", n) } else { format!("{:.0}", n) };
|
|
|
|
format!("{}{}", Green.bold().paint(number.as_slice()), Green.paint(prefix.symbol()))
|
|
|
|
}
|
|
|
|
}
|
2014-05-25 14:52:36 +00:00
|
|
|
}
|
2014-05-04 20:33:14 +00:00
|
|
|
}
|
|
|
|
|
2014-11-26 07:36:09 +00:00
|
|
|
fn type_char(&self) -> ANSIString {
|
2014-05-04 20:33:14 +00:00
|
|
|
return match self.stat.kind {
|
2014-12-02 14:20:28 +00:00
|
|
|
io::FileType::RegularFile => Plain.paint("."),
|
|
|
|
io::FileType::Directory => Blue.paint("d"),
|
|
|
|
io::FileType::NamedPipe => Yellow.paint("|"),
|
|
|
|
io::FileType::BlockSpecial => Purple.paint("s"),
|
|
|
|
io::FileType::Symlink => Cyan.paint("l"),
|
|
|
|
io::FileType::Unknown => Plain.paint("?"),
|
2014-05-04 20:33:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-07-06 17:00:27 +00:00
|
|
|
pub fn file_colour(&self) -> Style {
|
2014-06-17 21:17:22 +00:00
|
|
|
self.get_type().style()
|
2014-05-04 20:33:14 +00:00
|
|
|
}
|
2014-11-25 20:50:23 +00:00
|
|
|
|
2014-07-22 21:27:37 +00:00
|
|
|
fn has_multiple_links(&self) -> bool {
|
2014-12-02 14:20:28 +00:00
|
|
|
self.stat.kind == io::FileType::RegularFile && self.stat.unstable.nlink > 1
|
2014-07-22 21:27:37 +00:00
|
|
|
}
|
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.
|
2014-11-23 21:29:11 +00:00
|
|
|
File::permission_bit(bits, io::USER_READ, "r", Yellow.bold()),
|
|
|
|
File::permission_bit(bits, io::USER_WRITE, "w", Red.bold()),
|
|
|
|
File::permission_bit(bits, io::USER_EXECUTE, "x", Green.bold().underline()),
|
|
|
|
File::permission_bit(bits, io::GROUP_READ, "r", Yellow.normal()),
|
|
|
|
File::permission_bit(bits, io::GROUP_WRITE, "w", Red.normal()),
|
|
|
|
File::permission_bit(bits, io::GROUP_EXECUTE, "x", Green.normal()),
|
|
|
|
File::permission_bit(bits, io::OTHER_READ, "r", Yellow.normal()),
|
|
|
|
File::permission_bit(bits, io::OTHER_WRITE, "w", Red.normal()),
|
|
|
|
File::permission_bit(bits, io::OTHER_EXECUTE, "x", Green.normal()),
|
2014-05-04 20:33:14 +00:00
|
|
|
);
|
|
|
|
}
|
2014-05-26 10:50:46 +00:00
|
|
|
|
2014-11-26 07:36:09 +00:00
|
|
|
fn permission_bit(bits: io::FilePermission, bit: io::FilePermission, character: &'static str, style: Style) -> ANSIString {
|
2014-05-26 10:50:46 +00:00
|
|
|
if bits.contains(bit) {
|
2014-12-12 14:17:57 +00:00
|
|
|
style.paint(character)
|
2014-11-25 20:50:23 +00:00
|
|
|
}
|
|
|
|
else {
|
2014-12-12 14:17:57 +00:00
|
|
|
GREY.paint("-")
|
2014-05-26 10:50:46 +00:00
|
|
|
}
|
|
|
|
}
|
2014-05-04 20:33:14 +00:00
|
|
|
}
|