file.rs 9.6 KB
Newer Older
B
Ben S 已提交
1
use colours::{Plain, Style, Black, Red, Green, Yellow, Blue, Purple, Cyan, Fixed};
B
Ben S 已提交
2
use std::io::{fs, IoResult};
B
Ben S 已提交
3
use std::io;
B
Ben S 已提交
4
use std::str::from_utf8_lossy;
B
Ben S 已提交
5

B
Ben S 已提交
6
use column::{Column, Permissions, FileName, FileSize, User, Group, HardLinks, Inode, Blocks};
B
Ben S 已提交
7
use format::{format_metric_bytes, format_IEC_bytes};
B
Ben S 已提交
8
use unix::Unix;
B
Ben S 已提交
9
use sort::SortPart;
B
Ben S 已提交
10
use dir::Dir;
B
Ben S 已提交
11
use filetype::HasType;
B
Ben S 已提交
12

B
Ben S 已提交
13 14 15 16 17 18 19
// 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.

B
Ben S 已提交
20
pub struct File<'a> {
B
Ben S 已提交
21
    pub name:  String,
B
Ben S 已提交
22
    pub dir:   &'a Dir<'a>,
B
Ben S 已提交
23
    pub ext:   Option<String>,
B
Ben S 已提交
24 25
    pub path:  &'a Path,
    pub stat:  io::FileStat,
B
Ben S 已提交
26
    pub parts: Vec<SortPart>,
B
Ben S 已提交
27 28 29
}

impl<'a> File<'a> {
B
Ben S 已提交
30
    pub fn from_path(path: &'a Path, parent: &'a Dir) -> IoResult<File<'a>> {
B
Ben S 已提交
31
        let v = path.filename().unwrap();  // fails if / or . or ..
B
Ben S 已提交
32
        let filename = from_utf8_lossy(v).to_string();
B
Ben S 已提交
33
        
B
Ben S 已提交
34 35 36
        // 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.
B
Ben S 已提交
37

B
Ben S 已提交
38
        fs::lstat(path).map(|stat| File {
B
Ben S 已提交
39
            path:  path,
B
Ben S 已提交
40
            dir:   parent,
B
Ben S 已提交
41
            stat:  stat,
B
Ben S 已提交
42 43 44
            name:  filename.clone(),
            ext:   File::ext(filename.clone()),
            parts: SortPart::split_into_parts(filename.clone()),
B
Ben S 已提交
45
        })
B
Ben S 已提交
46 47
    }

B
Ben S 已提交
48
    fn ext(name: String) -> Option<String> {
B
Ben S 已提交
49 50 51 52
        // 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"\.([^.]+)$");
B
Ben S 已提交
53
        re.captures(name.as_slice()).map(|caps| caps.at(1).to_string())
B
Ben S 已提交
54 55
    }

B
Ben S 已提交
56
    pub fn is_dotfile(&self) -> bool {
B
Ben S 已提交
57
        self.name.as_slice().starts_with(".")
B
Ben S 已提交
58 59
    }

60
    pub fn is_tmpfile(&self) -> bool {
B
Ben S 已提交
61 62
        let name = self.name.as_slice();
        name.ends_with("~") || (name.starts_with("#") && name.ends_with("#"))
B
Ben S 已提交
63
    }
B
Ben S 已提交
64

B
Ben S 已提交
65 66 67 68 69
    // 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.
B
Ben S 已提交
70

71
    pub fn get_source_files(&self) -> Vec<Path> {
B
Ben S 已提交
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
        let ext = self.ext.clone().unwrap();
        match ext.as_slice() {
            "class" => vec![self.path.with_extension("java")],  // Java
            "elc" => vec![self.path.with_extension("el")],  // Emacs Lisp
            "hi" => vec![self.path.with_extension("hs")],  // Haskell
            "o" => vec![self.path.with_extension("c"), self.path.with_extension("cpp")],  // C, C++
            "pyc" => vec![self.path.with_extension("py")],  // Python
            "js" => vec![self.path.with_extension("coffee"), self.path.with_extension("ts")],  // CoffeeScript, TypeScript
            "css" => vec![self.path.with_extension("sass"), self.path.with_extension("less")],  // SASS, Less

            "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")],  // list of figures
            "log" => vec![self.path.with_extension("tex")],  // TeX log file
            "lot" => vec![self.path.with_extension("tex")],  // list of tables
            "toc" => vec![self.path.with_extension("tex")],  // table of contents
B
Ben S 已提交
89

B
Ben S 已提交
90 91 92
            _ => vec![],
        }
    }
B
Ben S 已提交
93

B
Ben S 已提交
94
    pub fn display(&self, column: &Column, unix: &mut Unix) -> String {
B
Ben S 已提交
95
        match *column {
B
Ben S 已提交
96
            Permissions => self.permissions_string(),
B
Ben S 已提交
97
            FileName => self.file_name(),
B
Ben S 已提交
98
            FileSize(use_iec) => self.file_size(use_iec),
99 100 101 102 103 104 105 106 107

            // A file with multiple links is interesting, but
            // directories and suchlike can have multiple links all
            // the time.
            HardLinks => {
                let style = if self.stat.kind == io::TypeFile && self.stat.unstable.nlink > 1 { Red.on(Yellow) } else { Red.normal() };
                style.paint(self.stat.unstable.nlink.to_str().as_slice())
            },

B
Ben S 已提交
108
            Inode => Purple.paint(self.stat.unstable.inode.to_str().as_slice()),
B
Ben S 已提交
109 110 111 112 113 114 115 116
            Blocks => {
                if self.stat.kind == io::TypeFile || self.stat.kind == io::TypeSymlink {
                    Cyan.paint(self.stat.unstable.blocks.to_str().as_slice())
                }
                else {
                    Fixed(244).paint("-")
                }
            },
B
Ben S 已提交
117 118 119

            // Display the ID if the user/group doesn't exist, which
            // usually means it was deleted but its files weren't.
B
Ben S 已提交
120
            User => {
121 122 123 124
                let uid = self.stat.unstable.uid as u32;
                unix.load_user(uid);
                let style = if unix.uid == uid { Yellow.bold() } else { Plain };
                let string = unix.get_user_name(uid).unwrap_or(uid.to_str());
B
Ben S 已提交
125 126 127
                style.paint(string.as_slice())
            },
            Group => {
128 129 130 131
                let gid = self.stat.unstable.gid as u32;
                unix.load_group(gid);
                let name = unix.get_group_name(gid).unwrap_or(gid.to_str());
                let style = if unix.is_group_member(gid) { Yellow.normal() } else { Plain };
B
Ben S 已提交
132
                style.paint(name.as_slice())
B
Ben S 已提交
133
            },
B
Ben S 已提交
134 135
        }
    }
B
Ben S 已提交
136

B
Ben S 已提交
137
    fn file_name(&self) -> String {
B
Ben S 已提交
138 139
        let name = self.name.as_slice();
        let displayed_name = self.file_colour().paint(name);
B
Ben S 已提交
140 141
        if self.stat.kind == io::TypeSymlink {
            match fs::readlink(self.path) {
B
Ben S 已提交
142 143 144 145 146
                Ok(path) => {
                    let target_path = if path.is_absolute() { path } else { self.dir.path.join(path) };
                    format!("{} {}", displayed_name, self.target_file_name_and_arrow(target_path))
                }
                Err(_) => displayed_name,
B
Ben S 已提交
147 148 149 150 151 152
            }
        }
        else {
            displayed_name
        }
    }
B
Ben S 已提交
153 154

    fn target_file_name_and_arrow(&self, target_path: Path) -> String {
B
Ben S 已提交
155
        let v = target_path.filename().unwrap();
B
Ben S 已提交
156
        let filename = from_utf8_lossy(v).to_string();
B
Ben S 已提交
157
        
B
Ben S 已提交
158 159 160 161
        let link_target = fs::stat(&target_path).map(|stat| File {
            path:  &target_path,
            dir:   self.dir,
            stat:  stat,
B
Ben S 已提交
162 163
            name:  filename.clone(),
            ext:   File::ext(filename.clone()),
B
Ben S 已提交
164 165 166 167 168 169 170 171 172 173
            parts: vec![],  // not needed
        });

        // 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 and it's almost always
        // that reason anyway.

        match link_target {
B
Ben S 已提交
174 175
            Ok(file) => format!("{} {}", Fixed(244).paint("=>"), file.file_colour().paint(filename.as_slice())),
            Err(_)   => format!("{} {}", Red.paint("=>"), Red.underline().paint(filename.as_slice())),
B
Ben S 已提交
176 177 178
        }
    }

B
Ben S 已提交
179
    fn file_size(&self, use_iec_prefixes: bool) -> String {
180 181 182
        // 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 {
B
Ben S 已提交
183
            Fixed(244).paint("-")
B
Ben S 已提交
184
        } else {
B
Ben S 已提交
185
            let (size, suffix) = if use_iec_prefixes {
B
Ben S 已提交
186
                format_IEC_bytes(self.stat.size)
187
            } else {
B
Ben S 已提交
188
                format_metric_bytes(self.stat.size)
189
            };
B
Ben S 已提交
190

B
Ben S 已提交
191
            return format!("{}{}", Green.bold().paint(size.as_slice()), Green.paint(suffix.as_slice()));
192
        }
B
Ben S 已提交
193 194
    }

B
Ben S 已提交
195
    fn type_char(&self) -> String {
B
Ben S 已提交
196
        return match self.stat.kind {
B
Ben S 已提交
197
            io::TypeFile         => ".".to_string(),
B
Ben S 已提交
198 199
            io::TypeDirectory    => Blue.paint("d"),
            io::TypeNamedPipe    => Yellow.paint("|"),
B
Ben S 已提交
200
            io::TypeBlockSpecial => Purple.paint("s"),
B
Ben S 已提交
201
            io::TypeSymlink      => Cyan.paint("l"),
202
            io::TypeUnknown      => "?".to_string(),
B
Ben S 已提交
203 204 205 206
        }
    }

    fn file_colour(&self) -> Style {
B
Ben S 已提交
207
        self.get_type().style()
B
Ben S 已提交
208 209
    }

B
Ben S 已提交
210
    fn permissions_string(&self) -> String {
B
Ben S 已提交
211 212 213
        let bits = self.stat.perm;
        return format!("{}{}{}{}{}{}{}{}{}{}",
            self.type_char(),
B
Ben S 已提交
214 215 216 217 218 219 220 221 222 223 224 225

            // 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()),
B
Ben S 已提交
226 227
       );
    }
228

B
Ben S 已提交
229
    fn permission_bit(bits: io::FilePermission, bit: io::FilePermission, character: &'static str, style: Style) -> String {
230
        if bits.contains(bit) {
B
Ben S 已提交
231
            style.paint(character.as_slice())
232 233 234 235
        } else {
            Black.bold().paint("-".as_slice())
        }
    }
B
Ben S 已提交
236
}