From 95e27b220ec536c3d5bbc3245ded6abe40d75db3 Mon Sep 17 00:00:00 2001 From: ericzakariasson Date: Wed, 25 Feb 2026 16:32:44 -0800 Subject: [PATCH 1/4] feat: add --ignore-ext and --only-ext flags for extension filtering Adds two new CLI options for filtering scan results by file extension: - --ignore-ext: exclude files with specified extensions - --only-ext: only include files with specified extensions Both accept comma-separated values (e.g. --ignore-ext dmg,iso,pkg). Integrates with large files, old files, and downloads scanners. Also supports configuration via config.toml fields. Made-with: Cursor --- src/cli.rs | 8 +++++++ src/config.rs | 49 ++++++++++++++++++++++++++++++++++++++ src/scanner/downloads.rs | 5 ++++ src/scanner/large_files.rs | 5 ++++ src/scanner/old_files.rs | 5 ++++ 5 files changed, 72 insertions(+) diff --git a/src/cli.rs b/src/cli.rs index 68aa2bb..73ee74d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -89,6 +89,14 @@ pub struct ScanOptions { #[arg(long, value_name = "PATTERN")] pub exclude: Vec, + /// Ignore files with these extensions (comma-separated, e.g. "dmg,iso,pkg") + #[arg(long, value_name = "EXTS", value_delimiter = ',')] + pub ignore_ext: Vec, + + /// Only include files with these extensions (comma-separated, e.g. "dmg,iso,zip") + #[arg(long, value_name = "EXTS", value_delimiter = ',')] + pub only_ext: Vec, + /// Output results as JSON #[arg(long)] pub json: bool, diff --git a/src/config.rs b/src/config.rs index 682b7ed..4276822 100644 --- a/src/config.rs +++ b/src/config.rs @@ -39,6 +39,14 @@ pub struct Config { #[serde(default)] pub custom_paths: Vec, + /// File extensions to ignore during scanning + #[serde(default)] + pub ignored_extensions: Vec, + + /// When set, only include files with these extensions + #[serde(default)] + pub only_extensions: Vec, + /// Base path for scanning (default: home directory) #[serde(skip)] pub base_path: Option, @@ -139,6 +147,8 @@ impl Default for Config { excluded_paths: Vec::new(), cache_paths: Vec::new(), custom_paths: Vec::new(), + ignored_extensions: Vec::new(), + only_extensions: Vec::new(), base_path: None, } } @@ -213,6 +223,22 @@ impl Config { self.excluded_paths.push(exclude.clone()); } } + + // Add CLI ignored extensions + for ext in &options.ignore_ext { + let ext_lower = ext.to_lowercase(); + if !self.ignored_extensions.contains(&ext_lower) { + self.ignored_extensions.push(ext_lower); + } + } + + // Add CLI only-ext filters + for ext in &options.only_ext { + let ext_lower = ext.to_lowercase(); + if !self.only_extensions.contains(&ext_lower) { + self.only_extensions.push(ext_lower); + } + } } /// Get the base path for scanning @@ -228,6 +254,29 @@ impl Config { self.min_large_size_mb * 1024 * 1024 } + /// Check if a file should be skipped based on extension filters. + /// Handles both --ignore-ext (exclusion) and --only-ext (inclusion) logic. + pub fn should_skip_extension(&self, path: &std::path::Path) -> bool { + if self.ignored_extensions.is_empty() && self.only_extensions.is_empty() { + return false; + } + + let ext = match path.extension() { + Some(e) => e.to_string_lossy().to_lowercase(), + None => return !self.only_extensions.is_empty(), + }; + + if self.ignored_extensions.iter().any(|ignored| ignored == &ext) { + return true; + } + + if !self.only_extensions.is_empty() { + return self.only_extensions.iter().any(|only| only == &ext); + } + + false + } + /// Check if a path should be excluded pub fn is_excluded(&self, path: &std::path::Path) -> bool { let path_str = path.to_string_lossy(); diff --git a/src/scanner/downloads.rs b/src/scanner/downloads.rs index da431a5..8a88ba0 100644 --- a/src/scanner/downloads.rs +++ b/src/scanner/downloads.rs @@ -60,6 +60,11 @@ impl Scanner for DownloadsScanner { continue; } + // Skip based on extension filters + if config.should_skip_extension(&path) { + continue; + } + // Skip hidden files if let Some(name) = path.file_name() { if name.to_string_lossy().starts_with('.') { diff --git a/src/scanner/large_files.rs b/src/scanner/large_files.rs index 6bf1263..b37bb2b 100644 --- a/src/scanner/large_files.rs +++ b/src/scanner/large_files.rs @@ -103,6 +103,11 @@ impl Scanner for LargeFilesScanner { continue; } + // Skip based on extension filters + if config.should_skip_extension(path) { + continue; + } + // Skip hidden files if let Some(name) = path.file_name() { if name.to_string_lossy().starts_with('.') { diff --git a/src/scanner/old_files.rs b/src/scanner/old_files.rs index 5028fee..dd54b19 100644 --- a/src/scanner/old_files.rs +++ b/src/scanner/old_files.rs @@ -115,6 +115,11 @@ impl Scanner for OldFilesScanner { continue; } + // Skip based on extension filters + if config.should_skip_extension(path) { + continue; + } + // Skip hidden files if let Some(name) = path.file_name() { if name.to_string_lossy().starts_with('.') { From 6ef0f8e965ec20435552a271995f3516c79f33ef Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 26 Feb 2026 01:12:46 +0000 Subject: [PATCH 2/4] Fix extension filtering behavior Applied via @cursor push command --- src/config.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/config.rs b/src/config.rs index 4276822..3ae7448 100644 --- a/src/config.rs +++ b/src/config.rs @@ -174,9 +174,18 @@ impl Config { let contents = fs::read_to_string(&config_path) .with_context(|| format!("Failed to read config file: {}", config_path.display()))?; - let config: Config = toml::from_str(&contents) + let mut config: Config = toml::from_str(&contents) .with_context(|| format!("Failed to parse config file: {}", config_path.display()))?; + config + .ignored_extensions + .iter_mut() + .for_each(|ext| *ext = ext.to_lowercase()); + config + .only_extensions + .iter_mut() + .for_each(|ext| *ext = ext.to_lowercase()); + Ok(config) } @@ -271,7 +280,7 @@ impl Config { } if !self.only_extensions.is_empty() { - return self.only_extensions.iter().any(|only| only == &ext); + return !self.only_extensions.iter().any(|only| only == &ext); } false From 82eefef00f30b0112275575cac5bc526e4e4203b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 26 Feb 2026 01:46:10 +0000 Subject: [PATCH 3/4] Fix extension filtering edge cases Applied via @cursor push command --- src/config.rs | 12 ++++++++---- src/scanner/downloads.rs | 4 ++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/config.rs b/src/config.rs index 3ae7448..ba202ea 100644 --- a/src/config.rs +++ b/src/config.rs @@ -180,11 +180,11 @@ impl Config { config .ignored_extensions .iter_mut() - .for_each(|ext| *ext = ext.to_lowercase()); + .for_each(|ext| *ext = normalize_extension(ext)); config .only_extensions .iter_mut() - .for_each(|ext| *ext = ext.to_lowercase()); + .for_each(|ext| *ext = normalize_extension(ext)); Ok(config) } @@ -235,7 +235,7 @@ impl Config { // Add CLI ignored extensions for ext in &options.ignore_ext { - let ext_lower = ext.to_lowercase(); + let ext_lower = normalize_extension(ext); if !self.ignored_extensions.contains(&ext_lower) { self.ignored_extensions.push(ext_lower); } @@ -243,7 +243,7 @@ impl Config { // Add CLI only-ext filters for ext in &options.only_ext { - let ext_lower = ext.to_lowercase(); + let ext_lower = normalize_extension(ext); if !self.only_extensions.contains(&ext_lower) { self.only_extensions.push(ext_lower); } @@ -347,6 +347,10 @@ fn parse_size_mb(s: &str) -> Option { s.parse::().ok() } +fn normalize_extension(ext: &str) -> String { + ext.trim_start_matches('.').to_lowercase() +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/scanner/downloads.rs b/src/scanner/downloads.rs index 8a88ba0..65657ad 100644 --- a/src/scanner/downloads.rs +++ b/src/scanner/downloads.rs @@ -60,8 +60,8 @@ impl Scanner for DownloadsScanner { continue; } - // Skip based on extension filters - if config.should_skip_extension(&path) { + // Extension filters only apply to files. + if entry.file_type().is_file() && config.should_skip_extension(&path) { continue; } From 150f89e2fb33f3bd67329b23c1eaa8374c24efa3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 26 Feb 2026 02:54:06 +0000 Subject: [PATCH 4/4] Include extension filters in scan cache fingerprint Applied via @cursor push command --- src/scan_cache.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/scan_cache.rs b/src/scan_cache.rs index 2e932f7..dddfca7 100644 --- a/src/scan_cache.rs +++ b/src/scan_cache.rs @@ -26,8 +26,12 @@ fn options_fingerprint(options: &ScanOptions) -> String { .unwrap_or_default(); let mut exclude = options.exclude.clone(); exclude.sort(); + let mut ignore_ext = options.ignore_ext.clone(); + ignore_ext.sort(); + let mut only_ext = options.only_ext.clone(); + only_ext.sort(); format!( - "path={} all={} cache={} trash={} temp={} downloads={} build={} large={} duplicates={} old={} min_age={:?} min_size={:?} project_age={:?} exclude={:?}", + "path={} all={} cache={} trash={} temp={} downloads={} build={} large={} duplicates={} old={} min_age={:?} min_size={:?} project_age={:?} exclude={:?} ignore_ext={:?} only_ext={:?}", path, options.all, options.cache, @@ -42,6 +46,8 @@ fn options_fingerprint(options: &ScanOptions) -> String { options.min_size, options.project_age, exclude, + ignore_ext, + only_ext, ) }