diff --git a/Cargo.lock b/Cargo.lock index 530c945a2..72b4501f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -370,6 +370,7 @@ dependencies = [ "natord", "num_cpus", "number_prefix", + "once_cell", "percent-encoding", "phf", "proc-mounts", diff --git a/Cargo.toml b/Cargo.toml index 2a0dd12da..a7104b330 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,6 +81,7 @@ log = "0.4" natord = "1.0" num_cpus = "1.16" number_prefix = "0.4" +once_cell = "1.18.0" percent-encoding = "2.3.0" phf = { version = "0.11.2", features = ["macros"] } scoped_threadpool = "0.1" diff --git a/README.md b/README.md index 1c8097941..fb686142d 100644 --- a/README.md +++ b/README.md @@ -356,6 +356,7 @@ These options are available when running with `--long` (`-l`): - **--git-repos-no-status**: list whether a directory is a Git repository, but not its status (faster) - **--no-git**: suppress Git status (always overrides `--git`, `--git-repos`, `--git-repos-no-status`) - **--time-style**: how to format timestamps. valid timestamp styles are ‘`default`’, ‘`iso`’, ‘`long-iso`’, ‘`full-iso`’, ‘`relative`', or you can use a `custom` style with '`+`' as prefix. (Ex: "`+%Y/%m/%d, %H:%M`" => "`2023/9/30, 12:00`"). [more about format syntax](https://docs.rs/chrono/latest/chrono/format/strftime/index.html). +- **--total-size**: show recursive directory size - **--no-permissions**: suppress the permissions field - **-o**, **--octal-permissions**: list each file's permission in octal format - **--no-filesize**: suppress the filesize field @@ -425,4 +426,3 @@ The Nix Flake has a few features: ## Star History [![Star History Chart](https://api.star-history.com/svg?repos=eza-community/eza&type=Date)](https://star-history.com/#eza-community/eza&Date) - diff --git a/completions/fish/eza.fish b/completions/fish/eza.fish index ef1c8cf64..90c1417f4 100644 --- a/completions/fish/eza.fish +++ b/completions/fish/eza.fish @@ -94,6 +94,7 @@ complete -c eza -l time-style -d "How to format timestamps" -x -a " full-iso\t'Display full ISO timestamps, up to the nanosecond' relative\t'Display relative timestamps' " +complete -c eza -l total-size -d "Show recursive directory size" complete -c eza -l no-permissions -d "Suppress the permissions field" complete -c eza -s o -l octal-permissions -d "List each file's permission in octal format" complete -c eza -l no-filesize -d "Suppress the filesize field" diff --git a/completions/nush/eza.nu b/completions/nush/eza.nu index d3b02df14..8c10e7bc9 100644 --- a/completions/nush/eza.nu +++ b/completions/nush/eza.nu @@ -42,6 +42,7 @@ export extern "eza" [ --accessed(-u) # Use the accessed timestamp field --created(-U) # Use the created timestamp field --time-style # How to format timestamps + --total-size # Show recursive directory size --no-permissions # Suppress the permissions field --octal-permissions(-o) # List each file's permission in octal format --no-filesize # Suppress the filesize field diff --git a/completions/zsh/_eza b/completions/zsh/_eza index 22ee6f509..ce1167eb1 100644 --- a/completions/zsh/_eza +++ b/completions/zsh/_eza @@ -49,6 +49,7 @@ __eza() { {-S,--blocksize}"[List each file's size of allocated file system blocks.]" \ {-t,--time}="[Which time field to show]:(time field):(accessed changed created modified)" \ --time-style="[How to format timestamps]:(time style):(default iso long-iso full-iso relative)" \ + --total-size="[Show recursive directory size]" \ --no-permissions"[Suppress the permissions field]" \ {-o,--octal-permissions}"[List each file's permission in octal format]" \ --no-filesize"[Suppress the filesize field]" \ diff --git a/man/eza.1.md b/man/eza.1.md index 066f3e692..b81bf8785 100644 --- a/man/eza.1.md +++ b/man/eza.1.md @@ -201,6 +201,9 @@ These options are available when running with `--long` (`-l`): : Valid timestamp styles are ‘`default`’, ‘`iso`’, ‘`long-iso`’, ‘`full-iso`’, ‘`relative`', or you can use a `custom` style with '`+`' as prefix. (Ex: "`+%Y/%m/%d, %H:%M`" => "`2023/9/30, 12:00`"). for more details about format syntax, please read: https://docs.rs/chrono/latest/chrono/format/strftime/index.html +`--total-size` +: Show recursive directory size. + `-u`, `--accessed` : Use the accessed timestamp field. @@ -230,7 +233,7 @@ These options are available when running with `--long` (`-l`): `--git` [if eza was built with git support] : List each file’s Git status, if tracked. -This adds a two-character column indicating the staged and unstaged statuses respectively. The status character can be ‘`-`’ for not modified, ‘`M`’ for a modified file, ‘`N`’ for a new file, ‘`D`’ for deleted, ‘`R`’ for renamed, ‘`T`’ for type-change, ‘`I`’ for ignored, and ‘`U`’ for conflicted. Directories will be shown to have the status of their contents, which is how ‘deleted’ is possible if a directory contains a file that has a certain status, it will be shown to have that status. +This adds a two-character column indicating the staged and unstaged statuses respectively. The status character can be ‘`-`’ for not modified, ‘`M`’ for a modified file, ‘`N`’ for a new file, ‘`D`’ for deleted, ‘`R`’ for renamed, ‘`T`’ for type-change, ‘`I`’ for ignored, and ‘`U`’ for conflicted. Directories will be shown to have the status of their contents, which is how ‘deleted’ is possible if a directory contains a file that has a certain status, it will be shown to have that status. `--git-repos` [if eza was built with git support] : List each directory’s Git status, if tracked. diff --git a/src/fs/dir.rs b/src/fs/dir.rs index b22245f59..193afebc5 100644 --- a/src/fs/dir.rs +++ b/src/fs/dir.rs @@ -51,6 +51,7 @@ impl Dir { git: Option<&'ig GitCache>, git_ignoring: bool, deref_links: bool, + total_size: bool, ) -> Files<'dir, 'ig> { Files { inner: self.contents.iter(), @@ -60,6 +61,7 @@ impl Dir { git, git_ignoring, deref_links, + total_size, } } @@ -75,6 +77,7 @@ impl Dir { } /// Iterator over reading the contents of a directory as `File` objects. +#[allow(clippy::struct_excessive_bools)] pub struct Files<'dir, 'ig> { /// The internal iterator over the paths that have been read already. inner: SliceIter<'dir, PathBuf>, @@ -95,6 +98,9 @@ pub struct Files<'dir, 'ig> { /// Whether symbolic links should be dereferenced when querying information. deref_links: bool, + + /// Whether to calculate the directory size recursively + total_size: bool, } impl<'dir, 'ig> Files<'dir, 'ig> { @@ -131,8 +137,14 @@ impl<'dir, 'ig> Files<'dir, 'ig> { } } - let file = File::from_args(path.clone(), self.dir, filename, self.deref_links) - .map_err(|e| (path.clone(), e)); + let file = File::from_args( + path.clone(), + self.dir, + filename, + self.deref_links, + self.total_size, + ) + .map_err(|e| (path.clone(), e)); // Windows has its own concept of hidden files, when dotfiles are // hidden Windows hidden files should also be filtered out @@ -169,12 +181,18 @@ impl<'dir, 'ig> Iterator for Files<'dir, 'ig> { match self.dots { DotsNext::Dot => { self.dots = DotsNext::DotDot; - Some(File::new_aa_current(self.dir).map_err(|e| (Path::new(".").to_path_buf(), e))) + Some( + File::new_aa_current(self.dir, self.total_size) + .map_err(|e| (Path::new(".").to_path_buf(), e)), + ) } DotsNext::DotDot => { self.dots = DotsNext::Files; - Some(File::new_aa_parent(self.parent(), self.dir).map_err(|e| (self.parent(), e))) + Some( + File::new_aa_parent(self.parent(), self.dir, self.total_size) + .map_err(|e| (self.parent(), e)), + ) } DotsNext::Files => self.next_visible_file(), diff --git a/src/fs/file.rs b/src/fs/file.rs index ade6b3c5b..131e3f3be 100644 --- a/src/fs/file.rs +++ b/src/fs/file.rs @@ -1,25 +1,41 @@ //! Files, and methods and fields to access their metadata. +#[cfg(unix)] +use std::collections::HashMap; use std::io; #[cfg(unix)] use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt}; #[cfg(windows)] use std::os::windows::fs::MetadataExt; use std::path::{Path, PathBuf}; +#[cfg(unix)] +use std::sync::Mutex; use std::sync::OnceLock; use chrono::prelude::*; use log::*; +#[cfg(unix)] +use once_cell::sync::Lazy; use crate::fs::dir::Dir; use crate::fs::feature::xattr; use crate::fs::feature::xattr::{Attribute, FileAttributes}; use crate::fs::fields as f; +use crate::fs::recursive_size::RecursiveSize; use super::mounts::all_mounts; use super::mounts::MountedFs; +// Maps (device_id, inode) => (size_in_bytes, size_in_blocks) +// Mutex::new is const but HashMap::new is not const requiring us to use lazy +// initialization. +// TODO: Replace with std::sync::LazyLock when it is stable. +#[allow(clippy::type_complexity)] +#[cfg(unix)] +static DIRECTORY_SIZE_CACHE: Lazy>> = + Lazy::new(|| Mutex::new(HashMap::new())); + /// A **File** is a wrapper around one of Rust’s `PathBuf` values, along with /// associated data about the file. /// @@ -79,6 +95,9 @@ pub struct File<'dir> { /// instead. pub deref_links: bool, + /// The recursive directory size when total_size is used. + recursive_size: RecursiveSize, + /// The extended attributes of this file. extended_attributes: OnceLock>, @@ -92,6 +111,7 @@ impl<'dir> File<'dir> { parent_dir: PD, filename: FN, deref_links: bool, + total_size: bool, ) -> io::Result> where PD: Into>, @@ -106,8 +126,13 @@ impl<'dir> File<'dir> { let is_all_all = false; let extended_attributes = OnceLock::new(); let absolute_path = OnceLock::new(); + let recursive_size = if total_size { + RecursiveSize::Unknown + } else { + RecursiveSize::None + }; - Ok(File { + let mut file = File { name, ext, path, @@ -115,13 +140,24 @@ impl<'dir> File<'dir> { parent_dir, is_all_all, deref_links, + recursive_size, extended_attributes, absolute_path, - }) + }; + + if total_size { + file.recursive_size = file.recursive_directory_size(); + } + + Ok(file) } - pub fn new_aa_current(parent_dir: &'dir Dir) -> io::Result> { - let path = parent_dir.path.clone(); + fn new_aa( + path: PathBuf, + parent_dir: &'dir Dir, + name: &'static str, + total_size: bool, + ) -> io::Result> { let ext = File::ext(&path); debug!("Statting file {:?}", &path); @@ -130,41 +166,42 @@ impl<'dir> File<'dir> { let parent_dir = Some(parent_dir); let extended_attributes = OnceLock::new(); let absolute_path = OnceLock::new(); + let recursive_size = if total_size { + RecursiveSize::Unknown + } else { + RecursiveSize::None + }; - Ok(File { + let mut file = File { + name: name.into(), + ext, path, - parent_dir, metadata, - ext, - name: ".".into(), + parent_dir, is_all_all, deref_links: false, extended_attributes, absolute_path, - }) - } + recursive_size, + }; - pub fn new_aa_parent(path: PathBuf, parent_dir: &'dir Dir) -> io::Result> { - let ext = File::ext(&path); + if total_size { + file.recursive_size = file.recursive_directory_size(); + } - debug!("Statting file {:?}", &path); - let metadata = std::fs::symlink_metadata(&path)?; - let is_all_all = true; - let parent_dir = Some(parent_dir); - let extended_attributes = OnceLock::new(); - let absolute_path = OnceLock::new(); + Ok(file) + } - Ok(File { - path, - parent_dir, - metadata, - ext, - name: "..".into(), - is_all_all, - deref_links: false, - extended_attributes, - absolute_path, - }) + pub fn new_aa_current(parent_dir: &'dir Dir, total_size: bool) -> io::Result> { + File::new_aa(parent_dir.path.clone(), parent_dir, ".", total_size) + } + + pub fn new_aa_parent( + path: PathBuf, + parent_dir: &'dir Dir, + total_size: bool, + ) -> io::Result> { + File::new_aa(path, parent_dir, "..", total_size) } /// A file’s name is derived from its string. This needs to handle directories @@ -375,6 +412,7 @@ impl<'dir> File<'dir> { deref_links: self.deref_links, extended_attributes, absolute_path: absolute_path_cell, + recursive_size: RecursiveSize::None, }; FileTarget::Ok(Box::new(file)) } @@ -437,6 +475,10 @@ impl<'dir> File<'dir> { FileTarget::Ok(f) => f.blocksize(), _ => f::Blocksize::None, } + } else if self.is_directory() { + self.recursive_size.map_or(f::Blocksize::None, |_, blocks| { + f::Blocksize::Some(blocks * 512) + }) } else if self.is_file() { // Note that metadata.blocks returns the number of blocks // for 512 byte blocks according to the POSIX standard @@ -453,10 +495,10 @@ impl<'dir> File<'dir> { #[cfg(unix)] pub fn user(&self) -> Option { if self.is_link() && self.deref_links { - match self.link_target_recurse() { - FileTarget::Ok(f) => return f.user(), - _ => return None, - } + return match self.link_target_recurse() { + FileTarget::Ok(f) => f.user(), + _ => None, + }; } Some(f::User(self.metadata.uid())) } @@ -465,26 +507,25 @@ impl<'dir> File<'dir> { #[cfg(unix)] pub fn group(&self) -> Option { if self.is_link() && self.deref_links { - match self.link_target_recurse() { - FileTarget::Ok(f) => return f.group(), - _ => return None, - } + return match self.link_target_recurse() { + FileTarget::Ok(f) => f.group(), + _ => None, + }; } Some(f::Group(self.metadata.gid())) } /// This file’s size, if it’s a regular file. /// - /// For directories, no size is given. Although they do have a size on - /// some filesystems, I’ve never looked at one of those numbers and gained - /// any information from it. So it’s going to be hidden instead. + /// For directories, the recursive size or no size is given depending on + /// flags. Although they do have a size on some filesystems, I’ve never + /// looked at one of those numbers and gained any information from it. /// /// Block and character devices return their device IDs, because they /// usually just have a file size of zero. /// /// Links will return the size of their target (recursively through other - /// links) if dereferencing is enabled, otherwise the size of the link - /// itself. + /// links) if dereferencing is enabled, otherwise None. #[cfg(unix)] pub fn size(&self) -> f::Size { if self.deref_links && self.is_link() { @@ -492,6 +533,9 @@ impl<'dir> File<'dir> { FileTarget::Ok(f) => f.size(), _ => f::Size::None, } + } else if self.is_directory() { + self.recursive_size + .map_or(f::Size::None, |bytes, _| f::Size::Some(bytes)) } else if self.is_char_device() || self.is_block_device() { let device_id = self.metadata.rdev(); @@ -509,7 +553,7 @@ impl<'dir> File<'dir> { } else if self.is_file() { f::Size::Some(self.metadata.len()) } else { - // directory and symlink + // symlink f::Size::None } } @@ -527,6 +571,68 @@ impl<'dir> File<'dir> { } } + /// Calculate the total directory size recursively. If not a directory `None` + /// will be returned. The directory size is cached for recursive directory + /// listing. + #[cfg(unix)] + fn recursive_directory_size(&self) -> RecursiveSize { + if self.is_directory() { + let key = (self.metadata.dev(), self.metadata.ino()); + if let Some(size) = DIRECTORY_SIZE_CACHE.lock().unwrap().get(&key) { + return RecursiveSize::Some(size.0, size.1); + } + Dir::read_dir(self.path.clone()).map_or(RecursiveSize::Unknown, |dir| { + let mut size = 0; + let mut blocks = 0; + for file in dir + .files(super::DotFilter::Dotfiles, None, false, false, true) + .flatten() + { + match file.recursive_directory_size() { + RecursiveSize::Some(bytes, blks) => { + size += bytes; + blocks += blks; + } + RecursiveSize::Unknown => {} + RecursiveSize::None => { + size += file.metadata.size(); + blocks += file.metadata.blocks(); + } + } + } + DIRECTORY_SIZE_CACHE + .lock() + .unwrap() + .insert(key, (size, blocks)); + RecursiveSize::Some(size, blocks) + }) + } else { + RecursiveSize::None + } + } + + /// Windows version always returns None. The metadata for + /// `volume_serial_number` and `file_index` are marked unstable so we can + /// not cache the sizes. Without caching we could end up walking the + /// directory structure several times. + #[cfg(windows)] + fn recursive_directory_size(&self) -> RecursiveSize { + RecursiveSize::None + } + + /// Returns the same value as `self.metadata.len()` or the recursive size + /// of a directory when `total_size` is used. + #[inline] + pub fn length(&self) -> u64 { + self.recursive_size.unwrap_bytes_or(self.metadata.len()) + } + + /// Is the file is using recursive size calculation + #[inline] + pub fn is_recursive_size(&self) -> bool { + !self.recursive_size.is_none() + } + /// Determines if the directory is empty or not. /// /// For Unix platforms, this function first checks the link count to quickly @@ -581,7 +687,7 @@ impl<'dir> File<'dir> { match Dir::read_dir(self.path.clone()) { // . & .. are skipped, if the returned iterator has .next(), it's not empty Ok(has_files) => has_files - .files(super::DotFilter::Dotfiles, None, false, false) + .files(super::DotFilter::Dotfiles, None, false, false, false) .next() .is_none(), Err(_) => false, @@ -691,10 +797,10 @@ impl<'dir> File<'dir> { // If the chain of links is broken, we instead fall through and // return the permissions of the original link, as would have been // done if we were not dereferencing. - match self.link_target_recurse() { - FileTarget::Ok(f) => return f.permissions(), - _ => return None, - } + return match self.link_target_recurse() { + FileTarget::Ok(f) => f.permissions(), + _ => None, + }; } let bits = self.metadata.mode(); let has_bit = |bit| bits & bit == bit; @@ -814,17 +920,17 @@ mod ext_test { #[test] fn extension() { - assert_eq!(Some("dat".to_string()), File::ext(Path::new("fester.dat"))) + assert_eq!(Some("dat".to_string()), File::ext(Path::new("fester.dat"))); } #[test] fn dotfile() { - assert_eq!(Some("vimrc".to_string()), File::ext(Path::new(".vimrc"))) + assert_eq!(Some("vimrc".to_string()), File::ext(Path::new(".vimrc"))); } #[test] fn no_extension() { - assert_eq!(None, File::ext(Path::new("jarlsberg"))) + assert_eq!(None, File::ext(Path::new("jarlsberg"))); } } @@ -835,32 +941,32 @@ mod filename_test { #[test] fn file() { - assert_eq!("fester.dat", File::filename(Path::new("fester.dat"))) + assert_eq!("fester.dat", File::filename(Path::new("fester.dat"))); } #[test] fn no_path() { - assert_eq!("foo.wha", File::filename(Path::new("/var/cache/foo.wha"))) + assert_eq!("foo.wha", File::filename(Path::new("/var/cache/foo.wha"))); } #[test] fn here() { - assert_eq!(".", File::filename(Path::new("."))) + assert_eq!(".", File::filename(Path::new("."))); } #[test] fn there() { - assert_eq!("..", File::filename(Path::new(".."))) + assert_eq!("..", File::filename(Path::new(".."))); } #[test] fn everywhere() { - assert_eq!("..", File::filename(Path::new("./.."))) + assert_eq!("..", File::filename(Path::new("./.."))); } #[test] #[cfg(unix)] fn topmost() { - assert_eq!("/", File::filename(Path::new("/"))) + assert_eq!("/", File::filename(Path::new("/"))); } } diff --git a/src/fs/filter.rs b/src/fs/filter.rs index ca5bcbca9..162cf0ac0 100644 --- a/src/fs/filter.rs +++ b/src/fs/filter.rs @@ -240,7 +240,8 @@ impl SortField { Self::Name(ABCabc) => natord::compare(&a.name, &b.name), Self::Name(AaBbCc) => natord::compare_ignore_case(&a.name, &b.name), - Self::Size => a.metadata.len().cmp(&b.metadata.len()), + Self::Size => a.length().cmp(&b.length()), + #[cfg(unix)] Self::FileInode => a.metadata.ino().cmp(&b.metadata.ino()), Self::ModifiedDate => a.modified_time().cmp(&b.modified_time()), @@ -271,7 +272,7 @@ impl SortField { Self::NameMixHidden(AaBbCc) => natord::compare_ignore_case( Self::strip_dot(&a.name), Self::strip_dot(&b.name) - ) + ), }; } diff --git a/src/fs/mod.rs b/src/fs/mod.rs index 70f35ecf6..0f0d908b2 100644 --- a/src/fs/mod.rs +++ b/src/fs/mod.rs @@ -9,3 +9,4 @@ pub mod feature; pub mod fields; pub mod filter; pub mod mounts; +pub mod recursive_size; diff --git a/src/fs/recursive_size.rs b/src/fs/recursive_size.rs new file mode 100644 index 000000000..7a4e17a96 --- /dev/null +++ b/src/fs/recursive_size.rs @@ -0,0 +1,80 @@ +/// Used to represent a the size of a recursive directory traversal. `None` +/// should be used when the file does not represent a directory or the recursive +/// size should not be calculated. +#[derive(Copy, Clone, Debug)] +pub enum RecursiveSize { + /// Size should not be computed + None, + /// Size should be computed but has not been computed yet + Unknown, + /// Size has been computed. First field is size in bytes and second field + /// is size in blocks + #[cfg_attr(target_family = "windows", allow(dead_code))] + Some(u64, u64), +} + +impl RecursiveSize { + /// Returns `true` if `None` + /// + /// # Examples + /// + /// ``` + /// use eza::fs::recursive_size::RecursiveSize; + /// + /// let x = RecursiveSize::None; + /// assert_eq!(x.is_none(), true); + /// + /// let x = RecursiveSize::Unknown; + /// assert_eq!(x.is_none(), false); + /// + /// let x = RecursiveSize::Some(0, 0); + /// assert_eq!(x.is_none(), false); + /// ``` + #[inline] + pub const fn is_none(&self) -> bool { + matches!(*self, Self::None) + } + + /// Returns the contained [`Some`] value or a provided default. + /// + /// # Examples + /// + /// ``` + /// use eza::fs::recursive_size::RecursiveSize; + /// + /// assert_eq!(RecursiveSize::None.unwrap_bytes_or(1), 1); + /// assert_eq!(RecursiveSize::Unknown.unwrap_bytes_or(1), 1); + /// assert_eq!(RecursiveSize::Some(2, 3).unwrap_bytes_or(1), 2); + /// ``` + #[inline] + pub const fn unwrap_bytes_or(self, default: u64) -> u64 { + match self { + Self::Some(bytes, _blocks) => bytes, + _ => default, + } + } + + /// Returns the provided default result (if None or Unknown), + /// or applies a function to the contained value (if Some). + /// + /// # Examples + /// + /// ``` + /// use eza::fs::recursive_size::RecursiveSize; + /// + /// assert_eq!(RecursiveSize::None.map_or(None, |s, _| Some(s * 2)), None); + /// assert_eq!(RecursiveSize::Unknown.map_or(None, |s, _| Some(s * 2)), None); + /// assert_eq!(RecursiveSize::Some(2, 3).map_or(None, |s, _| Some(s * 2)), Some(4)); + /// ``` + #[inline] + #[cfg_attr(target_family = "windows", allow(dead_code))] + pub fn map_or(self, default: U, f: F) -> U + where + F: FnOnce(u64, u64) -> U, + { + match self { + RecursiveSize::Some(bytes, blocks) => f(bytes, blocks), + _ => default, + } + } +} diff --git a/src/main.rs b/src/main.rs index dbba5e965..18456bc4d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -187,6 +187,7 @@ impl<'args> Exa<'args> { None, None, self.options.view.deref_links, + self.options.view.total_size, ) { Err(e) => { exit_status = 2; @@ -263,6 +264,7 @@ impl<'args> Exa<'args> { self.git.as_ref(), git_ignore, self.options.view.deref_links, + self.options.view.total_size, ) { match file { Ok(file) => children.push(file), @@ -397,8 +399,8 @@ impl<'args> Exa<'args> { let filter = &self.options.filter; let recurse = self.options.dir_action.recurse_options(); let git_ignoring = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore; - let git = self.git.as_ref(); + let r = details::Render { dir, files, diff --git a/src/options/flags.rs b/src/options/flags.rs index db6da964c..427da4396 100644 --- a/src/options/flags.rs +++ b/src/options/flags.rs @@ -14,7 +14,7 @@ pub static TREE: Arg = Arg { short: Some(b'T'), long: "tree", take pub static CLASSIFY: Arg = Arg { short: Some(b'F'), long: "classify", takes_value: TakesValue::Forbidden }; pub static DEREF_LINKS: Arg = Arg { short: Some(b'X'), long: "dereference", takes_value: TakesValue::Forbidden }; pub static WIDTH: Arg = Arg { short: Some(b'w'), long: "width", takes_value: TakesValue::Necessary(None) }; -pub static NO_QUOTES:Arg = Arg { short: None, long: "no-quotes",takes_value: TakesValue::Forbidden }; +pub static NO_QUOTES: Arg = Arg { short: None, long: "no-quotes", takes_value: TakesValue::Forbidden }; pub static COLOR: Arg = Arg { short: None, long: "color", takes_value: TakesValue::Optional(Some(WHEN)) }; pub static COLOUR: Arg = Arg { short: None, long: "colour", takes_value: TakesValue::Optional(Some(WHEN)) }; @@ -34,29 +34,30 @@ pub static IGNORE_GLOB: Arg = Arg { short: Some(b'I'), long: "ignore-glob", take pub static GIT_IGNORE: Arg = Arg { short: None, long: "git-ignore", takes_value: TakesValue::Forbidden }; pub static DIRS_FIRST: Arg = Arg { short: None, long: "group-directories-first", takes_value: TakesValue::Forbidden }; pub static ONLY_DIRS: Arg = Arg { short: Some(b'D'), long: "only-dirs", takes_value: TakesValue::Forbidden }; -pub static ONLY_FILES: Arg = Arg { short: Some(b'f'), long: "only-files", takes_value: TakesValue::Forbidden }; +pub static ONLY_FILES: Arg = Arg { short: Some(b'f'), long: "only-files", takes_value: TakesValue::Forbidden }; const SORTS: Values = &[ "name", "Name", "size", "extension", "Extension", "modified", "changed", "accessed", "created", "inode", "type", "none" ]; // display options -pub static BINARY: Arg = Arg { short: Some(b'b'), long: "binary", takes_value: TakesValue::Forbidden }; -pub static BYTES: Arg = Arg { short: Some(b'B'), long: "bytes", takes_value: TakesValue::Forbidden }; -pub static GROUP: Arg = Arg { short: Some(b'g'), long: "group", takes_value: TakesValue::Forbidden }; -pub static NUMERIC: Arg = Arg { short: Some(b'n'), long: "numeric", takes_value: TakesValue::Forbidden }; -pub static HEADER: Arg = Arg { short: Some(b'h'), long: "header", takes_value: TakesValue::Forbidden }; -pub static ICONS: Arg = Arg { short: None, long: "icons", takes_value: TakesValue::Optional(Some(WHEN))}; -pub static INODE: Arg = Arg { short: Some(b'i'), long: "inode", takes_value: TakesValue::Forbidden }; -pub static LINKS: Arg = Arg { short: Some(b'H'), long: "links", takes_value: TakesValue::Forbidden }; -pub static MODIFIED: Arg = Arg { short: Some(b'm'), long: "modified", takes_value: TakesValue::Forbidden }; -pub static CHANGED: Arg = Arg { short: None, long: "changed", takes_value: TakesValue::Forbidden }; -pub static BLOCKSIZE: Arg = Arg { short: Some(b'S'), long: "blocksize", takes_value: TakesValue::Forbidden }; -pub static TIME: Arg = Arg { short: Some(b't'), long: "time", takes_value: TakesValue::Necessary(Some(TIMES)) }; -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 MOUNTS: Arg = Arg { short: Some(b'M'), long: "mounts", takes_value: TakesValue::Forbidden }; +pub static BINARY: Arg = Arg { short: Some(b'b'), long: "binary", takes_value: TakesValue::Forbidden }; +pub static BYTES: Arg = Arg { short: Some(b'B'), long: "bytes", takes_value: TakesValue::Forbidden }; +pub static GROUP: Arg = Arg { short: Some(b'g'), long: "group", takes_value: TakesValue::Forbidden }; +pub static NUMERIC: Arg = Arg { short: Some(b'n'), long: "numeric", takes_value: TakesValue::Forbidden }; +pub static HEADER: Arg = Arg { short: Some(b'h'), long: "header", takes_value: TakesValue::Forbidden }; +pub static ICONS: Arg = Arg { short: None, long: "icons", takes_value: TakesValue::Optional(Some(WHEN))}; +pub static INODE: Arg = Arg { short: Some(b'i'), long: "inode", takes_value: TakesValue::Forbidden }; +pub static LINKS: Arg = Arg { short: Some(b'H'), long: "links", takes_value: TakesValue::Forbidden }; +pub static MODIFIED: Arg = Arg { short: Some(b'm'), long: "modified", takes_value: TakesValue::Forbidden }; +pub static CHANGED: Arg = Arg { short: None, long: "changed", takes_value: TakesValue::Forbidden }; +pub static BLOCKSIZE: Arg = Arg { short: Some(b'S'), long: "blocksize", takes_value: TakesValue::Forbidden }; +pub static TOTAL_SIZE: Arg = Arg { short: None, long: "total-size", takes_value: TakesValue::Forbidden }; +pub static TIME: Arg = Arg { short: Some(b't'), long: "time", takes_value: TakesValue::Necessary(Some(TIMES)) }; +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 MOUNTS: Arg = Arg { short: Some(b'M'), long: "mounts", takes_value: TakesValue::Forbidden }; pub static SMART_GROUP: Arg = Arg { short: None, long: "smart-group", takes_value: TakesValue::Forbidden }; const TIMES: Values = &["modified", "changed", "accessed", "created"]; const TIME_STYLES: Values = &["default", "long-iso", "full-iso", "iso", "relative"]; @@ -86,7 +87,7 @@ pub static ALL_ARGS: Args = Args(&[ &IGNORE_GLOB, &GIT_IGNORE, &ONLY_DIRS, &ONLY_FILES, &BINARY, &BYTES, &GROUP, &NUMERIC, &HEADER, &ICONS, &INODE, &LINKS, &MODIFIED, &CHANGED, - &BLOCKSIZE, &TIME, &ACCESSED, &CREATED, &TIME_STYLE, &HYPERLINK, &MOUNTS, + &BLOCKSIZE, &TOTAL_SIZE, &TIME, &ACCESSED, &CREATED, &TIME_STYLE, &HYPERLINK, &MOUNTS, &NO_PERMISSIONS, &NO_FILESIZE, &NO_USER, &NO_TIME, &SMART_GROUP, &GIT, &NO_GIT, &GIT_REPOS, &GIT_REPOS_NO_STAT, diff --git a/src/options/help.rs b/src/options/help.rs index e87a9d1af..9b0fb010e 100644 --- a/src/options/help.rs +++ b/src/options/help.rs @@ -65,6 +65,7 @@ LONG VIEW OPTIONS -U, --created use the created timestamp field --changed use the changed timestamp field --time-style how to format timestamps (default, iso, long-iso, full-iso, relative, or a custom style with '+' as prefix. Ex: '+%Y/%m/%d') + --total-size show the size of a directory as the size of all files and directories inside --no-permissions suppress the permissions field -o, --octal-permissions list each file's permission in octal format --no-filesize suppress the filesize field diff --git a/src/options/view.rs b/src/options/view.rs index c9ed17ccc..0e16233b6 100644 --- a/src/options/view.rs +++ b/src/options/view.rs @@ -13,6 +13,7 @@ impl View { pub fn deduce(matches: &MatchedFlags<'_>, vars: &V) -> Result { let mode = Mode::deduce(matches, vars)?; let deref_links = matches.has(&flags::DEREF_LINKS)?; + let total_size = matches.has(&flags::TOTAL_SIZE)?; let width = TerminalWidth::deduce(matches, vars)?; let file_style = FileStyle::deduce(matches, vars, width.actual_terminal_width().is_some())?; Ok(Self { @@ -20,6 +21,7 @@ impl View { width, file_style, deref_links, + total_size, }) } } diff --git a/src/output/details.rs b/src/output/details.rs index 152423779..485fcae60 100644 --- a/src/output/details.rs +++ b/src/output/details.rs @@ -347,6 +347,7 @@ impl<'a> Render<'a> { self.git, self.git_ignoring, egg.file.deref_links, + egg.file.is_recursive_size(), ) { match file_to_add { Ok(f) => { diff --git a/src/output/mod.rs b/src/output/mod.rs index feb633681..a61effe1c 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -22,6 +22,7 @@ pub struct View { pub width: TerminalWidth, pub file_style: file_name::Options, pub deref_links: bool, + pub total_size: bool, } /// The **mode** is the “type” of output.