feat: Add highlighting of mounted directories (Linux only)

Closes #167
This commit is contained in:
Steven Davies 2023-09-05 13:26:45 +01:00
parent 51cdb54722
commit 3436171e9f
20 changed files with 198 additions and 12 deletions

43
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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 files number of hard links
- **-i**, **--inode**: list each files 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

View file

@ -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"

View file

@ -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'
}

View file

@ -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.

View file

@ -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 dont need to re-write an existing `LS_COLORS` variable with proprietary extensions.

View file

@ -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 Rusts `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 files 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 its 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) => {

View file

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

6
src/fs/mounts.rs Normal file
View file

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

View file

@ -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;

View file

@ -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,

View file

@ -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)

View file

@ -82,7 +82,8 @@ impl Mode {
// user about flags that wont 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)?,
})
}
}

View file

@ -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();

View file

@ -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;
}

View file

@ -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()
}
}

View file

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

View file

@ -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()) ]);

View file

@ -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,
}