diff --git a/Cargo.lock b/Cargo.lock index 87ff7af46..cf1b00095 100644 --- a/Cargo.lock +++ b/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" diff --git a/Cargo.toml b/Cargo.toml index 084604238..ae4f343c5 100644 --- a/Cargo.toml +++ b/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" diff --git a/README.md b/README.md index 765ff5524..c5cf4dbda 100644 --- a/README.md +++ b/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 diff --git a/completions/fish/eza.fish b/completions/fish/eza.fish index 7ae6880ca..0b79649e6 100755 --- a/completions/fish/eza.fish +++ b/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" diff --git a/completions/zsh/_eza b/completions/zsh/_eza index 3724dbfbc..f5c7d1abe 100644 --- a/completions/zsh/_eza +++ b/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' } diff --git a/man/eza.1.md b/man/eza.1.md index f67d3dd16..26db56c8b 100644 --- a/man/eza.1.md +++ b/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. diff --git a/src/fs/file.rs b/src/fs/file.rs index 025fa3604..0905d96cc 100644 --- a/src/fs/file.rs +++ b/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, -} + + /// The absolute value of this path, used to look up mount points. + pub absolute_path: PathBuf,} impl<'dir> File<'dir> { pub fn from_args(path: PathBuf, parent_dir: PD, filename: FN, deref_links: bool) -> io::Result> @@ -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> { @@ -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> { @@ -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) => { diff --git a/src/fs/mod.rs b/src/fs/mod.rs index 1188f615a..0cbb1b5dd 100644 --- a/src/fs/mod.rs +++ b/src/fs/mod.rs @@ -8,3 +8,4 @@ pub mod dir_action; pub mod feature; pub mod fields; pub mod filter; +pub mod mounts; \ No newline at end of file diff --git a/src/fs/mounts.rs b/src/fs/mounts.rs new file mode 100644 index 000000000..a700e3d5b --- /dev/null +++ b/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, +} diff --git a/src/main.rs b/src/main.rs index a33cf15fa..ca5ff6614 100644 --- a/src/main.rs +++ b/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 = { + #[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; diff --git a/src/options/flags.rs b/src/options/flags.rs index cbc672e2e..689d30212 100644 --- a/src/options/flags.rs +++ b/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, diff --git a/src/options/help.rs b/src/options/help.rs index 99a1deac1..9e47b55a1 100644 --- a/src/options/help.rs +++ b/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) diff --git a/src/options/view.rs b/src/options/view.rs index eba0fb7b5..f050101b1 100644 --- a/src/options/view.rs +++ b/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)?, }) } } diff --git a/src/output/details.rs b/src/output/details.rs index 8df63116e..814bccc26 100644 --- a/src/output/details.rs +++ b/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(); diff --git a/src/output/file_name.rs b/src/output/file_name.rs index 87c3b4d24..c8f4f5fe2 100644 --- a/src/output/file_name.rs +++ b/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; } diff --git a/src/output/lines.rs b/src/output/lines.rs index 2343ce506..f525915ea 100644 --- a/src/output/lines.rs +++ b/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() } } diff --git a/src/theme/default_theme.rs b/src/theme/default_theme.rs index b0649e3fa..8660ad046 100644 --- a/src/theme/default_theme.rs +++ b/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 { diff --git a/src/theme/mod.rs b/src/theme/mod.rs index ae8d62979..0af63b89f 100644 --- a/src/theme/mod.rs +++ b/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) diff --git a/src/theme/ui_styles.rs b/src/theme/ui_styles.rs index 43f5d91d3..670ebb863 100644 --- a/src/theme/ui_styles.rs +++ b/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)]