瀏覽代碼

feat: Add highlighting of mounted directories (Linux only)

Closes #167
Steven Davies 2 年之前
父節點
當前提交
3436171e9f

+ 41 - 2
Cargo.lock

@@ -94,6 +94,7 @@ dependencies = [
  "num_cpus",
  "number_prefix",
  "phf",
+ "proc-mounts",
  "scoped_threadpool",
  "term_grid",
  "terminal_size",
@@ -288,6 +289,15 @@ dependencies = [
  "unicode-width",
 ]
 
+[[package]]
+name = "partition-identity"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa925f9becb532d758b0014b472c576869910929cf4c3f8054b386f19ab9e21"
+dependencies = [
+ "thiserror",
+]
+
 [[package]]
 name = "percent-encoding"
 version = "2.1.0"
@@ -351,6 +361,15 @@ dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "proc-mounts"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d652f8435d0ab70bf4f3590a6a851d59604831a458086541b95238cc51ffcf2"
+dependencies = [
+ "partition-identity",
+]
+
 [[package]]
 name = "quote"
 version = "1.0.33"
@@ -409,9 +428,9 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
 
 [[package]]
 name = "syn"
-version = "2.0.29"
+version = "2.0.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a"
+checksum = "718fa2415bcb8d8bd775917a1bf12a7931b6dfa890753378538118181e0cb398"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -437,6 +456,26 @@ dependencies = [
  "windows-sys",
 ]
 
+[[package]]
+name = "thiserror"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "timeago"
 version = "0.4.1"

+ 3 - 0
Cargo.toml

@@ -67,6 +67,9 @@ default-features = false
 # See: https://github.com/eza-community/eza/pull/192
 features = ["vendored-libgit2"]
 
+[target.'cfg(target_os = "linux")'.dependencies]
+proc-mounts = "0.3"
+
 [target.'cfg(unix)'.dependencies]
 uzers = "0.11.2"
 

+ 2 - 0
README.md

@@ -34,6 +34,7 @@ By deliberately making some decisions differently, eza attempts to be a more fea
 
  -   Fixes [“The Grid Bug”](https://github.com/eza-community/eza/issues/66#issuecomment-1656758327) introduced in exa 2021.
  -   Hyperlink support.
+ -   Mount point details.
  -   Selinux context output.
  -   Git repo status output.
  -   Human readable relative dates.
@@ -170,6 +171,7 @@ These options are available when running with `--long` (`-l`):
 - **-H**, **--links**: list each file’s number of hard links
 - **-i**, **--inode**: list each file’s inode number
 - **-m**, **--modified**: use the modified timestamp field
+- **-M**, **--mounts**: Show mount details (Linux only).
 - **-S**, **--blocksize**: show size of allocated file system blocks
 - **-t**, **--time=(field)**: which timestamp field to use
 - **-u**, **--accessed**: use the accessed timestamp field

+ 1 - 0
completions/fish/eza.fish

@@ -89,6 +89,7 @@ complete -c eza -s o -l octal-permissions -d "List each file's permission in oct
 complete -c eza -l no-filesize -d "Suppress the filesize field"
 complete -c eza -l no-user -d "Suppress the user field"
 complete -c eza -l no-time -d "Suppress the time field"
+complete -c eza -s M -l mounts -d "Show mount details"
 
 # Optional extras
 complete -c eza -l git -d "List each file's Git status, if tracked"

+ 1 - 0
completions/zsh/_eza

@@ -60,6 +60,7 @@ __eza() {
         --git-repos-no-status"[List each git-repos branch name (much faster)]" \
         {-@,--extended}"[List each file's extended attributes and sizes]" \
         {-Z,--context}"[List each file's security context]" \
+        {-M,--mounts}"[Show mount details (long mode only)]" \
         '*:filename:_files'
 }
 

+ 3 - 0
man/eza.1.md

@@ -149,6 +149,9 @@ These options are available when running with `--long` (`-l`):
 `-m`, `--modified`
 : Use the modified timestamp field.
 
+`-M`, `--mounts`
+: Show mount details (Linux only)
+
 `-n`, `--numeric`
 : List numeric user and group IDs.
 

+ 3 - 0
man/eza_colors.5.md

@@ -220,6 +220,9 @@ LIST OF CODES
 `bO`
 : the overlay style for broken symlink paths
 
+`mp`
+: a mount point
+
 Values in `EXA_COLORS` override those given in `LS_COLORS`, so you don’t need to re-write an existing `LS_COLORS` variable with proprietary extensions.
 
 

+ 38 - 5
src/fs/file.rs

@@ -12,11 +12,14 @@ use std::time::{Duration, UNIX_EPOCH};
 
 use log::*;
 
+use crate::ALL_MOUNTS;
 use crate::fs::dir::Dir;
 use crate::fs::feature::xattr;
 use crate::fs::feature::xattr::{FileAttributes, Attribute};
 use crate::fs::fields as f;
 
+use super::mounts::MountedFs;
+
 
 /// A **File** is a wrapper around one of Rust’s `PathBuf` values, along with
 /// associated data about the file.
@@ -79,7 +82,9 @@ pub struct File<'dir> {
     pub deref_links: bool,
     /// The extended attributes of this file.
     pub extended_attributes: Vec<Attribute>,
-}
+
+    /// The absolute value of this path, used to look up mount points.
+    pub absolute_path: PathBuf,}
 
 impl<'dir> File<'dir> {
     pub fn from_args<PD, FN>(path: PathBuf, parent_dir: PD, filename: FN, deref_links: bool) -> io::Result<File<'dir>>
@@ -94,8 +99,9 @@ impl<'dir> File<'dir> {
         let metadata   = std::fs::symlink_metadata(&path)?;
         let is_all_all = false;
         let extended_attributes = File::gather_extended_attributes(&path);
+        let absolute_path = std::fs::canonicalize(&path)?;
 
-        Ok(File { name, ext, path, metadata, parent_dir, is_all_all, deref_links, extended_attributes })
+        Ok(File { name, ext, path, metadata, parent_dir, is_all_all, deref_links, extended_attributes, absolute_path })
     }
 
     pub fn new_aa_current(parent_dir: &'dir Dir) -> io::Result<File<'dir>> {
@@ -107,8 +113,9 @@ impl<'dir> File<'dir> {
         let is_all_all = true;
         let parent_dir = Some(parent_dir);
         let extended_attributes = File::gather_extended_attributes(&path);
+        let absolute_path = std::fs::canonicalize(&path)?;
 
-        Ok(File { path, parent_dir, metadata, ext, name: ".".into(), is_all_all, deref_links: false, extended_attributes })
+        Ok(File { path, parent_dir, metadata, ext, name: ".".into(), is_all_all, deref_links: false, extended_attributes, absolute_path })
     }
 
     pub fn new_aa_parent(path: PathBuf, parent_dir: &'dir Dir) -> io::Result<File<'dir>> {
@@ -119,8 +126,9 @@ impl<'dir> File<'dir> {
         let is_all_all = true;
         let parent_dir = Some(parent_dir);
         let extended_attributes = File::gather_extended_attributes(&path);
+        let absolute_path = std::fs::canonicalize(&path)?;
 
-        Ok(File { path, parent_dir, metadata, ext, name: "..".into(), is_all_all, deref_links: false, extended_attributes })
+        Ok(File { path, parent_dir, metadata, ext, name: "..".into(), is_all_all, deref_links: false, extended_attributes, absolute_path })
     }
 
     /// A file’s name is derived from its string. This needs to handle directories
@@ -243,6 +251,21 @@ impl<'dir> File<'dir> {
         self.metadata.file_type().is_socket()
     }
 
+    /// Whether this file is a mount point
+    pub fn is_mount_point(&self) -> bool {
+        if cfg!(target_os = "linux") && self.is_directory() {
+            return ALL_MOUNTS.contains_key(&self.absolute_path);
+        }
+        false
+    }
+
+    /// The filesystem device and type for a mount point
+    pub fn mount_point_info(&self) -> Option<&MountedFs> {
+        if cfg!(target_os = "linux") {
+            return ALL_MOUNTS.get(&self.absolute_path);
+        }
+        None
+    }
 
     /// Re-prefixes the path pointed to by this file, if it’s a symlink, to
     /// make it an absolute path that can be accessed from whichever
@@ -293,7 +316,17 @@ impl<'dir> File<'dir> {
                 let ext  = File::ext(&path);
                 let name = File::filename(&path);
                 let extended_attributes = File::gather_extended_attributes(&absolute_path);
-                let file = File { parent_dir: None, path, ext, metadata, name, is_all_all: false, deref_links: self.deref_links, extended_attributes };
+                let file = File {
+                    parent_dir: None,
+                    path,
+                    ext,
+                    metadata,
+                    name,
+                    is_all_all: false,
+                    deref_links: self.deref_links,
+                    extended_attributes,
+                    absolute_path
+                };
                 FileTarget::Ok(Box::new(file))
             }
             Err(e) => {

+ 1 - 0
src/fs/mod.rs

@@ -8,3 +8,4 @@ pub mod dir_action;
 pub mod feature;
 pub mod fields;
 pub mod filter;
+pub mod mounts;

+ 6 - 0
src/fs/mounts.rs

@@ -0,0 +1,6 @@
+/// Details of a mounted filesystem.
+pub struct MountedFs {
+    pub dest: String,
+    pub fstype: String,
+    pub source: String,
+}

+ 29 - 0
src/main.rs

@@ -22,6 +22,7 @@
 #![allow(clippy::upper_case_acronyms)]
 #![allow(clippy::wildcard_imports)]
 
+use std::collections::HashMap;
 use std::env;
 use std::ffi::{OsStr, OsString};
 use std::io::{self, Write, ErrorKind};
@@ -31,6 +32,13 @@ use ansi_term::{ANSIStrings, Style};
 
 use log::*;
 
+#[cfg(target_os = "linux")]
+use proc_mounts::MountList;
+
+#[macro_use]
+extern crate lazy_static;
+
+use crate::fs::mounts::MountedFs;
 use crate::fs::{Dir, File};
 use crate::fs::feature::git::GitCache;
 use crate::fs::filter::GitIgnore;
@@ -45,6 +53,27 @@ mod options;
 mod output;
 mod theme;
 
+lazy_static! {
+    static ref ALL_MOUNTS: HashMap<PathBuf, MountedFs> = {
+        #[cfg(target_os = "linux")]
+        match MountList::new() {
+            Ok(mount_list) => {
+                let mut m = HashMap::new();
+                mount_list.0.iter().for_each(|mount| {
+                    m.insert(mount.dest.clone(), MountedFs {
+                        dest: mount.dest.to_string_lossy().into_owned(),
+                        fstype: mount.fstype.clone(),
+                        source: mount.source.to_string_lossy().into(),
+                    });
+                });
+                m
+            }
+            Err(_) => HashMap::new()
+        }
+        #[cfg(not(target_os = "linux"))]
+        HashMap::new()
+    };
+}
 
 fn main() {
     use std::process::exit;

+ 3 - 2
src/options/flags.rs

@@ -54,7 +54,8 @@ pub static TIME:       Arg = Arg { short: Some(b't'), long: "time",       takes_
 pub static ACCESSED:   Arg = Arg { short: Some(b'u'), long: "accessed",   takes_value: TakesValue::Forbidden };
 pub static CREATED:    Arg = Arg { short: Some(b'U'), long: "created",    takes_value: TakesValue::Forbidden };
 pub static TIME_STYLE: Arg = Arg { short: None,       long: "time-style", takes_value: TakesValue::Necessary(Some(TIME_STYLES)) };
-pub static HYPERLINK:  Arg = Arg { short: None,       long: "hyperlink",  takes_value: TakesValue::Forbidden};
+pub static HYPERLINK:  Arg = Arg { short: None,       long: "hyperlink",  takes_value: TakesValue::Forbidden };
+pub static MOUNTS:     Arg = Arg { short: Some(b'M'), long: "mounts",     takes_value: TakesValue::Forbidden };
 const TIMES: Values = &["modified", "changed", "accessed", "created"];
 const TIME_STYLES: Values = &["default", "long-iso", "full-iso", "iso", "relative"];
 
@@ -85,7 +86,7 @@ pub static ALL_ARGS: Args = Args(&[
     &IGNORE_GLOB, &GIT_IGNORE, &ONLY_DIRS,
 
     &BINARY, &BYTES, &GROUP, &NUMERIC, &HEADER, &ICONS, &INODE, &LINKS, &MODIFIED, &CHANGED,
-    &BLOCKSIZE, &TIME, &ACCESSED, &CREATED, &TIME_STYLE, &HYPERLINK,
+    &BLOCKSIZE, &TIME, &ACCESSED, &CREATED, &TIME_STYLE, &HYPERLINK, &MOUNTS,
     &NO_PERMISSIONS, &NO_FILESIZE, &NO_USER, &NO_TIME, &NO_ICONS,
 
     &GIT, &NO_GIT, &GIT_REPOS, &GIT_REPOS_NO_STAT,

+ 1 - 0
src/options/help.rs

@@ -53,6 +53,7 @@ LONG VIEW OPTIONS
   -H, --links              list each file's number of hard links
   -i, --inode              list each file's inode number
   -m, --modified           use the modified timestamp field
+  -M, --mounts             show mount details (Linux only)
   -n, --numeric            list numeric user and group IDs
   -S, --blocksize          show size of allocated file system blocks
   -t, --time FIELD         which timestamp field to list (modified, accessed, created)

+ 4 - 1
src/options/view.rs

@@ -82,7 +82,8 @@ impl Mode {
         // user about flags that won’t have any effect.
         if matches.is_strict() {
             for option in &[ &flags::BINARY, &flags::BYTES, &flags::INODE, &flags::LINKS,
-                             &flags::HEADER, &flags::BLOCKSIZE, &flags::TIME, &flags::GROUP, &flags::NUMERIC ] {
+                             &flags::HEADER, &flags::BLOCKSIZE, &flags::TIME, &flags::GROUP, &flags::NUMERIC,
+                             &flags::MOUNTS ] {
                 if matches.has(option)? {
                     return Err(OptionsError::Useless(option, false, &flags::LONG));
                 }
@@ -119,6 +120,7 @@ impl details::Options {
             header: false,
             xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?,
             secattr: xattr::ENABLED && matches.has(&flags::SECURITY_CONTEXT)?,
+            mounts: matches.has(&flags::MOUNTS)?,
         };
 
         Ok(details)
@@ -139,6 +141,7 @@ impl details::Options {
             header: matches.has(&flags::HEADER)?,
             xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?,
             secattr: xattr::ENABLED && matches.has(&flags::SECURITY_CONTEXT)?,
+            mounts: matches.has(&flags::MOUNTS)?,
         })
     }
 }

+ 5 - 0
src/output/details.rs

@@ -92,6 +92,7 @@ use crate::theme::Theme;
 ///
 /// Almost all the heavy lifting is done in a Table object, which handles the
 /// columns for each row.
+#[allow(clippy::struct_excessive_bools)] /// This clearly isn't a state machine
 #[derive(PartialEq, Eq, Debug)]
 pub struct Options {
 
@@ -109,6 +110,9 @@ pub struct Options {
 
     /// Whether to show each file's security attribute.
     pub secattr: bool,
+
+    /// Whether to show a directory's mounted filesystem details
+    pub mounts: bool,
 }
 
 
@@ -288,6 +292,7 @@ impl<'a> Render<'a> {
 
             let file_name = self.file_style.for_file(egg.file, self.theme)
                                 .with_link_paths()
+                                .with_mount_details(self.opts.mounts)
                                 .paint()
                                 .promote();
 

+ 48 - 1
src/output/file_name.rs

@@ -3,6 +3,7 @@ use std::path::Path;
 
 use ansi_term::{ANSIString, Style};
 
+use crate::fs::mounts::MountedFs;
 use crate::fs::{File, FileTarget};
 use crate::output::cell::TextCellContents;
 use crate::output::escape;
@@ -35,7 +36,9 @@ impl Options {
             link_style: LinkStyle::JustFilenames,
             options:    self,
             target:     if file.is_link() { Some(file.link_target()) }
-                                     else { None }
+                                     else { None },
+            mount_style: MountStyle::JustDirectoryNames,
+            mounted_fs: file.mount_point_info(),
         }
     }
 }
@@ -74,6 +77,18 @@ impl Default for Classify {
     }
 }
 
+/// When displaying a directory name, there needs to be some way to handle
+/// mount details, depending on how long the resulting Cell can be.
+#[derive(PartialEq, Debug, Copy, Clone)]
+enum MountStyle {
+
+    /// Just display the directory names.
+    JustDirectoryNames,
+
+    /// Display mount points as directories and include information about
+    /// the filesystem that's mounted there.
+    MountInfo,
+}
 
 /// Whether and how to show icons.
 #[derive(PartialEq, Eq, Debug, Copy, Clone)]
@@ -112,6 +127,12 @@ pub struct FileName<'a, 'dir, C> {
     link_style: LinkStyle,
 
     pub options: Options,
+
+    /// The filesystem details for a mounted filesystem.
+    mounted_fs: Option<&'a MountedFs>,
+
+    /// How to handle displaying a mounted filesystem.
+    mount_style: MountStyle,
 }
 
 impl<'a, 'dir, C> FileName<'a, 'dir, C> {
@@ -122,6 +143,17 @@ impl<'a, 'dir, C> FileName<'a, 'dir, C> {
         self.link_style = LinkStyle::FullLinkPaths;
         self
     }
+
+    /// Sets the flag on this file name to display mounted filesystem
+    ///details.
+    pub fn with_mount_details(mut self, enable: bool) -> Self {
+        self.mount_style = if enable {
+            MountStyle::MountInfo
+        } else {
+            MountStyle::JustDirectoryNames
+        };
+        self
+    }
 }
 
 impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
@@ -190,6 +222,8 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
                             target: None,
                             link_style: LinkStyle::FullLinkPaths,
                             options: target_options,
+                            mounted_fs: None,
+                            mount_style: MountStyle::JustDirectoryNames,
                         };
 
                         for bit in target_name.escaped_file_name() {
@@ -228,6 +262,15 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
             }
         }
 
+        if let (MountStyle::MountInfo, Some(mount_details)) = (self.mount_style, self.mounted_fs.as_ref()) {
+            // This is a filesystem mounted on the directory, output its details
+            bits.push(Style::default().paint(" ["));
+            bits.push(Style::default().paint(mount_details.source.clone()));
+            bits.push(Style::default().paint(" ("));
+            bits.push(Style::default().paint(mount_details.fstype.clone()));
+            bits.push(Style::default().paint(")]"));
+        }
+
         bits.into()
     }
 
@@ -372,6 +415,7 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
         }
 
         match self.file {
+            f if f.is_mount_point()      => self.colours.mount_point(),
             f if f.is_directory()        => self.colours.directory(),
             #[cfg(unix)]
             f if f.is_executable_file()  => self.colours.executable_file(),
@@ -424,6 +468,9 @@ pub trait Colours: FiletypeColours {
     /// The style to paint a file that has its executable bit set.
     fn executable_file(&self) -> Style;
 
+    /// The style to paint a directory that has a filesystem mounted on it.
+    fn mount_point(&self) -> Style;
+
     fn colour_file(&self, file: &File<'_>) -> Style;
 }
 

+ 2 - 1
src/output/lines.rs

@@ -5,7 +5,7 @@ use ansi_term::ANSIStrings;
 use crate::fs::File;
 use crate::fs::filter::FileFilter;
 use crate::output::cell::TextCellContents;
-use crate::output::file_name::{Options as FileStyle};
+use crate::output::file_name::Options as FileStyle;
 use crate::theme::Theme;
 
 
@@ -32,6 +32,7 @@ impl<'a> Render<'a> {
         self.file_style
             .for_file(file, self.theme)
             .with_link_paths()
+            .with_mount_details(false)
             .paint()
     }
 }

+ 1 - 0
src/theme/default_theme.rs

@@ -20,6 +20,7 @@ impl UiStyles {
                 socket:       Red.bold(),
                 special:      Yellow.normal(),
                 executable:   Green.bold(),
+                mount_point:  Blue.bold().underline(),
             },
 
             perms: Permissions {

+ 3 - 0
src/theme/mod.rs

@@ -327,6 +327,7 @@ impl FileNameColours for Theme {
     fn control_char(&self)        -> Style { self.ui.control_char }
     fn symlink_path(&self)        -> Style { self.ui.symlink_path }
     fn executable_file(&self)     -> Style { self.ui.filekinds.executable }
+    fn mount_point(&self)         -> Style { self.ui.filekinds.mount_point }
 
     fn colour_file(&self, file: &File<'_>) -> Style {
         self.exts.colour_file(file).unwrap_or(self.ui.filekinds.normal)
@@ -534,6 +535,8 @@ mod customs_test {
     test!(exa_cc:  ls "", exa "cc=38;5;134"  =>  colours c -> { c.control_char              = Fixed(134).normal(); });
     test!(exa_bo:  ls "", exa "bO=4"         =>  colours c -> { c.broken_path_overlay       = Style::default().underline(); });
 
+    test!(exa_mp:  ls "", exa "mp=1;34;4"    =>  colours c -> { c.filekinds.mount_point     = Blue.bold().underline(); });
+
     // All the while, LS_COLORS treats them as filenames:
     test!(ls_uu:   ls "uu=38;5;117", exa ""  =>  exts [ ("uu", Fixed(117).normal()) ]);
     test!(ls_un:   ls "un=38;5;118", exa ""  =>  exts [ ("un", Fixed(118).normal()) ]);

+ 3 - 0
src/theme/ui_styles.rs

@@ -39,6 +39,7 @@ pub struct FileKinds {
     pub socket: Style,
     pub special: Style,
     pub executable: Style,
+    pub mount_point: Style,
 }
 
 #[derive(Clone, Copy, Debug, Default, PartialEq)]
@@ -210,6 +211,8 @@ impl UiStyles {
             "cc" => self.control_char             = pair.to_style(),
             "bO" => self.broken_path_overlay      = pair.to_style(),
 
+            "mp" => self.filekinds.mount_point    = pair.to_style(),
+
              _   => return false,
         }