diff --git a/completions/_dust b/completions/_dust index c6ff4d00..48e608db 100644 --- a/completions/_dust +++ b/completions/_dust @@ -44,6 +44,8 @@ _dust() { '-y+[just like -mtime, but based on file change time]: : ' \ '--ctime=[just like -mtime, but based on file change time]: : ' \ '--files0-from=[run dust on NUL-terminated file names specified in file; if argument is -, then read names from standard input]: :_files' \ +'-m+[Directory '\''size'\'' is max filetime of child files instead of disk size. while a/c/m for last accessed/changed/modified time]: :(a c m)' \ +'--filetime=[Directory '\''size'\'' is max filetime of child files instead of disk size. while a/c/m for last accessed/changed/modified time]: :(a c m)' \ '-p[Subdirectories will not have their path shortened]' \ '--full-paths[Subdirectories will not have their path shortened]' \ '-L[dereference sym links - Treat sym links as directories and go into them]' \ diff --git a/completions/_dust.ps1 b/completions/_dust.ps1 index 145a036a..0432af17 100644 --- a/completions/_dust.ps1 +++ b/completions/_dust.ps1 @@ -50,6 +50,8 @@ Register-ArgumentCompleter -Native -CommandName 'dust' -ScriptBlock { [CompletionResult]::new('-y', 'y', [CompletionResultType]::ParameterName, 'just like -mtime, but based on file change time') [CompletionResult]::new('--ctime', 'ctime', [CompletionResultType]::ParameterName, 'just like -mtime, but based on file change time') [CompletionResult]::new('--files0-from', 'files0-from', [CompletionResultType]::ParameterName, 'run dust on NUL-terminated file names specified in file; if argument is -, then read names from standard input') + [CompletionResult]::new('-m', 'm', [CompletionResultType]::ParameterName, 'Directory ''size'' is max filetime of child files instead of disk size. while a/c/m for last accessed/changed/modified time') + [CompletionResult]::new('--filetime', 'filetime', [CompletionResultType]::ParameterName, 'Directory ''size'' is max filetime of child files instead of disk size. while a/c/m for last accessed/changed/modified time') [CompletionResult]::new('-p', 'p', [CompletionResultType]::ParameterName, 'Subdirectories will not have their path shortened') [CompletionResult]::new('--full-paths', 'full-paths', [CompletionResultType]::ParameterName, 'Subdirectories will not have their path shortened') [CompletionResult]::new('-L', 'L ', [CompletionResultType]::ParameterName, 'dereference sym links - Treat sym links as directories and go into them') diff --git a/completions/dust.bash b/completions/dust.bash index c7ed8058..a0babd24 100644 --- a/completions/dust.bash +++ b/completions/dust.bash @@ -19,7 +19,7 @@ _dust() { case "${cmd}" in dust) - opts="-d -T -n -p -X -I -L -x -s -r -c -C -b -B -z -R -f -i -v -e -t -w -P -D -F -o -S -j -M -A -y -h -V --depth --threads --number-of-lines --full-paths --ignore-directory --ignore-all-in-file --dereference-links --limit-filesystem --apparent-size --reverse --no-colors --force-colors --no-percent-bars --bars-on-right --min-size --screen-reader --skip-total --filecount --ignore_hidden --invert-filter --filter --file_types --terminal_width --no-progress --print-errors --only-dir --only-file --output-format --stack-size --output-json --mtime --atime --ctime --files0-from --help --version [PATH]..." + opts="-d -T -n -p -X -I -L -x -s -r -c -C -b -B -z -R -f -i -v -e -t -w -P -D -F -o -S -j -M -A -y -m -h -V --depth --threads --number-of-lines --full-paths --ignore-directory --ignore-all-in-file --dereference-links --limit-filesystem --apparent-size --reverse --no-colors --force-colors --no-percent-bars --bars-on-right --min-size --screen-reader --skip-total --filecount --ignore_hidden --invert-filter --filter --file_types --terminal_width --no-progress --print-errors --only-dir --only-file --output-format --stack-size --output-json --mtime --atime --ctime --files0-from --filetime --help --version [PATH]..." if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -163,6 +163,14 @@ _dust() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; + --filetime) + COMPREPLY=($(compgen -W "a c m" -- "${cur}")) + return 0 + ;; + -m) + COMPREPLY=($(compgen -W "a c m" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; diff --git a/completions/dust.elv b/completions/dust.elv index f8cfc8aa..fa62ef7a 100644 --- a/completions/dust.elv +++ b/completions/dust.elv @@ -47,6 +47,8 @@ set edit:completion:arg-completer[dust] = {|@words| cand -y 'just like -mtime, but based on file change time' cand --ctime 'just like -mtime, but based on file change time' cand --files0-from 'run dust on NUL-terminated file names specified in file; if argument is -, then read names from standard input' + cand -m 'Directory ''size'' is max filetime of child files instead of disk size. while a/c/m for last accessed/changed/modified time' + cand --filetime 'Directory ''size'' is max filetime of child files instead of disk size. while a/c/m for last accessed/changed/modified time' cand -p 'Subdirectories will not have their path shortened' cand --full-paths 'Subdirectories will not have their path shortened' cand -L 'dereference sym links - Treat sym links as directories and go into them' diff --git a/completions/dust.fish b/completions/dust.fish index f0056a59..ff6fd611 100644 --- a/completions/dust.fish +++ b/completions/dust.fish @@ -13,6 +13,7 @@ complete -c dust -s M -l mtime -d '+/-n matches files modified more/less than n complete -c dust -s A -l atime -d 'just like -mtime, but based on file access time' -r complete -c dust -s y -l ctime -d 'just like -mtime, but based on file change time' -r complete -c dust -l files0-from -d 'run dust on NUL-terminated file names specified in file; if argument is -, then read names from standard input' -r -F +complete -c dust -s m -l filetime -d 'Directory \'size\' is max filetime of child files instead of disk size. while a/c/m for last accessed/changed/modified time' -r -f -a "{a '',c '',m ''}" complete -c dust -s p -l full-paths -d 'Subdirectories will not have their path shortened' complete -c dust -s L -l dereference-links -d 'dereference sym links - Treat sym links as directories and go into them' complete -c dust -s x -l limit-filesystem -d 'Only count the files and directories on the same filesystem as the supplied directory' diff --git a/man-page/dust.1 b/man-page/dust.1 index 74c7f2a1..5ceebdde 100644 --- a/man-page/dust.1 +++ b/man-page/dust.1 @@ -4,7 +4,7 @@ .SH NAME Dust \- Like du but more intuitive .SH SYNOPSIS -\fBdust\fR [\fB\-d\fR|\fB\-\-depth\fR] [\fB\-T\fR|\fB\-\-threads\fR] [\fB\-n\fR|\fB\-\-number\-of\-lines\fR] [\fB\-p\fR|\fB\-\-full\-paths\fR] [\fB\-X\fR|\fB\-\-ignore\-directory\fR] [\fB\-I\fR|\fB\-\-ignore\-all\-in\-file\fR] [\fB\-L\fR|\fB\-\-dereference\-links\fR] [\fB\-x\fR|\fB\-\-limit\-filesystem\fR] [\fB\-s\fR|\fB\-\-apparent\-size\fR] [\fB\-r\fR|\fB\-\-reverse\fR] [\fB\-c\fR|\fB\-\-no\-colors\fR] [\fB\-C\fR|\fB\-\-force\-colors\fR] [\fB\-b\fR|\fB\-\-no\-percent\-bars\fR] [\fB\-B\fR|\fB\-\-bars\-on\-right\fR] [\fB\-z\fR|\fB\-\-min\-size\fR] [\fB\-R\fR|\fB\-\-screen\-reader\fR] [\fB\-\-skip\-total\fR] [\fB\-f\fR|\fB\-\-filecount\fR] [\fB\-i\fR|\fB\-\-ignore_hidden\fR] [\fB\-v\fR|\fB\-\-invert\-filter\fR] [\fB\-e\fR|\fB\-\-filter\fR] [\fB\-t\fR|\fB\-\-file_types\fR] [\fB\-w\fR|\fB\-\-terminal_width\fR] [\fB\-P\fR|\fB\-\-no\-progress\fR] [\fB\-\-print\-errors\fR] [\fB\-D\fR|\fB\-\-only\-dir\fR] [\fB\-F\fR|\fB\-\-only\-file\fR] [\fB\-o\fR|\fB\-\-output\-format\fR] [\fB\-S\fR|\fB\-\-stack\-size\fR] [\fB\-j\fR|\fB\-\-output\-json\fR] [\fB\-M\fR|\fB\-\-mtime\fR] [\fB\-A\fR|\fB\-\-atime\fR] [\fB\-y\fR|\fB\-\-ctime\fR] [\fB\-\-files0\-from\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fIPATH\fR] +\fBdust\fR [\fB\-d\fR|\fB\-\-depth\fR] [\fB\-T\fR|\fB\-\-threads\fR] [\fB\-n\fR|\fB\-\-number\-of\-lines\fR] [\fB\-p\fR|\fB\-\-full\-paths\fR] [\fB\-X\fR|\fB\-\-ignore\-directory\fR] [\fB\-I\fR|\fB\-\-ignore\-all\-in\-file\fR] [\fB\-L\fR|\fB\-\-dereference\-links\fR] [\fB\-x\fR|\fB\-\-limit\-filesystem\fR] [\fB\-s\fR|\fB\-\-apparent\-size\fR] [\fB\-r\fR|\fB\-\-reverse\fR] [\fB\-c\fR|\fB\-\-no\-colors\fR] [\fB\-C\fR|\fB\-\-force\-colors\fR] [\fB\-b\fR|\fB\-\-no\-percent\-bars\fR] [\fB\-B\fR|\fB\-\-bars\-on\-right\fR] [\fB\-z\fR|\fB\-\-min\-size\fR] [\fB\-R\fR|\fB\-\-screen\-reader\fR] [\fB\-\-skip\-total\fR] [\fB\-f\fR|\fB\-\-filecount\fR] [\fB\-i\fR|\fB\-\-ignore_hidden\fR] [\fB\-v\fR|\fB\-\-invert\-filter\fR] [\fB\-e\fR|\fB\-\-filter\fR] [\fB\-t\fR|\fB\-\-file_types\fR] [\fB\-w\fR|\fB\-\-terminal_width\fR] [\fB\-P\fR|\fB\-\-no\-progress\fR] [\fB\-\-print\-errors\fR] [\fB\-D\fR|\fB\-\-only\-dir\fR] [\fB\-F\fR|\fB\-\-only\-file\fR] [\fB\-o\fR|\fB\-\-output\-format\fR] [\fB\-S\fR|\fB\-\-stack\-size\fR] [\fB\-j\fR|\fB\-\-output\-json\fR] [\fB\-M\fR|\fB\-\-mtime\fR] [\fB\-A\fR|\fB\-\-atime\fR] [\fB\-y\fR|\fB\-\-ctime\fR] [\fB\-\-files0\-from\fR] [\fB\-m\fR|\fB\-\-filetime\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fIPATH\fR] .SH DESCRIPTION Like du but more intuitive .SH OPTIONS @@ -115,6 +115,13 @@ just like \-mtime, but based on file change time \fB\-\-files0\-from\fR run dust on NUL\-terminated file names specified in file; if argument is \-, then read names from standard input .TP +\fB\-m\fR, \fB\-\-filetime\fR +Directory \*(Aqsize\*(Aq is max filetime of child files instead of disk size. while a/c/m for last accessed/changed/modified time +.br + +.br +[\fIpossible values: \fRa, c, m] +.TP \fB\-h\fR, \fB\-\-help\fR Print help .TP diff --git a/src/cli.rs b/src/cli.rs index a429297f..66a8c0c8 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -294,4 +294,16 @@ pub fn build_cli() -> Command { .num_args(1) .help("run dust on NUL-terminated file names specified in file; if argument is -, then read names from standard input"), ) + .arg( + Arg::new("filetime") + .short('m') + .long("filetime") + .num_args(1) + .value_parser([ + PossibleValue::new("a").alias("accessed"), + PossibleValue::new("c").alias("changed"), + PossibleValue::new("m").alias("modified"), + ]) + .help("Directory 'size' is max filetime of child files instead of disk size. while a/c/m for last accessed/changed/modified time"), + ) } diff --git a/src/config.rs b/src/config.rs index dceb7838..723a1ec4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,3 +1,4 @@ +use crate::node::FileTime; use chrono::{Local, TimeZone}; use clap::ArgMatches; use config_file::FromConfigFile; @@ -84,6 +85,20 @@ impl Config { }) .to_lowercase() } + + pub fn get_filetime(&self, options: &ArgMatches) -> Option { + let out_fmt = options.get_one::("filetime"); + match out_fmt { + None => None, + Some(x) => match x.as_str() { + "m" | "modified" => Some(FileTime::Modified), + "a" | "accessed" => Some(FileTime::Accessed), + "c" | "changed" => Some(FileTime::Changed), + _ => unreachable!(), + }, + } + } + pub fn get_skip_total(&self, options: &ArgMatches) -> bool { Some(true) == self.skip_total || options.get_flag("skip_total") } @@ -159,7 +174,7 @@ impl Config { ) } - pub fn get_created_time_operator(&self, options: &ArgMatches) -> Option<(Operater, i64)> { + pub fn get_changed_time_operator(&self, options: &ArgMatches) -> Option<(Operater, i64)> { get_filter_time_operator( options.get_one::("ctime"), get_current_date_epoch_seconds(), @@ -259,6 +274,7 @@ mod tests { #[allow(unused_imports)] use super::*; use chrono::{Datelike, Timelike}; + use clap::builder::PossibleValue; use clap::{value_parser, Arg, ArgMatches, Command}; #[test] @@ -338,4 +354,56 @@ mod tests { ) .get_matches_from(args) } + + #[test] + fn test_get_filetime() { + // No config and no flag. + let c = Config::default(); + let args = get_filetime_args(vec!["dust"]); + assert_eq!(c.get_filetime(&args), None); + + // Config is not defined and flag is defined as access time + let c = Config::default(); + let args = get_filetime_args(vec!["dust", "--filetime", "a"]); + assert_eq!(c.get_filetime(&args), Some(FileTime::Accessed)); + + let c = Config::default(); + let args = get_filetime_args(vec!["dust", "--filetime", "accessed"]); + assert_eq!(c.get_filetime(&args), Some(FileTime::Accessed)); + + // Config is not defined and flag is defined as modified time + let c = Config::default(); + let args = get_filetime_args(vec!["dust", "--filetime", "m"]); + assert_eq!(c.get_filetime(&args), Some(FileTime::Modified)); + + let c = Config::default(); + let args = get_filetime_args(vec!["dust", "--filetime", "modified"]); + assert_eq!(c.get_filetime(&args), Some(FileTime::Modified)); + + // Config is not defined and flag is defined as changed time + let c = Config::default(); + let args = get_filetime_args(vec!["dust", "--filetime", "c"]); + assert_eq!(c.get_filetime(&args), Some(FileTime::Changed)); + + let c = Config::default(); + let args = get_filetime_args(vec!["dust", "--filetime", "changed"]); + assert_eq!(c.get_filetime(&args), Some(FileTime::Changed)); + } + + fn get_filetime_args(args: Vec<&str>) -> ArgMatches { + Command::new("Dust") + .arg( + Arg::new("filetime") + .short('m') + .long("filetime") + .num_args(1) + .value_parser([ + PossibleValue::new("a").alias("accessed"), + PossibleValue::new("c").alias("changed"), + PossibleValue::new("m").alias("modified"), + ]) + .help("Directory 'size' is max filetime of child files instead of disk size. while a/c/m for accessed/changed/modified time"), + ) + .get_matches_from(args) + } } diff --git a/src/dir_walker.rs b/src/dir_walker.rs index f4836393..33249e79 100644 --- a/src/dir_walker.rs +++ b/src/dir_walker.rs @@ -21,6 +21,7 @@ use std::collections::HashSet; use crate::node::build_node; use std::fs::DirEntry; +use crate::node::FileTime; use crate::platform::get_metadata; #[derive(Debug)] @@ -40,6 +41,7 @@ pub struct WalkData<'a> { pub filter_changed_time: Option<(Operater, i64)>, pub use_apparent_size: bool, pub by_filecount: bool, + pub by_filetime: &'a Option, pub ignore_hidden: bool, pub follow_links: bool, pub progress_data: Arc, @@ -57,19 +59,15 @@ pub fn walk_it(dirs: HashSet, walk_data: &WalkData) -> Vec { prog_data.state.store(Operation::PREPARING, ORDERING); - clean_inodes(node, &mut inodes, walk_data.use_apparent_size) + clean_inodes(node, &mut inodes, walk_data) }) .collect(); top_level_nodes } // Remove files which have the same inode, we don't want to double count them. -fn clean_inodes( - x: Node, - inodes: &mut HashSet<(u64, u64)>, - use_apparent_size: bool, -) -> Option { - if !use_apparent_size { +fn clean_inodes(x: Node, inodes: &mut HashSet<(u64, u64)>, walk_data: &WalkData) -> Option { + if !walk_data.use_apparent_size { if let Some(id) = x.inode_device { if !inodes.insert(id) { return None; @@ -82,12 +80,25 @@ fn clean_inodes( tmp.sort_by(sort_by_inode); let new_children: Vec<_> = tmp .into_iter() - .filter_map(|c| clean_inodes(c, inodes, use_apparent_size)) + .filter_map(|c| clean_inodes(c, inodes, walk_data)) .collect(); + let actual_size = if walk_data.by_filetime.is_some() { + // If by_filetime is Some, directory 'size' is the maximum filetime among child files instead of disk size + new_children + .iter() + .map(|c| c.size) + .chain(std::iter::once(x.size)) + .max() + .unwrap_or(0) + } else { + // If by_filetime is None, directory 'size' is the sum of disk sizes or file counts of child files + x.size + new_children.iter().map(|c| c.size).sum::() + }; + Some(Node { name: x.name, - size: x.size + new_children.iter().map(|c| c.size).sum::(), + size: actual_size, children: new_children, inode_device: x.inode_device, depth: x.depth, @@ -270,17 +281,43 @@ mod tests { } } + #[cfg(test)] + fn create_walker<'a>(use_apparent_size: bool) -> WalkData<'a> { + use crate::PIndicator; + let indicator = PIndicator::build_me(); + WalkData { + ignore_directories: HashSet::new(), + filter_regex: &[], + invert_filter_regex: &[], + allowed_filesystems: HashSet::new(), + filter_modified_time: Some((Operater::GreaterThan, 0)), + filter_accessed_time: Some((Operater::GreaterThan, 0)), + filter_changed_time: Some((Operater::GreaterThan, 0)), + use_apparent_size, + by_filecount: false, + by_filetime: &None, + ignore_hidden: false, + follow_links: false, + progress_data: indicator.data.clone(), + errors: Arc::new(Mutex::new(RuntimeErrors::default())), + } + } + #[test] #[allow(clippy::redundant_clone)] fn test_should_ignore_file() { let mut inodes = HashSet::new(); let n = create_node(); + let walkdata = create_walker(false); // First time we insert the node - assert_eq!(clean_inodes(n.clone(), &mut inodes, false), Some(n.clone())); + assert_eq!( + clean_inodes(n.clone(), &mut inodes, &walkdata), + Some(n.clone()) + ); // Second time is a duplicate - we ignore it - assert_eq!(clean_inodes(n.clone(), &mut inodes, false), None); + assert_eq!(clean_inodes(n.clone(), &mut inodes, &walkdata), None); } #[test] @@ -288,10 +325,17 @@ mod tests { fn test_should_not_ignore_files_if_using_apparent_size() { let mut inodes = HashSet::new(); let n = create_node(); + let walkdata = create_walker(true); // If using apparent size we include Nodes, even if duplicate inodes - assert_eq!(clean_inodes(n.clone(), &mut inodes, true), Some(n.clone())); - assert_eq!(clean_inodes(n.clone(), &mut inodes, true), Some(n.clone())); + assert_eq!( + clean_inodes(n.clone(), &mut inodes, &walkdata), + Some(n.clone()) + ); + assert_eq!( + clean_inodes(n.clone(), &mut inodes, &walkdata), + Some(n.clone()) + ); } #[test] diff --git a/src/display.rs b/src/display.rs index 28b58989..84a026cc 100644 --- a/src/display.rs +++ b/src/display.rs @@ -1,4 +1,5 @@ use crate::display_node::DisplayNode; +use crate::node::FileTime; use ansi_term::Colour::Red; use lscolors::{LsColors, Style}; @@ -7,6 +8,7 @@ use unicode_width::UnicodeWidthStr; use stfu8::encode_u8; +use chrono::{DateTime, Local, TimeZone, Utc}; use std::cmp::max; use std::cmp::min; use std::fs; @@ -16,12 +18,14 @@ use thousands::Separable; pub static UNITS: [char; 4] = ['T', 'G', 'M', 'K']; static BLOCKS: [char; 5] = ['█', '▓', '▒', '░', ' ']; +const FILETIME_SHOW_LENGTH: usize = 19; pub struct InitialDisplayData { pub short_paths: bool, pub is_reversed: bool, pub colors_on: bool, pub by_filecount: bool, + pub by_filetime: Option, pub is_screen_reader: bool, pub output_format: String, pub bars_on_right: bool, @@ -141,6 +145,8 @@ pub fn draw_it( let num_chars_needed_on_left_most = if idd.by_filecount { let max_size = biggest.size; max_size.separate_with_commas().chars().count() + } else if idd.by_filetime.is_some() { + FILETIME_SHOW_LENGTH } else { find_biggest_size_str(root_node, &idd.output_format) }; @@ -342,6 +348,8 @@ pub fn format_string( if display_data.initial.is_screen_reader { // if screen_reader then bars is 'depth' format!("{pretty_name} {bars} {pretty_size}{percent}") + } else if display_data.initial.by_filetime.is_some() { + format!("{pretty_size} {indent}{pretty_name}") } else { format!("{pretty_size} {indent} {pretty_name}{percent}") } @@ -376,6 +384,8 @@ fn get_name_percent( fn get_pretty_size(node: &DisplayNode, is_biggest: bool, display_data: &DisplayData) -> String { let output = if display_data.initial.by_filecount { node.size.separate_with_commas() + } else if display_data.initial.by_filetime.is_some() { + get_pretty_file_modified_time(node.size as i64) } else { human_readable_number(node.size, &display_data.initial.output_format) }; @@ -389,6 +399,14 @@ fn get_pretty_size(node: &DisplayNode, is_biggest: bool, display_data: &DisplayD } } +fn get_pretty_file_modified_time(timestamp: i64) -> String { + let datetime: DateTime = Utc.timestamp_opt(timestamp, 0).unwrap(); + + let local_datetime = datetime.with_timezone(&Local); + + local_datetime.format("%Y-%m-%dT%H:%M:%S").to_string() +} + fn get_pretty_name( node: &DisplayNode, name_and_padding: String, @@ -469,6 +487,7 @@ mod tests { is_reversed: false, colors_on: false, by_filecount: false, + by_filetime: None, is_screen_reader: false, output_format: "".into(), bars_on_right: false, @@ -625,4 +644,37 @@ mod tests { let bar = dd.generate_bar(&n, 5); assert_eq!(bar, "████▓▓▓▓▓▓▓▓▓"); } + + #[test] + fn test_get_pretty_file_modified_time() { + // Create a timestamp for 2023-07-12 00:00:00 in local time + let local_dt = Local.with_ymd_and_hms(2023, 7, 12, 0, 0, 0).unwrap(); + let timestamp = local_dt.timestamp(); + + // Format expected output + let expected_output = local_dt.format("%Y-%m-%dT%H:%M:%S").to_string(); + + assert_eq!(get_pretty_file_modified_time(timestamp), expected_output); + + // Test another timestamp + let local_dt = Local.with_ymd_and_hms(2020, 1, 1, 12, 0, 0).unwrap(); + let timestamp = local_dt.timestamp(); + let expected_output = local_dt.format("%Y-%m-%dT%H:%M:%S").to_string(); + + assert_eq!(get_pretty_file_modified_time(timestamp), expected_output); + + // Test timestamp for epoch start (1970-01-01T00:00:00) + let local_dt = Local.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap(); + let timestamp = local_dt.timestamp(); + let expected_output = local_dt.format("%Y-%m-%dT%H:%M:%S").to_string(); + + assert_eq!(get_pretty_file_modified_time(timestamp), expected_output); + + // Test a future timestamp + let local_dt = Local.with_ymd_and_hms(2030, 12, 25, 6, 30, 0).unwrap(); + let timestamp = local_dt.timestamp(); + let expected_output = local_dt.format("%Y-%m-%dT%H:%M:%S").to_string(); + + assert_eq!(get_pretty_file_modified_time(timestamp), expected_output); + } } diff --git a/src/filter.rs b/src/filter.rs index 6aea2520..c81aad08 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -1,4 +1,5 @@ use crate::display_node::DisplayNode; +use crate::node::FileTime; use crate::node::Node; use std::collections::BinaryHeap; use std::collections::HashMap; @@ -14,7 +15,11 @@ pub struct AggregateData { pub using_a_filter: bool, } -pub fn get_biggest(top_level_nodes: Vec, display_data: AggregateData) -> Option { +pub fn get_biggest( + top_level_nodes: Vec, + display_data: AggregateData, + by_filetime: &Option, +) -> Option { if top_level_nodes.is_empty() { // perhaps change this, bring back Error object? return None; @@ -24,7 +29,15 @@ pub fn get_biggest(top_level_nodes: Vec, display_data: AggregateData) -> O let root; if number_top_level_nodes > 1 { - let size = top_level_nodes.iter().map(|node| node.size).sum(); + let size = if by_filetime.is_some() { + top_level_nodes + .iter() + .map(|node| node.size) + .max() + .unwrap_or(0) + } else { + top_level_nodes.iter().map(|node| node.size).sum() + }; root = Node { name: PathBuf::from("(total)"), size, diff --git a/src/filter_type.rs b/src/filter_type.rs index 974d9c3b..516ce25e 100644 --- a/src/filter_type.rs +++ b/src/filter_type.rs @@ -1,4 +1,5 @@ use crate::display_node::DisplayNode; +use crate::node::FileTime; use crate::node::Node; use std::collections::HashMap; use std::ffi::OsStr; @@ -10,7 +11,11 @@ struct ExtensionNode<'a> { extension: Option<&'a OsStr>, } -pub fn get_all_file_types(top_level_nodes: &[Node], n: usize) -> Option { +pub fn get_all_file_types( + top_level_nodes: &[Node], + n: usize, + by_filetime: &Option, +) -> Option { let ext_nodes = { let mut extension_cumulative_sizes = HashMap::new(); build_by_all_file_types(top_level_nodes, &mut extension_cumulative_sizes); @@ -44,16 +49,27 @@ pub fn get_all_file_types(top_level_nodes: &[Node], n: usize) -> Option 0 { + let actual_size = if by_filetime.is_some() { + ext_nodes_iter.map(|node| node.size).max().unwrap_or(0) + } else { + ext_nodes_iter.map(|node| node.size).sum() + }; displayed.push(DisplayNode { name: PathBuf::from("(others)"), - size: ext_nodes_iter.map(|node| node.size).sum(), + size: actual_size, children: vec![], }); } + let actual_size: u64 = if by_filetime.is_some() { + displayed.iter().map(|node| node.size).max().unwrap_or(0) + } else { + displayed.iter().map(|node| node.size).sum() + }; + let result = DisplayNode { name: PathBuf::from("(total)"), - size: displayed.iter().map(|node| node.size).sum(), + size: actual_size, children: displayed, }; diff --git a/src/main.rs b/src/main.rs index 62e70cc4..5c08e32a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -224,6 +224,7 @@ fn main() { .collect::>(); let by_filecount = options.get_flag("by_filecount"); + let by_filetime = config.get_filetime(&options); let limit_filesystem = options.get_flag("limit_filesystem"); let follow_links = options.get_flag("dereference_links"); @@ -248,7 +249,7 @@ fn main() { let filter_modified_time = config.get_modified_time_operator(&options); let filter_accessed_time = config.get_accessed_time_operator(&options); - let filter_changed_time = config.get_created_time_operator(&options); + let filter_changed_time = config.get_changed_time_operator(&options); let walk_data = WalkData { ignore_directories: ignored_full_path, @@ -260,6 +261,7 @@ fn main() { filter_changed_time, use_apparent_size: config.get_apparent_size(&options), by_filecount, + by_filetime: &by_filetime, ignore_hidden, follow_links, progress_data: indicator.data.clone(), @@ -272,7 +274,7 @@ fn main() { let top_level_nodes = walk_it(simplified_dirs, &walk_data); let tree = match summarize_file_types { - true => get_all_file_types(&top_level_nodes, number_of_lines), + true => get_all_file_types(&top_level_nodes, number_of_lines, &by_filetime), false => { let agg_data = AggregateData { min_size: config.get_min_size(&options), @@ -282,7 +284,7 @@ fn main() { depth, using_a_filter: !filter_regexs.is_empty() || !invert_filter_regexs.is_empty(), }; - get_biggest(top_level_nodes, agg_data) + get_biggest(top_level_nodes, agg_data, &by_filetime) } }; @@ -334,6 +336,7 @@ fn main() { is_reversed: !config.get_reverse(&options), colors_on: is_colors, by_filecount, + by_filetime, is_screen_reader: config.get_screen_reader(&options), output_format, bars_on_right: config.get_bars_on_right(&options), diff --git a/src/node.rs b/src/node.rs index 08b28f3a..00d70a3e 100644 --- a/src/node.rs +++ b/src/node.rs @@ -16,6 +16,13 @@ pub struct Node { pub depth: usize, } +#[derive(Debug, PartialEq)] +pub enum FileTime { + Modified, + Accessed, + Changed, +} + #[allow(clippy::too_many_arguments)] pub fn build_node( dir: PathBuf, @@ -27,6 +34,7 @@ pub fn build_node( ) -> Option { let use_apparent_size = walk_data.use_apparent_size; let by_filecount = walk_data.by_filecount; + let by_filetime = &walk_data.by_filetime; get_metadata(&dir, use_apparent_size).map(|data| { let inode_device = if is_symlink && !use_apparent_size { @@ -51,6 +59,13 @@ pub fn build_node( 0 } else if by_filecount { 1 + } else if by_filetime.is_some() { + match by_filetime { + Some(FileTime::Modified) => data.2 .0.unsigned_abs(), + Some(FileTime::Accessed) => data.2 .1.unsigned_abs(), + Some(FileTime::Changed) => data.2 .2.unsigned_abs(), + None => unreachable!(), + } } else { data.0 };