diff --git a/README.md b/README.md index e0f5612a..f2011130 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ Usage: dust -i (Do not show hidden files) Usage: dust -c (No colors [monochrome]) Usage: dust -f (Count files instead of diskspace) Usage: dust -t (Group by filetype) +Usage: dust -z 10M (min-size, Only include files larger than 10M) Usage: dust -e regex (Only include files matching this regex (eg dust -e "\.png$" would match png files)) Usage: dust -v regex (Exculde files matching this regex (eg dust -v "\.png$" would ignore png files)) diff --git a/completions/_dust b/completions/_dust index b9d50af5..2391d2df 100644 --- a/completions/_dust +++ b/completions/_dust @@ -21,6 +21,8 @@ _dust() { '--number-of-lines=[Number of lines of output to show. (Default is terminal_height - 10)]: : ' \ '*-X+[Exclude any file or directory with this name]: : ' \ '*--ignore-directory=[Exclude any file or directory with this name]: : ' \ +'-z+[Minimum size file to include in output]: : ' \ +'--min-size=[Minimum size file to include in output]: : ' \ '(-e --filter -t --file_types)*-v+[Exclude filepaths matching this regex. To ignore png files type: -v "\\.png$" ]: : ' \ '(-e --filter -t --file_types)*--invert-filter=[Exclude filepaths matching this regex. To ignore png files type: -v "\\.png$" ]: : ' \ '(-t --file_types)*-e+[Only include filepaths matching this regex. For png files type: -e "\\.png$" ]: : ' \ diff --git a/completions/_dust.ps1 b/completions/_dust.ps1 index e08646e9..18e30376 100644 --- a/completions/_dust.ps1 +++ b/completions/_dust.ps1 @@ -27,6 +27,8 @@ Register-ArgumentCompleter -Native -CommandName 'dust' -ScriptBlock { [CompletionResult]::new('--number-of-lines', 'number-of-lines', [CompletionResultType]::ParameterName, 'Number of lines of output to show. (Default is terminal_height - 10)') [CompletionResult]::new('-X', 'X', [CompletionResultType]::ParameterName, 'Exclude any file or directory with this name') [CompletionResult]::new('--ignore-directory', 'ignore-directory', [CompletionResultType]::ParameterName, 'Exclude any file or directory with this name') + [CompletionResult]::new('-z', 'z', [CompletionResultType]::ParameterName, 'Minimum size file to include in output') + [CompletionResult]::new('--min-size', 'min-size', [CompletionResultType]::ParameterName, 'Minimum size file to include in output') [CompletionResult]::new('-v', 'v', [CompletionResultType]::ParameterName, 'Exclude filepaths matching this regex. To ignore png files type: -v "\.png$" ') [CompletionResult]::new('--invert-filter', 'invert-filter', [CompletionResultType]::ParameterName, 'Exclude filepaths matching this regex. To ignore png files type: -v "\.png$" ') [CompletionResult]::new('-e', 'e', [CompletionResultType]::ParameterName, 'Only include filepaths matching this regex. For png files type: -e "\.png$" ') diff --git a/completions/dust.bash b/completions/dust.bash index aabf8ec8..f261db5e 100644 --- a/completions/dust.bash +++ b/completions/dust.bash @@ -19,7 +19,7 @@ _dust() { case "${cmd}" in dust) - opts="-h -V -d -n -p -X -x -s -r -c -b -f -i -v -e -t -w -H --help --version --depth --number-of-lines --full-paths --ignore-directory --limit-filesystem --apparent-size --reverse --no-colors --no-percent-bars --skip-total --filecount --ignore_hidden --invert-filter --filter --file_types --terminal_width --si ..." + opts="-h -V -d -n -p -X -x -s -r -c -b -z -f -i -v -e -t -w -H --help --version --depth --number-of-lines --full-paths --ignore-directory --limit-filesystem --apparent-size --reverse --no-colors --no-percent-bars --min-size --skip-total --filecount --ignore_hidden --invert-filter --filter --file_types --terminal_width --si ..." if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -49,6 +49,14 @@ _dust() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; + --min-size) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -z) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; --invert-filter) COMPREPLY=($(compgen -f "${cur}")) return 0 diff --git a/completions/dust.elv b/completions/dust.elv index 4ef4edea..e0ddc2ac 100644 --- a/completions/dust.elv +++ b/completions/dust.elv @@ -24,6 +24,8 @@ set edit:completion:arg-completer[dust] = {|@words| cand --number-of-lines 'Number of lines of output to show. (Default is terminal_height - 10)' cand -X 'Exclude any file or directory with this name' cand --ignore-directory 'Exclude any file or directory with this name' + cand -z 'Minimum size file to include in output' + cand --min-size 'Minimum size file to include in output' cand -v 'Exclude filepaths matching this regex. To ignore png files type: -v "\.png$" ' cand --invert-filter 'Exclude filepaths matching this regex. To ignore png files type: -v "\.png$" ' cand -e 'Only include filepaths matching this regex. For png files type: -e "\.png$" ' diff --git a/completions/dust.fish b/completions/dust.fish index a3c5e9fe..1c2ccb3d 100644 --- a/completions/dust.fish +++ b/completions/dust.fish @@ -1,6 +1,7 @@ complete -c dust -s d -l depth -d 'Depth to show' -r complete -c dust -s n -l number-of-lines -d 'Number of lines of output to show. (Default is terminal_height - 10)' -r complete -c dust -s X -l ignore-directory -d 'Exclude any file or directory with this name' -r +complete -c dust -s z -l min-size -d 'Minimum size file to include in output' -r complete -c dust -s v -l invert-filter -d 'Exclude filepaths matching this regex. To ignore png files type: -v "\\.png$" ' -r complete -c dust -s e -l filter -d 'Only include filepaths matching this regex. For png files type: -e "\\.png$" ' -r complete -c dust -s w -l terminal_width -d 'Specify width of output overriding the auto detection of terminal width' -r diff --git a/src/cli.rs b/src/cli.rs index 08d67287..b4b46b99 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -64,6 +64,14 @@ pub fn build_cli() -> Command<'static> { .long("no-percent-bars") .help("No percent bars or percentages will be displayed"), ) + .arg( + Arg::new("min_size") + .short('z') + .long("min-size") + .takes_value(true) + .number_of_values(1) + .help("Minimum size file to include in output"), + ) .arg( Arg::new("skip_total") .long("skip-total") diff --git a/src/config.rs b/src/config.rs index 25e64bb6..80c22165 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,6 +4,8 @@ use serde::Deserialize; use std::path::Path; use std::path::PathBuf; +use crate::display::UNITS; + #[derive(Deserialize, Default)] #[serde(rename_all = "kebab-case")] #[serde(deny_unknown_fields)] @@ -16,6 +18,7 @@ pub struct Config { pub skip_total: Option, pub ignore_hidden: Option, pub iso: Option, + pub min_size: Option, } impl Config { @@ -43,6 +46,54 @@ impl Config { pub fn get_skip_total(&self, options: &ArgMatches) -> bool { Some(true) == self.skip_total || options.is_present("skip_total") } + pub fn get_min_size(&self, options: &ArgMatches, iso: bool) -> Option { + let size_from_param = options.value_of("min_size"); + self._get_min_size(size_from_param, iso) + } + fn _get_min_size(&self, min_size: Option<&str>, iso: bool) -> Option { + let size_from_param = min_size.and_then(|a| convert_min_size(a, iso)); + + if size_from_param.is_none() { + self.min_size + .as_ref() + .and_then(|a| convert_min_size(a.as_ref(), iso)) + } else { + size_from_param + } + } +} + +fn convert_min_size(input: &str, iso: bool) -> Option { + let chars_as_vec: Vec = input.chars().collect(); + match chars_as_vec.split_last() { + Some((last, start)) => { + let mut starts: String = start.iter().collect::(); + + for (i, u) in UNITS.iter().rev().enumerate() { + if Some(*u) == last.to_uppercase().next() { + return match starts.parse::() { + Ok(pure) => { + let num: usize = if iso { 1000 } else { 1024 }; + let marker = pure * num.pow((i + 1) as u32); + Some(marker) + } + Err(_) => { + eprintln!("Ignoring invalid min-size: {}", input); + None + } + }; + } + } + starts.push(*last); + starts + .parse() + .map_err(|_| { + eprintln!("Ignoring invalid min-size: {}", input); + }) + .ok() + } + None => None, + } } fn get_config_locations(base: &Path) -> Vec { @@ -66,3 +117,32 @@ pub fn get_config() -> Config { ..Default::default() } } + +mod tests { + #[allow(unused_imports)] + use super::*; + + #[test] + fn test_conversion() { + assert_eq!(convert_min_size("55", false), Some(55)); + assert_eq!(convert_min_size("12344321", false), Some(12344321)); + assert_eq!(convert_min_size("95RUBBISH", false), None); + assert_eq!(convert_min_size("10K", false), Some(10 * 1024)); + assert_eq!(convert_min_size("10M", false), Some(10 * 1024usize.pow(2))); + assert_eq!(convert_min_size("10M", true), Some(10 * 1000usize.pow(2))); + assert_eq!(convert_min_size("2G", false), Some(2 * 1024usize.pow(3))); + } + + #[test] + fn test_min_size_from_config_applied_or_overridden() { + let c = Config { + min_size: Some("1K".to_owned()), + ..Default::default() + }; + assert_eq!(c._get_min_size(None, false), Some(1024)); + assert_eq!(c._get_min_size(Some("2K"), false), Some(2048)); + + assert_eq!(c._get_min_size(None, true), Some(1000)); + assert_eq!(c._get_min_size(Some("2K"), true), Some(2000)); + } +} diff --git a/src/display.rs b/src/display.rs index 233d4fee..332f0b81 100644 --- a/src/display.rs +++ b/src/display.rs @@ -14,7 +14,7 @@ use std::iter::repeat; use std::path::Path; use thousands::Separable; -static UNITS: [char; 4] = ['T', 'G', 'M', 'K']; +pub static UNITS: [char; 4] = ['T', 'G', 'M', 'K']; static BLOCKS: [char; 5] = ['█', '▓', '▒', '░', ' ']; pub struct DisplayData { diff --git a/src/filter.rs b/src/filter.rs index ed4be0a0..8f1d7b3e 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -7,6 +7,7 @@ use std::path::PathBuf; pub fn get_biggest( top_level_nodes: Vec, + min_size: Option, n: usize, depth: usize, using_a_filter: bool, @@ -22,14 +23,14 @@ pub fn get_biggest( let mut allowed_nodes = HashSet::new(); allowed_nodes.insert(root.name.as_path()); - heap = add_children(using_a_filter, &root, depth, heap); + heap = add_children(using_a_filter, min_size, &root, depth, heap); for _ in number_top_level_nodes..n { let line = heap.pop(); match line { Some(line) => { allowed_nodes.insert(line.name.as_path()); - heap = add_children(using_a_filter, line, depth, heap); + heap = add_children(using_a_filter, min_size, line, depth, heap); } None => break, } @@ -39,17 +40,16 @@ pub fn get_biggest( fn add_children<'a>( using_a_filter: bool, + min_size: Option, file_or_folder: &'a Node, depth: usize, mut heap: BinaryHeap<&'a Node>, ) -> BinaryHeap<&'a Node> { if depth > file_or_folder.depth { - heap.extend( - file_or_folder - .children - .iter() - .filter(|c| !using_a_filter || c.name.is_file() || c.size > 0), - ) + heap.extend(file_or_folder.children.iter().filter(|c| match min_size { + Some(ms) => c.size > ms as u64, + None => !using_a_filter || c.name.is_file() || c.size > 0, + })) } heap } diff --git a/src/main.rs b/src/main.rs index cc9df046..f020f1ce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -91,7 +91,6 @@ fn get_regex_value(maybe_value: Option) -> Vec { fn main() { let options = build_cli().get_matches(); - let config = get_config(); let target_dirs = options @@ -161,12 +160,14 @@ fn main() { .build_global() .unwrap(); + let iso = config.get_iso(&options); let (top_level_nodes, has_errors) = walk_it(simplified_dirs, walk_data); let tree = match summarize_file_types { true => get_all_file_types(&top_level_nodes, number_of_lines), false => get_biggest( top_level_nodes, + config.get_min_size(&options, iso), number_of_lines, depth, options.values_of("filter").is_some() || options.value_of("invert_filter").is_some(), @@ -185,7 +186,7 @@ fn main() { terminal_width, by_filecount, &root_node, - config.get_iso(&options), + iso, config.get_skip_total(&options), ) }