diff --git a/src/fs/file.rs b/src/fs/file.rs index 306d33e66..a7984b5c4 100644 --- a/src/fs/file.rs +++ b/src/fs/file.rs @@ -400,6 +400,45 @@ impl<'dir> File<'dir> { .is_some_and(|p| all_mounts().contains_key(p)) } + /// Whether the directory represents a Btrfs subvolume + #[cfg(target_os = "linux")] + pub fn is_btrfs_subvolume(&self) -> bool { + // Listing all subvolumes with ioctl(BTRFS_IOC_TREE_SEARCH) requires CAP_SYS_ADMIN, normal users can't do that. + // So we test the inode number, directory representing a subvolume has always inode number 256 + const BTRFS_FIRST_FREE_OBJECTID: u64 = 256; + self.metadata.file_type().is_dir() + && self.metadata.ino() == BTRFS_FIRST_FREE_OBJECTID + && self.is_btrfs().unwrap_or(false) + } + + #[cfg(not(target_os = "linux"))] + pub fn is_btrfs_subvolume(&self) -> bool { + false + } + + #[cfg(target_os = "linux")] + fn is_btrfs(&self) -> io::Result { + use std::os::unix::ffi::OsStrExt; + + const BTRFS_FSTYPE_NAME: &str = "btrfs"; + + for part in self.absolute_path().unwrap_or(&self.path).ancestors() { + if let Some(mount) = all_mounts().get(part) { + return Ok(mount.fstype == BTRFS_FSTYPE_NAME); + } + } + + // /proc/mounts not working? fallback to statfs + let file_path = &self.path; + let mut out = std::mem::MaybeUninit::::uninit(); + let path = std::ffi::CString::new(file_path.as_os_str().as_bytes()).unwrap(); + match unsafe { libc::statfs(path.as_ptr(), out.as_mut_ptr()) } { + 0 => Ok(unsafe { out.assume_init() }.f_type == libc::BTRFS_SUPER_MAGIC), + _ => Err(io::Error::last_os_error()), + // eprintln!("eza: statfs {:?}: {}", self.path, os_err); + } + } + /// The filesystem device and type for a mount point pub fn mount_point_info(&self) -> Option<&MountedFs> { if cfg!(any(target_os = "linux", target_os = "macos")) { diff --git a/src/options/config.rs b/src/options/config.rs index ff8ddb6ad..495f60199 100644 --- a/src/options/config.rs +++ b/src/options/config.rs @@ -266,6 +266,7 @@ pub struct FileKindsOverride { pub special: Option, // sp pub executable: Option, // ex pub mount_point: Option, // mp + pub btrfs_subvol: Option, // sv } impl FromOverride for FileKinds { @@ -281,6 +282,7 @@ impl FromOverride for FileKinds { special: FromOverride::from(value.special, default.special), executable: FromOverride::from(value.executable, default.executable), mount_point: FromOverride::from(value.mount_point, default.mount_point), + btrfs_subvol: FromOverride::from(value.btrfs_subvol, default.btrfs_subvol), } } } diff --git a/src/output/file_name.rs b/src/output/file_name.rs index ae232a3d9..6444c7cd6 100644 --- a/src/output/file_name.rs +++ b/src/output/file_name.rs @@ -488,6 +488,7 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> { #[rustfmt::skip] return match self.file { f if f.is_mount_point() => self.colours.mount_point(), + f if f.is_btrfs_subvolume() => self.colours.btrfs_subvol(), f if f.is_directory() => self.colours.directory(), #[cfg(unix)] f if f.is_executable_file() => self.colours.executable_file(), @@ -541,6 +542,9 @@ pub trait Colours: FiletypeColours { /// The style to paint a directory that has a filesystem mounted on it. fn mount_point(&self) -> Style; + /// The style to paint a directory representing a Btrfs subvolume. + fn btrfs_subvol(&self) -> Style; + fn colour_file(&self, file: &File<'_>) -> Style; fn style_override(&self, file: &File<'_>) -> Option; diff --git a/src/theme/default_theme.rs b/src/theme/default_theme.rs index 6817fa4da..71c027b71 100644 --- a/src/theme/default_theme.rs +++ b/src/theme/default_theme.rs @@ -36,6 +36,7 @@ impl Default for UiStyles { special: Some(Yellow.normal()), executable: Some(Green.bold()), mount_point: Some(Blue.bold().underline()), + btrfs_subvol: Some(Blue.underline()), }), #[rustfmt::skip] diff --git a/src/theme/mod.rs b/src/theme/mod.rs index c62abc1db..79b8ec1fb 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -412,6 +412,9 @@ impl FileNameColours for Theme { fn broken_control_char(&self) -> Style { apply_overlay(self.ui.control_char(), self.ui.broken_path_overlay()) } fn executable_file(&self) -> Style { self.ui.filekinds.unwrap_or_default().executable() } fn mount_point(&self) -> Style { self.ui.filekinds.unwrap_or_default().mount_point() } + fn btrfs_subvol(&self) -> Style { + self.ui.filekinds.unwrap_or_default().btrfs_subvol() + } fn colour_file(&self, file: &File<'_>) -> Style { self.exts @@ -643,6 +646,7 @@ mod customs_test { test!(exa_bo: ls "", exa "bO=4" => colours c -> { c.broken_path_overlay = Some(Style::default().underline()); }); test!(exa_mp: ls "", exa "mp=1;34;4" => colours c -> { c.filekinds().mount_point = Some(Blue.bold().underline()); }); + test!(exa_sv: ls "", exa "sv=0;34;4" => colours c -> { c.filekinds().btrfs_subvol = Some(Blue.underline()); }); test!(exa_sp: ls "", exa "sp=1;35;4" => colours c -> { c.filekinds().special = Some(Purple.bold().underline()); }); test!(exa_im: ls "", exa "im=38;5;128" => colours c -> { c.file_type().image = Some(Fixed(128).normal()); }); diff --git a/src/theme/ui_styles.rs b/src/theme/ui_styles.rs index c151260c7..816d3908d 100644 --- a/src/theme/ui_styles.rs +++ b/src/theme/ui_styles.rs @@ -125,6 +125,7 @@ pub struct FileKinds { pub special: Option