diff --git a/Cargo.lock b/Cargo.lock index 5ee181df..ed6a0253 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -57,6 +57,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "err-derive" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22deed3a8124cff5fa835713fa105621e43bbdc46690c3a6b68328a012d350d4" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "rustversion", + "syn", + "synstructure", +] + [[package]] name = "exa" version = "0.10.1" @@ -72,6 +86,7 @@ dependencies = [ "natord", "num_cpus", "number_prefix", + "proc-mounts", "scoped_threadpool", "term_grid", "terminal_size", @@ -253,6 +268,15 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "partition-identity" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec13ba9a0eec5c10a89f6ec1b6e9e2ef7d29b810d771355abbd1c43cae003ed6" +dependencies = [ + "err-derive", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -265,18 +289,99 @@ version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "proc-mounts" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ad7e9c8d1b8c20f16a84d61d7c4c0325a5837c1307a2491b509cd92fb4e4442" +dependencies = [ + "lazy_static", + "partition-identity", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + [[package]] name = "redox_syscall" version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" +[[package]] +name = "rustversion" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088" + [[package]] name = "scoped_threadpool" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" +[[package]] +name = "syn" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f58f7e8eaa0009c5fec437aabf511bd9933e4b2d7407bd05273c01a8906ea7" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "synstructure" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "474aaa926faa1603c40b7885a9eaea29b444d1cb2850cb7c0e37bb1a4182f4fa" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + [[package]] name = "term_grid" version = "0.2.0" @@ -335,6 +440,12 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + [[package]] name = "url" version = "2.2.1" @@ -363,6 +474,12 @@ version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbdbff6266a24120518560b5dc983096efb98462e51d0d68169895b237be3e5d" +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 18aeacdc..777d13d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,8 @@ default = [ "git" ] git = [ "git2" ] vendored-openssl = ["git2/vendored-openssl"] +[target.'cfg(unix)'.dependencies] +proc-mounts = "0.2" # make dev builds faster by excluding debug symbols [profile.dev] diff --git a/src/fs/file.rs b/src/fs/file.rs index ea83f08b..9e753d1b 100644 --- a/src/fs/file.rs +++ b/src/fs/file.rs @@ -7,6 +7,7 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; use log::*; +use crate::ALL_MOUNTS; use crate::fs::dir::Dir; use crate::fs::fields as f; @@ -63,6 +64,9 @@ pub struct File<'dir> { /// directory’s children, and are in fact added specifically by exa; this /// means that they should be skipped when recursing. pub is_all_all: bool, + + /// The absolute value of this path, used to look up mount points. + pub absolute_path: PathBuf, } impl<'dir> File<'dir> { @@ -77,8 +81,9 @@ impl<'dir> File<'dir> { debug!("Statting file {:?}", &path); let metadata = std::fs::symlink_metadata(&path)?; let is_all_all = false; + let absolute_path = std::fs::canonicalize(&path)?; - Ok(File { name, ext, path, metadata, parent_dir, is_all_all }) + Ok(File { name, ext, path, metadata, parent_dir, is_all_all, absolute_path }) } pub fn new_aa_current(parent_dir: &'dir Dir) -> io::Result> { @@ -89,8 +94,9 @@ impl<'dir> File<'dir> { let metadata = std::fs::symlink_metadata(&path)?; let is_all_all = true; let parent_dir = Some(parent_dir); + let absolute_path = std::fs::canonicalize(&path)?; - Ok(File { path, parent_dir, metadata, ext, name: ".".into(), is_all_all }) + Ok(File { path, parent_dir, metadata, ext, name: ".".into(), is_all_all, absolute_path }) } pub fn new_aa_parent(path: PathBuf, parent_dir: &'dir Dir) -> io::Result> { @@ -100,8 +106,9 @@ impl<'dir> File<'dir> { let metadata = std::fs::symlink_metadata(&path)?; let is_all_all = true; let parent_dir = Some(parent_dir); + let absolute_path = std::fs::canonicalize(&path)?; - Ok(File { path, parent_dir, metadata, ext, name: "..".into(), is_all_all }) + Ok(File { path, parent_dir, metadata, ext, name: "..".into(), is_all_all, absolute_path }) } /// A file’s name is derived from its string. This needs to handle directories @@ -204,6 +211,50 @@ 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!(unix) { + if self.is_directory() { + return ALL_MOUNTS.contains_key(&self.absolute_path); + } + } + return false; + } + + /// The filesystem device and type for a mount point + pub fn mount_point_info(&self) -> Option<&MountedFs> { + if cfg!(unix) { + return ALL_MOUNTS.get(&self.absolute_path); + } + None + } + + /// Whether this file (directory) is a `btrfs` subvolume + #[cfg(unix)] + pub fn is_btrfs_subvolume(&self) -> bool { + if cfg!(unix) { + if self.is_directory() && + (self.metadata.ino() == 2 || self.metadata.ino() == 256) { + //This directory matches the characteristics of a btrfs + //subvolume root. Check it's actually on a btrfs fs. + let mut ancestors: Vec = Vec::new(); + for ancestor in self.absolute_path.ancestors() { + ancestors.push(ancestor.to_path_buf()); + } + ancestors.reverse(); + let mut is_on_btrfs = false; + // Start at / and work downwards + for ancestor in ancestors { + is_on_btrfs = match ALL_MOUNTS.get(&ancestor) { + None => is_on_btrfs, + Some(mount) => mount.fstype.eq("btrfs"), + }; + } + return is_on_btrfs; + } + } + return false; + } /// 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 @@ -253,7 +304,7 @@ impl<'dir> File<'dir> { Ok(metadata) => { let ext = File::ext(&path); let name = File::filename(&path); - let file = File { parent_dir: None, path, ext, metadata, name, is_all_all: false }; + let file = File { parent_dir: None, path, ext, metadata, name, is_all_all: false, absolute_path }; FileTarget::Ok(Box::new(file)) } Err(e) => { @@ -479,6 +530,14 @@ impl<'dir> FileTarget<'dir> { } } +/// Details of a mounted filesystem. +pub struct MountedFs { + pub dest: String, + pub fstype: String, + pub source: String, + pub subvolume: Option, +} + /// More readable aliases for the permission bits exposed by libc. #[allow(trivial_numeric_casts)] diff --git a/src/fs/mod.rs b/src/fs/mod.rs index 1188f615..fcbe39f8 100644 --- a/src/fs/mod.rs +++ b/src/fs/mod.rs @@ -2,7 +2,7 @@ mod dir; pub use self::dir::{Dir, DotFilter}; mod file; -pub use self::file::{File, FileTarget}; +pub use self::file::{File, FileTarget, MountedFs}; pub mod dir_action; pub mod feature; diff --git a/src/main.rs b/src/main.rs index bbdf0d68..6857b21d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,16 +22,22 @@ #![allow(clippy::upper_case_acronyms)] #![allow(clippy::wildcard_imports)] +#[macro_use] +extern crate lazy_static; + +use std::collections::HashMap; use std::env; use std::ffi::{OsStr, OsString}; use std::io::{self, Write, ErrorKind}; use std::path::{Component, PathBuf}; use ansi_term::{ANSIStrings, Style}; +#[cfg(unix)] +use proc_mounts::MOUNTS; use log::*; -use crate::fs::{Dir, File}; +use crate::fs::{Dir, File, MountedFs}; use crate::fs::feature::git::GitCache; use crate::fs::filter::GitIgnore; use crate::options::{Options, Vars, vars, OptionsResult}; @@ -45,6 +51,35 @@ mod options; mod output; mod theme; +lazy_static! { + static ref ALL_MOUNTS: HashMap = { + let mut m = HashMap::new(); + #[cfg(unix)] + for mount_point in &MOUNTS.read().unwrap().0 { + let mount = MountedFs { + dest: mount_point.dest.to_string_lossy().into_owned(), + fstype: mount_point.fstype.clone(), + source: mount_point.source.to_string_lossy().into_owned(), + subvolume: match mount_point.fstype.as_str() { + "btrfs" => { + let mut subvol = None; + for option in &mount_point.options { + if option.starts_with("subvol=") { + subvol = Some(option[7..].to_string()); + break; + } + } + subvol + }, + _ => None, + } + }; + m.insert(mount_point.dest.clone(), mount); + } + m + }; +} + fn main() { use std::process::exit; diff --git a/src/output/details.rs b/src/output/details.rs index 62ef7d82..ce502a13 100644 --- a/src/output/details.rs +++ b/src/output/details.rs @@ -288,6 +288,7 @@ impl<'a> Render<'a> { let file_name = self.file_style.for_file(egg.file, self.theme) .with_link_paths() + .with_mount_details() .paint() .promote(); diff --git a/src/output/file_name.rs b/src/output/file_name.rs index b6a38c0e..698ed029 100644 --- a/src/output/file_name.rs +++ b/src/output/file_name.rs @@ -3,7 +3,7 @@ use std::path::Path; use ansi_term::{ANSIString, Style}; -use crate::fs::{File, FileTarget}; +use crate::fs::{File, FileTarget, MountedFs}; use crate::output::cell::TextCellContents; use crate::output::escape; use crate::output::icons::{icon_for_file, iconify_style}; @@ -32,7 +32,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(), } } } @@ -52,6 +54,19 @@ enum LinkStyle { FullLinkPaths, } +/// 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 to append file class characters to the file names. #[derive(PartialEq, Debug, Copy, Clone)] @@ -102,6 +117,12 @@ pub struct FileName<'a, 'dir, C> { link_style: LinkStyle, 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> { @@ -112,6 +133,13 @@ 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) -> Self { + self.mount_style = MountStyle::MountInfo; + self + } } impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> { @@ -179,6 +207,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.coloured_file_name() { @@ -216,6 +246,19 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> { bits.push(Style::default().paint(class)); } } + else 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())); + if mount_details.fstype.eq("btrfs") && mount_details.subvolume.is_some() { + bits.push(Style::default().paint(" @[")); + bits.push(Style::default().paint(mount_details.subvolume.clone().unwrap())); + bits.push(Style::default().paint("]")); + } + bits.push(Style::default().paint(")}")); + } bits.into() } @@ -300,6 +343,9 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> { } match self.file { + f if f.is_mount_point() => self.colours.mount_point(), + #[cfg(unix)] + f if f.is_btrfs_subvolume() => self.colours.btrfs_subvolume(), f if f.is_directory() => self.colours.directory(), f if f.is_executable_file() => self.colours.executable_file(), f if f.is_link() => self.colours.symlink(), @@ -342,6 +388,12 @@ 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; + + /// The style to paint a directory that is the root of a btrfs subvolume. + fn btrfs_subvolume(&self) -> Style; + fn colour_file(&self, file: &File<'_>) -> Style; } diff --git a/src/output/lines.rs b/src/output/lines.rs index 2343ce50..f90003a0 100644 --- a/src/output/lines.rs +++ b/src/output/lines.rs @@ -32,6 +32,7 @@ impl<'a> Render<'a> { self.file_style .for_file(file, self.theme) .with_link_paths() + .with_mount_details() .paint() } } diff --git a/src/theme/default_theme.rs b/src/theme/default_theme.rs index b4269b73..f19ee9a5 100644 --- a/src/theme/default_theme.rs +++ b/src/theme/default_theme.rs @@ -11,15 +11,17 @@ impl UiStyles { colourful: true, filekinds: FileKinds { - normal: Style::default(), - directory: Blue.bold(), - symlink: Cyan.normal(), - pipe: Yellow.normal(), - block_device: Yellow.bold(), - char_device: Yellow.bold(), - socket: Red.bold(), - special: Yellow.normal(), - executable: Green.bold(), + normal: Style::default(), + directory: Blue.bold(), + symlink: Cyan.normal(), + pipe: Yellow.normal(), + block_device: Yellow.bold(), + char_device: Yellow.bold(), + socket: Red.bold(), + special: Yellow.normal(), + executable: Green.bold(), + mount_point: Blue.bold().underline(), + btrfs_subvolume: Blue.underline(), }, perms: Permissions { diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 255f054d..9e8b5d7d 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -300,6 +300,8 @@ 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 btrfs_subvolume(&self) -> Style { self.ui.filekinds.btrfs_subvolume } 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 f92c5442..0702a6f6 100644 --- a/src/theme/ui_styles.rs +++ b/src/theme/ui_styles.rs @@ -38,6 +38,8 @@ pub struct FileKinds { pub socket: Style, pub special: Style, pub executable: Style, + pub mount_point: Style, + pub btrfs_subvolume: Style, } #[derive(Clone, Copy, Debug, Default, PartialEq)]