diff --git a/Cargo.lock b/Cargo.lock index 7644bb767..b01843776 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5383,6 +5383,7 @@ dependencies = [ "anyhow", "crossterm 0.29.0", "futures", + "globset", "hashbrown", "libc", "mlua", diff --git a/yazi-actor/Cargo.toml b/yazi-actor/Cargo.toml index 5e8ea2b82..7f942d454 100644 --- a/yazi-actor/Cargo.toml +++ b/yazi-actor/Cargo.toml @@ -37,6 +37,7 @@ yazi-widgets = { path = "../yazi-widgets", version = "26.1.4" } anyhow = { workspace = true } crossterm = { workspace = true } futures = { workspace = true } +globset = { workspace = true } hashbrown = { workspace = true } mlua = { workspace = true } paste = { workspace = true } diff --git a/yazi-actor/src/mgr/cd.rs b/yazi-actor/src/mgr/cd.rs index 1e0a62873..2e7fe80a0 100644 --- a/yazi-actor/src/mgr/cd.rs +++ b/yazi-actor/src/mgr/cd.rs @@ -38,19 +38,43 @@ impl Actor for Cd { } // Current - let rep = tab.history.remove_or(&opt.target); + let mut rep = tab.history.remove_or(&opt.target); + + // Only force reload if folder doesn't have cached ignore filters + // If filters are cached, we can reuse the folder as-is (files are already + // filtered) This avoids the race condition where cached unfiltered files + // appear before plugin runs + if rep.files.ignore_filter().is_none() { + rep.cha = Default::default(); + rep.files.update_ioerr(); + rep.stage = Default::default(); + } let rep = mem::replace(&mut tab.current, rep); tab.history.insert(rep.url.clone(), rep); // Parent if let Some(parent) = opt.target.parent() { - tab.parent = Some(tab.history.remove_or(parent)); + let mut parent_folder = tab.history.remove_or(parent); + // Only force parent reload if it doesn't have cached filters + if parent_folder.files.ignore_filter().is_none() { + parent_folder.cha = Default::default(); + parent_folder.files.update_ioerr(); + parent_folder.stage = Default::default(); + } + tab.parent = Some(parent_folder); } - err!(Pubsub::pub_after_cd(tab.id, tab.cwd())); act!(mgr:displace, cx)?; act!(mgr:hidden, cx)?; act!(mgr:sort, cx).ok(); + + // Apply config excludes if no plugin patterns are set + // This ensures config patterns work when gitignore plugin is disabled + // When plugins are enabled, they handle merging via exclude_add + if cx.tab().current.files.ignore_filter().is_none() { + act!(mgr:ignore, cx)?; + } + act!(mgr:hover, cx)?; act!(mgr:refresh, cx)?; act!(mgr:stash, cx, opt).ok(); diff --git a/yazi-actor/src/mgr/exclude_add.rs b/yazi-actor/src/mgr/exclude_add.rs new file mode 100644 index 000000000..6e980869b --- /dev/null +++ b/yazi-actor/src/mgr/exclude_add.rs @@ -0,0 +1,353 @@ +use std::sync::Arc; + +use anyhow::Result; +use globset::{Glob, GlobSetBuilder}; +use yazi_config::YAZI; +use yazi_core::tab::Folder; +use yazi_fs::{FolderStage, IgnoreFilter}; +use yazi_macro::{act, render, render_and, succ}; +use yazi_parser::mgr::ExcludeAddOpt; +use yazi_shared::{data::Data, url::UrlLike}; + +use crate::{Actor, Ctx}; + +pub struct ExcludeAdd; + +impl Actor for ExcludeAdd { + type Options = ExcludeAddOpt; + + const NAME: &str = "exclude_add"; + + fn act(cx: &mut Ctx, opt: Self::Options) -> Result { + if opt.patterns.is_empty() { + succ!(); + } + + // Get the appropriate context string for matching exclude rules + let cwd = cx.cwd(); + let cwd_str = if cwd.is_search() { + "search://**".to_string() + } else { + cwd.loc().as_os().ok().map(|p| p.display().to_string()).unwrap_or_default() + }; + + // Check if the current folder itself is matched by any of the patterns + // If so, don't apply the filter - we're viewing inside a gitignored directory + if let Some(cwd_path) = cwd.loc().as_os().ok() { + // Build a quick GlobSet to test if current folder matches any pattern + let mut test_builder = GlobSetBuilder::new(); + for pattern in &opt.patterns { + if pattern.starts_with('!') { + // Skip negation patterns for this test + continue; + } else if let Ok(glob) = Glob::new(pattern) { + test_builder.add(glob); + } + } + + // Check if CWD matches any ignore pattern + if let Ok(test_set) = test_builder.build() { + // Test both absolute path and relative components + let mut matches_ignore = test_set.is_match(cwd_path); + + // Also check path components for patterns like "target" matching + // "/path/to/target" + if !matches_ignore { + if let Some(components) = cwd_path.to_str() { + for (i, _) in components.match_indices('/').skip(1) { + if let Some(subpath) = components.get(i + 1..) { + if test_set.is_match(subpath) { + matches_ignore = true; + break; + } + } + } + } + } + + // If we're inside an excluded directory, don't apply any filters + // This allows viewing the contents of gitignored directories + if matches_ignore { + succ!(); + } + } + } + + // Get existing patterns from config + let config_patterns = YAZI.files.excludes_for_context(&cwd_str); + + // Merge plugin patterns with config patterns + // Config patterns come last so they can override plugin patterns with negation + let mut all_patterns = opt.patterns.clone(); + all_patterns.extend(config_patterns); + + // Compile glob patterns + let mut ignores_builder = GlobSetBuilder::new(); + let mut whitelists_builder = GlobSetBuilder::new(); + + for pattern in &all_patterns { + if let Some(negated) = pattern.strip_prefix('!') { + // Negation pattern - this is a whitelist + if let Ok(glob) = Glob::new(negated) { + whitelists_builder.add(glob); + } + } else { + // Regular ignore pattern + if let Ok(glob) = Glob::new(pattern) { + ignores_builder.add(glob); + } + } + } + + let ignores = ignores_builder.build().ok(); + let whitelists = whitelists_builder.build().ok(); + + // Create glob matcher function for compiled patterns + let glob_matcher: Option Option + Send + Sync>> = + if ignores.is_some() || whitelists.is_some() { + let context = cwd_str.clone(); + Some(Arc::new(move |path: &std::path::Path| { + // First check config patterns (for user overrides/negation) + if let Some(result) = YAZI.files.matches_path(path, &context) { + return Some(result); + } + + // For absolute paths, try both the full path and relative components + let paths_to_check: Vec<&std::path::Path> = if path.is_absolute() { + let mut paths = vec![path]; + if let Some(components) = path.to_str() { + for (i, _) in components.match_indices('/').skip(1) { + if let Some(subpath) = components.get(i + 1..) { + paths.push(std::path::Path::new(subpath)); + } + } + } + paths + } else { + vec![path] + }; + + // Check whitelist first (negation takes precedence) + if let Some(ref wl) = whitelists { + for p in &paths_to_check { + if wl.is_match(p) { + return Some(false); // Explicitly NOT ignored + } + } + } + + // Check ignore patterns + if let Some(ref ig) = ignores { + for p in &paths_to_check { + if ig.is_match(p) { + return Some(true); // Should be ignored + } + } + } + + None + })) + } else { + None + }; + + // Load ignore filter with merged patterns + let ignore_filter = IgnoreFilter::from_patterns(glob_matcher.clone()); + + let hovered = cx.hovered().map(|f| f.urn().to_owned()); + let apply = |f: &mut Folder, filter: Option| { + let changed = f.files.set_ignore_filter(filter); + if f.stage == FolderStage::Loading { + render!(); + false + } else { + render_and!(changed && f.files.catchup_revision()) + } + }; + + // Apply to CWD and parent + let cwd_changed = apply(cx.current_mut(), ignore_filter.clone()); + + let parent_changed = if let Some(p) = cx.parent_mut() { + let parent_str = if p.url.is_search() { + "search://**".to_string() + } else { + p.url.loc().as_os().ok().map(|p| p.display().to_string()).unwrap_or_default() + }; + + let parent_config_patterns = YAZI.files.excludes_for_context(&parent_str); + let mut parent_all_patterns = opt.patterns.clone(); + parent_all_patterns.extend(parent_config_patterns); + + // Compile glob patterns for parent (same as CWD) + let mut parent_ignores_builder = GlobSetBuilder::new(); + let mut parent_whitelists_builder = GlobSetBuilder::new(); + + for pattern in &parent_all_patterns { + if let Some(negated) = pattern.strip_prefix('!') { + if let Ok(glob) = Glob::new(negated) { + parent_whitelists_builder.add(glob); + } + } else if let Ok(glob) = Glob::new(pattern) { + parent_ignores_builder.add(glob); + } + } + + let parent_ignores = parent_ignores_builder.build().ok(); + let parent_whitelists = parent_whitelists_builder.build().ok(); + + let parent_matcher: Option Option + Send + Sync>> = + if parent_ignores.is_some() || parent_whitelists.is_some() { + let context = parent_str.clone(); + Some(Arc::new(move |path: &std::path::Path| { + // First check config patterns (for user overrides/negation) + if let Some(result) = YAZI.files.matches_path(path, &context) { + return Some(result); + } + + // For absolute paths, try both the full path and relative components + let paths_to_check: Vec<&std::path::Path> = if path.is_absolute() { + let mut paths = vec![path]; + if let Some(components) = path.to_str() { + for (i, _) in components.match_indices('/').skip(1) { + if let Some(subpath) = components.get(i + 1..) { + paths.push(std::path::Path::new(subpath)); + } + } + } + paths + } else { + vec![path] + }; + + // Check whitelist first (negation takes precedence) + if let Some(ref wl) = parent_whitelists { + for p in &paths_to_check { + if wl.is_match(p) { + return Some(false); // Explicitly NOT ignored + } + } + } + + // Check ignore patterns + if let Some(ref ig) = parent_ignores { + for p in &paths_to_check { + if ig.is_match(p) { + return Some(true); // Should be ignored + } + } + } + + None + })) + } else { + None + }; + + let parent_filter = IgnoreFilter::from_patterns(parent_matcher); + apply(p, parent_filter) + } else { + false + }; + + // Always reposition parent cursor on CWD, even if folders are still loading + // The hover action ensures parent panel tracks the current working directory + if parent_changed || cx.parent().is_some() { + act!(mgr:hover, cx)?; + } + + if cwd_changed || parent_changed { + act!(mgr:update_paged, cx)?; + } + + // Apply to hovered folder + if let Some(h) = cx.hovered_folder_mut() { + let hovered_str = if h.url.is_search() { + "search://**".to_string() + } else { + h.url.loc().as_os().ok().map(|p| p.display().to_string()).unwrap_or_default() + }; + + let hovered_config_patterns = YAZI.files.excludes_for_context(&hovered_str); + let mut hovered_all_patterns = opt.patterns; + hovered_all_patterns.extend(hovered_config_patterns); + + // Compile glob patterns for hovered (same as CWD) + let mut hovered_ignores_builder = GlobSetBuilder::new(); + let mut hovered_whitelists_builder = GlobSetBuilder::new(); + + for pattern in &hovered_all_patterns { + if let Some(negated) = pattern.strip_prefix('!') { + if let Ok(glob) = Glob::new(negated) { + hovered_whitelists_builder.add(glob); + } + } else if let Ok(glob) = Glob::new(pattern) { + hovered_ignores_builder.add(glob); + } + } + + let hovered_ignores = hovered_ignores_builder.build().ok(); + let hovered_whitelists = hovered_whitelists_builder.build().ok(); + + let hovered_matcher: Option Option + Send + Sync>> = + if hovered_ignores.is_some() || hovered_whitelists.is_some() { + let context = hovered_str.clone(); + Some(Arc::new(move |path: &std::path::Path| { + // First check config patterns (for user overrides/negation) + if let Some(result) = YAZI.files.matches_path(path, &context) { + return Some(result); + } + + // For absolute paths, try both the full path and relative components + let paths_to_check: Vec<&std::path::Path> = if path.is_absolute() { + let mut paths = vec![path]; + if let Some(components) = path.to_str() { + for (i, _) in components.match_indices('/').skip(1) { + if let Some(subpath) = components.get(i + 1..) { + paths.push(std::path::Path::new(subpath)); + } + } + } + paths + } else { + vec![path] + }; + + // Check whitelist first (negation takes precedence) + if let Some(ref wl) = hovered_whitelists { + for p in &paths_to_check { + if wl.is_match(p) { + return Some(false); // Explicitly NOT ignored + } + } + } + + // Check ignore patterns + if let Some(ref ig) = hovered_ignores { + for p in &paths_to_check { + if ig.is_match(p) { + return Some(true); // Should be ignored + } + } + } + + None + })) + } else { + None + }; + + let hovered_filter = IgnoreFilter::from_patterns(hovered_matcher); + + if apply(h, hovered_filter) { + render!(h.repos(None)); + act!(mgr:peek, cx, true)?; + } else if cx.hovered().map(|f| f.urn()) != hovered.as_ref().map(Into::into) { + act!(mgr:peek, cx)?; + act!(mgr:watch, cx)?; + } + } + + succ!(); + } +} diff --git a/yazi-actor/src/mgr/excluded.rs b/yazi-actor/src/mgr/excluded.rs new file mode 100644 index 000000000..2000ac32d --- /dev/null +++ b/yazi-actor/src/mgr/excluded.rs @@ -0,0 +1,53 @@ +use anyhow::Result; +use yazi_core::tab::Folder; +use yazi_fs::FolderStage; +use yazi_macro::{act, render, render_and, succ}; +use yazi_parser::mgr::ExcludedOpt; +use yazi_shared::data::Data; + +use crate::{Actor, Ctx}; + +pub struct Excluded; + +impl Actor for Excluded { + type Options = ExcludedOpt; + + const NAME: &str = "excluded"; + + fn act(cx: &mut Ctx, opt: Self::Options) -> Result { + let current_state = cx.tab().current.files.show_excluded(); + let state = opt.state.bool(current_state); + + let hovered = cx.hovered().map(|f| f.urn().to_owned()); + let apply = |f: &mut Folder| { + if f.stage == FolderStage::Loading { + render!(); + false + } else { + f.files.set_show_excluded(state); + render_and!(f.files.catchup_revision()) + } + }; + + // Apply to CWD and parent + if let (a, Some(b)) = (apply(cx.current_mut()), cx.parent_mut().map(apply)) + && (a | b) + { + act!(mgr:hover, cx)?; + act!(mgr:update_paged, cx)?; + } + + // Apply to hovered + if let Some(h) = cx.hovered_folder_mut() + && apply(h) + { + render!(h.repos(None)); + act!(mgr:peek, cx, true)?; + } else if cx.hovered().map(|f| f.urn()) != hovered.as_ref().map(Into::into) { + act!(mgr:peek, cx)?; + act!(mgr:watch, cx)?; + } + + succ!() + } +} diff --git a/yazi-actor/src/mgr/ignore.rs b/yazi-actor/src/mgr/ignore.rs new file mode 100644 index 000000000..5c2ca0d3b --- /dev/null +++ b/yazi-actor/src/mgr/ignore.rs @@ -0,0 +1,135 @@ +use std::sync::Arc; + +use anyhow::Result; +use yazi_config::YAZI; +use yazi_core::tab::Folder; +use yazi_fs::{FolderStage, IgnoreFilter}; +use yazi_macro::{act, render, render_and, succ}; +use yazi_parser::VoidOpt; +use yazi_shared::{data::Data, url::UrlLike}; + +use crate::{Actor, Ctx}; + +pub struct Ignore; + +impl Actor for Ignore { + type Options = VoidOpt; + + const NAME: &str = "ignore"; + + fn act(cx: &mut Ctx, _: Self::Options) -> Result { + // Get the appropriate context string for matching exclude rules + // For search directories, use "search://**" as the context + let cwd = cx.cwd(); + let cwd_str = if cwd.is_search() { + "search://**".to_string() + } else { + cwd.loc().as_os().ok().map(|p| p.display().to_string()).unwrap_or_default() + }; + + let exclude_patterns = YAZI.files.excludes_for_context(&cwd_str); + + // Check if we're inside an excluded directory + // If so, don't apply filters to allow viewing excluded directory contents + if let Some(cwd_path) = cwd.loc().as_os().ok() { + // Quick test: does the CWD match any exclude pattern? + for pattern in &exclude_patterns { + if pattern.starts_with('!') { + continue; // Skip negation patterns + } + + // Simple pattern matching for common cases like ".git", "target", etc. + if let Some(name) = cwd_path.file_name().and_then(|n| n.to_str()) { + // Remove glob wildcards for comparison + let clean_pattern = pattern.trim_end_matches("/**").trim_start_matches("**/"); + if name == clean_pattern || pattern == name { + // We're inside an excluded directory, skip applying filters + succ!(); + } + } + } + } + + // Create glob matcher function for compiled patterns + let glob_matcher: Option Option + Send + Sync>> = + if !exclude_patterns.is_empty() { + let context = cwd_str.clone(); + Some(Arc::new(move |path: &std::path::Path| YAZI.files.matches_path(path, &context))) + } else { + None + }; + + // Load ignore filter from exclude patterns + let ignore_filter = IgnoreFilter::from_patterns(glob_matcher.clone()); + + let hovered = cx.hovered().map(|f| f.urn().to_owned()); + let apply = |f: &mut Folder, filter: Option| { + // Always set the filter, even when loading + let changed = f.files.set_ignore_filter(filter); + if f.stage == FolderStage::Loading { + render!(); + false + } else { + render_and!(changed && f.files.catchup_revision()) + } + }; + + // Apply to CWD and parent + let cwd_changed = apply(cx.current_mut(), ignore_filter.clone()); + + let parent_changed = if let Some(p) = cx.parent_mut() { + let parent_str = if p.url.is_search() { + "search://**".to_string() + } else { + p.url.loc().as_os().ok().map(|p| p.display().to_string()).unwrap_or_default() + }; + + let parent_excludes = YAZI.files.excludes_for_context(&parent_str); + let parent_filter = if !parent_excludes.is_empty() { + let context = parent_str.clone(); + let matcher: Option Option + Send + Sync>> = + Some(Arc::new(move |path: &std::path::Path| YAZI.files.matches_path(path, &context))); + IgnoreFilter::from_patterns(matcher) + } else { + IgnoreFilter::from_patterns(None) + }; + + apply(p, parent_filter) + } else { + false + }; + + if cwd_changed || parent_changed { + act!(mgr:hover, cx)?; + act!(mgr:update_paged, cx)?; + } + + // Apply to hovered + if let Some(h) = cx.hovered_folder_mut() { + let hovered_str = if h.url.is_search() { + "search://**".to_string() + } else { + h.url.loc().as_os().ok().map(|p| p.display().to_string()).unwrap_or_default() + }; + let hovered_excludes = YAZI.files.excludes_for_context(&hovered_str); + let hovered_matcher: Option Option + Send + Sync>> = + if !hovered_excludes.is_empty() { + let context = hovered_str.clone(); + Some(Arc::new(move |path: &std::path::Path| YAZI.files.matches_path(path, &context))) + } else { + None + }; + let hovered_filter = IgnoreFilter::from_patterns(hovered_matcher); + + if apply(h, hovered_filter) { + render!(h.repos(None)); + act!(mgr:peek, cx, true)?; + } else if cx.hovered().map(|f| f.urn()) != hovered.as_ref().map(Into::into) { + act!(mgr:peek, cx)?; + act!(mgr:watch, cx)?; + } + } + + succ!(); + } +} diff --git a/yazi-actor/src/mgr/mod.rs b/yazi-actor/src/mgr/mod.rs index b6bf2d4e0..5d7e64bee 100644 --- a/yazi-actor/src/mgr/mod.rs +++ b/yazi-actor/src/mgr/mod.rs @@ -11,6 +11,8 @@ yazi_macro::mod_flat!( download enter escape + exclude_add + excluded filter filter_do find @@ -21,6 +23,7 @@ yazi_macro::mod_flat!( hardlink hidden hover + ignore leave linemode link diff --git a/yazi-actor/src/mgr/refresh.rs b/yazi-actor/src/mgr/refresh.rs index 7ca8ff142..5de557b99 100644 --- a/yazi-actor/src/mgr/refresh.rs +++ b/yazi-actor/src/mgr/refresh.rs @@ -48,12 +48,11 @@ impl Refresh { // TODO: performance improvement fn trigger_dirs(folders: &[&Folder]) { - async fn go(dir: UrlBuf, cha: Cha) { - let Some(cha) = Files::assert_stale(&dir, cha).await else { return }; - - match Files::from_dir_bulk(&dir).await { - Ok(files) => FilesOp::Full(dir, files, cha).emit(), - Err(e) => FilesOp::issue_error(&dir, e).await, + async fn go(cwd: UrlBuf, cha: Cha) { + let Some(cha) = Files::assert_stale(&cwd, cha).await else { return }; + match Files::from_dir_bulk(&cwd).await { + Ok(files) => FilesOp::Full(cwd, files, cha).emit(), + Err(e) => FilesOp::issue_error(&cwd, e).await, } } @@ -62,7 +61,6 @@ impl Refresh { .filter(|&f| f.url.is_absolute() && f.url.is_internal()) .map(|&f| go(f.url.clone(), f.cha)) .collect(); - if !futs.is_empty() { tokio::spawn(futures::future::join_all(futs)); } diff --git a/yazi-config/preset/yazi-default.toml b/yazi-config/preset/yazi-default.toml index b74651cd1..8fb9f3dbc 100644 --- a/yazi-config/preset/yazi-default.toml +++ b/yazi-config/preset/yazi-default.toml @@ -28,6 +28,27 @@ image_quality = 75 ueberzug_scale = 1 ueberzug_offset = [ 0, 0, 0, 0 ] + +# [files] +# Context-specific exclude patterns +# Patterns are compiled into glob matchers for efficient matching +# Patterns starting with '!' negate (whitelist) previous matches +# Note: To match directories, use patterns like ".git/" or "**/.git/**" +# excludes = [ +# # SFTP temporary files +# { urn = "*.tmp", in = "sftp://**" }, +# # Python cache in search results +# { urn = "/root/**/*.pyc", in = "search://**" }, +# # Multiple patterns for /code directory (supports arrays) +# { urn = [".git", ".DS_Store", "__pycache__"], in = "*" }, +# # Exclude release and debug directories inside target/ +# { urn = "release", in = "**/target" }, +# # Exclude all json files from all contexts +# { urn = "*.json", in = "*" }, +# # Negation: Show cspell.* even if it's excluded by a previous rule +# { urn = "!cspell*", in = "search://**" }, +# ] + [opener] edit = [ { run = "${EDITOR:-vi} %s", desc = "$EDITOR", for = "unix", block = true }, diff --git a/yazi-config/src/files/exclude.rs b/yazi-config/src/files/exclude.rs new file mode 100644 index 000000000..ae4919332 --- /dev/null +++ b/yazi-config/src/files/exclude.rs @@ -0,0 +1,320 @@ +use std::path::Path; + +use globset::{Glob, GlobSet, GlobSetBuilder}; +use serde::Deserialize; + +/// Represents a single exclude rule with patterns and context +#[derive(Debug, Clone, Deserialize)] +pub struct Exclude { + /// Pattern(s) to match files/directories against + /// Can be a single glob pattern string or an array of glob patterns + /// Patterns starting with '!' negate (whitelist) previously matched patterns + #[serde(deserialize_with = "deserialize_urn")] + pub urn: Vec, + + /// Context where this exclude rule applies + /// Supports glob patterns like "/code/**", "sftp://**", "search://**", or "*" for all + pub r#in: String, + + #[serde(skip)] + compiled: Option, +} + +#[derive(Debug, Clone)] +struct CompiledPatterns { + /// Regular patterns (to ignore) + ignores: GlobSet, + /// Negated patterns (to whitelist/un-ignore) + whitelists: GlobSet, +} + +fn deserialize_urn<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum UrnOrUrns { + Single(String), + Multiple(Vec), + } + + match UrnOrUrns::deserialize(deserializer)? { + UrnOrUrns::Single(s) => Ok(vec![s]), + UrnOrUrns::Multiple(v) => Ok(v), + } +} + +impl Exclude { + /// Compile the glob patterns into GlobSets for efficient matching + pub fn compile(&mut self) -> Result<(), globset::Error> { + let mut ignore_builder = GlobSetBuilder::new(); + let mut whitelist_builder = GlobSetBuilder::new(); + + for pattern in &self.urn { + if let Some(negated) = pattern.strip_prefix('!') { + // Negation pattern - add to whitelist + // Transform simple filename patterns to match anywhere in the tree + let transformed = if negated.contains('/') || negated.starts_with("**/") { + negated.to_string() + } else { + // Match the item itself and everything inside it + format!("**/{}", negated) + }; + let glob = Glob::new(&transformed)?; + whitelist_builder.add(glob); + // Also match everything inside directories with this name + if !negated.contains('/') && !negated.starts_with("**/") { + let glob_inner = Glob::new(&format!("**/{}/**", negated))?; + whitelist_builder.add(glob_inner); + } + } else { + // Regular pattern - add to ignore list + // Transform simple filename patterns to match anywhere in the tree + let transformed = if pattern.contains('/') || pattern.starts_with("**/") { + pattern.to_string() + } else { + // Match the item itself: **/.git + format!("**/{}", pattern) + }; + let glob = Glob::new(&transformed)?; + ignore_builder.add(glob); + // Also match everything inside directories with this name: **/.git/** + if !pattern.contains('/') && !pattern.starts_with("**/") { + let glob_inner = Glob::new(&format!("**/{}/**", pattern))?; + ignore_builder.add(glob_inner); + } + } + } + + self.compiled = Some(CompiledPatterns { + ignores: ignore_builder.build()?, + whitelists: whitelist_builder.build()?, + }); + + Ok(()) + } + + /// Check if a path matches this exclude rule + /// Returns Some(true) if path should be ignored, Some(false) if whitelisted, + /// None if no match + pub fn matches_path(&self, path: &Path) -> Option { + let compiled = self.compiled.as_ref()?; + + // Check whitelist first (negation takes precedence) + if compiled.whitelists.is_match(path) { + return Some(false); // Explicitly NOT ignored + } + + // Check ignore patterns + if compiled.ignores.is_match(path) { + return Some(true); // Should be ignored + } + + None // No match + } + + /// Check if this exclude rule applies to the given path context + pub fn matches_context(&self, path: &str) -> bool { + // Wildcard matches everything + if self.r#in == "*" { + return true; + } + + // Handle patterns starting with **/ (e.g., **/target or **/target/**) + if self.r#in.starts_with("**/") { + let pattern = &self.r#in[3..]; // Remove leading "**/", e.g., "target" or "target/**" + + // Strip trailing /** if present + let pattern = if let Some(p) = pattern.strip_suffix("/**") { p } else { pattern }; + + // Check if path ends with the pattern (e.g., /home/user/project/target matches + // **/target) + if path.ends_with(&format!("/{}", pattern)) || path.ends_with(pattern) { + return true; + } + + // Check if path contains the pattern as a directory segment + if path.contains(&format!("/{}/", pattern)) { + return true; + } + + return false; + } + + // Handle glob patterns with wildcard + if self.r#in.ends_with("/**") { + let prefix = &self.r#in[..self.r#in.len() - 3]; + + // Check if path starts with prefix (absolute path match) + if path.starts_with(prefix) { + return true; + } + + // Check if path contains the pattern anywhere (for relative patterns like + // "/target/**") This allows "/target/**" to match + // "/home/user/project/target/debug" + if prefix.starts_with('/') && !prefix.starts_with("//") { + // Single leading slash means relative pattern - check if path contains this + // segment + let pattern = &prefix[1..]; // Remove leading slash + if path.contains(&format!("/{}/", pattern)) || path.ends_with(&format!("/{}", pattern)) { + return true; + } + } + + return false; + } + + // Exact match or prefix match for non-wildcard patterns + path == self.r#in || path.starts_with(&format!("{}/", self.r#in)) + } +} + +#[cfg(test)] +mod tests { + use std::path::Path; + + use super::*; + + fn create_exclude(in_pattern: &str, urn_patterns: Vec<&str>) -> Exclude { + let mut exclude = Exclude { + r#in: in_pattern.to_string(), + urn: urn_patterns.into_iter().map(String::from).collect(), + compiled: None, + }; + exclude.compile().unwrap(); + exclude + } + + #[test] + fn test_context_matching_with_dots() { + let exclude = create_exclude("*", vec![".git"]); + + // Test various path contexts - all should match "*" + let test_cases = vec![ + ("/home/user/projects/yazi/yazi", true), + ("/home/user/projects/yazi/yazi-rs.github.io", true), + ("/home/user/projects/yazi/command-palette.yazi", true), + ("/home/user/projects/yazi/gitignore.yazi", true), + ]; + + for (context, should_match) in test_cases { + let matches = exclude.matches_context(context); + assert_eq!( + matches, should_match, + "Context matching failed for {}: expected {}, got {}", + context, should_match, matches + ); + } + } + + #[test] + fn test_simple_pattern_behavior() { + // Test how globset matches simple patterns like ".git" + use globset::Glob; + + let patterns_and_paths = vec![ + // Pattern ".git" gets transformed to "**/.git" in our compile() method + // So let's test the transformed version + ("**/.git", "/home/user/.git", true), + ("**/.git", "/home/user/proj/.git", true), + ("**/.git", "/home/user/command-palette.yazi/.git", true), + ("**/.git", ".git", true), + // **/.git/** matches files INSIDE .git, not the .git directory itself + ("**/.git/**", "/home/user/.git", false), + ("**/.git/**", "/home/user/.git/config", true), + ]; + + for (pattern, path, expected) in patterns_and_paths { + let glob = Glob::new(pattern).unwrap().compile_matcher(); + let matches = glob.is_match(Path::new(path)); + assert_eq!( + matches, expected, + "Pattern '{}' with path '{}': expected {}, got {}", + pattern, path, expected, matches + ); + } + } + + #[test] + fn test_path_matching_with_git() { + let exclude = create_exclude("*", vec![".git"]); + + // Test various .git paths + // Pattern ".git" is transformed to "**/.git" which matches any component named + // .git + let test_paths = vec![ + ("/home/user/projects/yazi/yazi/.git", true), + ("/home/user/projects/yazi/yazi-rs.github.io/.git", true), + ("/home/user/projects/yazi/command-palette.yazi/.git", true), + ("/home/user/projects/yazi/gitignore.yazi/.git", true), + // Files inside .git now also match "**/.git" since .git is the component name + ("/home/user/projects/yazi/yazi/.git/config", true), + ("/home/user/projects/yazi/command-palette.yazi/.git/config", true), + ]; + + for (path_str, should_match) in test_paths { + let path = Path::new(path_str); + let result = exclude.matches_path(path); + // None means no match, which is equivalent to false (not ignored) + let actual = result.unwrap_or(false); + assert_eq!( + actual, should_match, + "Path matching failed for {}: expected {}, got {}", + path_str, should_match, actual + ); + } + } + + #[test] + fn test_glob_pattern_matching() { + // User provides "**/.git" explicitly - matches only things NAMED .git + let exclude = create_exclude("*", vec!["**/.git"]); + + let test_paths = vec![ + // Should match .git directory itself + ("/home/user/projects/yazi/yazi/.git", true), + ("/home/user/projects/yazi/command-palette.yazi/.git", true), + // Should NOT match files inside .git when using **/.git alone + ("/home/user/projects/yazi/yazi/.git/config", false), + ]; + + for (path_str, should_match) in test_paths { + let path = Path::new(path_str); + let result = exclude.matches_path(path); + let actual = result.unwrap_or(false); + assert_eq!( + actual, should_match, + "Glob pattern matching failed for {}: expected {}, got {}", + path_str, should_match, actual + ); + } + } + + #[test] + fn test_glob_pattern_with_trailing_slash() { + // Pattern **/.git/** means match everything inside any .git directory + let exclude = create_exclude("*", vec!["**/.git/**"]); + + let test_paths = vec![ + // Should NOT match .git directory itself with /** + ("/home/user/projects/yazi/yazi/.git", false), + // Should match all files inside .git + ("/home/user/projects/yazi/yazi/.git/config", true), + ("/home/user/projects/yazi/command-palette.yazi/.git", false), + ("/home/user/projects/yazi/command-palette.yazi/.git/config", true), + ]; + + for (path_str, should_match) in test_paths { + let path = Path::new(path_str); + let result = exclude.matches_path(path); + let actual = result.unwrap_or(false); + assert_eq!( + actual, should_match, + "Glob pattern with /** matching failed for {}: expected {}, got {}", + path_str, should_match, actual + ); + } + } +} diff --git a/yazi-config/src/files/files.rs b/yazi-config/src/files/files.rs new file mode 100644 index 000000000..ca12f5de4 --- /dev/null +++ b/yazi-config/src/files/files.rs @@ -0,0 +1,52 @@ +use std::path::Path; + +use serde::Deserialize; +use yazi_codegen::DeserializeOver2; + +use super::Exclude; + +/// Configuration for file filtering +#[derive(Debug, Deserialize, DeserializeOver2, Default)] +pub struct Files { + /// List of exclude rules with context-specific patterns + #[serde(default)] + pub excludes: Vec, +} + +impl Files { + /// Compile all glob patterns in exclude rules + pub fn compile(&mut self) -> Result<(), String> { + for exclude in &mut self.excludes { + exclude.compile().map_err(|e| format!("Failed to compile glob pattern: {}", e))?; + } + Ok(()) + } + + /// Get all exclude patterns that apply to a given context + pub fn excludes_for_context(&self, context: &str) -> Vec { + self + .excludes + .iter() + .filter(|e| e.matches_context(context)) + .flat_map(|e| e.urn.iter().cloned()) + .collect() + } + + /// Check if a path should be excluded based on compiled patterns for a given + /// context Returns Some(true) if should be ignored, Some(false) if + /// whitelisted, None if no match + pub fn matches_path(&self, path: &Path, context: &str) -> Option { + // Process rules in order, last match wins + let mut result = None; + + for exclude in &self.excludes { + if exclude.matches_context(context) + && let Some(should_ignore) = exclude.matches_path(path) + { + result = Some(should_ignore); + } + } + + result + } +} diff --git a/yazi-config/src/files/mod.rs b/yazi-config/src/files/mod.rs new file mode 100644 index 000000000..ec9007bad --- /dev/null +++ b/yazi-config/src/files/mod.rs @@ -0,0 +1,5 @@ +mod exclude; +mod files; + +pub use exclude::*; +pub use files::*; diff --git a/yazi-config/src/lib.rs b/yazi-config/src/lib.rs index 7668fd29e..5884009ca 100644 --- a/yazi-config/src/lib.rs +++ b/yazi-config/src/lib.rs @@ -1,4 +1,4 @@ -yazi_macro::mod_pub!(keymap mgr open opener plugin popup preview tasks theme which vfs); +yazi_macro::mod_pub!(files keymap mgr open opener plugin popup preview tasks theme which vfs); yazi_macro::mod_flat!(icon layout pattern platform preset priority style utils yazi); diff --git a/yazi-config/src/yazi.rs b/yazi-config/src/yazi.rs index ba346ccf5..0701ccc83 100644 --- a/yazi-config/src/yazi.rs +++ b/yazi-config/src/yazi.rs @@ -3,11 +3,13 @@ use serde::Deserialize; use yazi_codegen::DeserializeOver1; use yazi_fs::{Xdg, ok_or_not_found}; -use crate::{mgr, open, opener, plugin, popup, preview, tasks, which}; +use crate::{files, mgr, open, opener, plugin, popup, preview, tasks, which}; #[derive(Deserialize, DeserializeOver1)] pub struct Yazi { pub mgr: mgr::Mgr, + #[serde(default)] + pub files: files::Files, pub preview: preview::Preview, pub opener: opener::Opener, pub open: open::Open, @@ -26,9 +28,13 @@ impl Yazi { .with_context(|| format!("Failed to read config {p:?}")) } - pub(super) fn reshape(self) -> Result { + pub(super) fn reshape(mut self) -> Result { + // Compile glob patterns in exclude rules + self.files.compile().map_err(|e| anyhow::anyhow!(e))?; + Ok(Self { mgr: self.mgr.reshape()?, + files: self.files, preview: self.preview.reshape()?, opener: self.opener.reshape()?, open: self.open.reshape()?, diff --git a/yazi-dds/src/spark/spark.rs b/yazi-dds/src/spark/spark.rs index 8aad493a6..d272e8537 100644 --- a/yazi-dds/src/spark/spark.rs +++ b/yazi-dds/src/spark/spark.rs @@ -25,6 +25,8 @@ pub enum Spark<'a> { EscapeSearch(yazi_parser::VoidOpt), EscapeSelect(yazi_parser::VoidOpt), EscapeVisual(yazi_parser::VoidOpt), + ExcludeAdd(yazi_parser::mgr::ExcludeAddOpt), + Excluded(yazi_parser::mgr::ExcludedOpt), Filter(yazi_parser::mgr::FilterOpt), FilterDo(yazi_parser::mgr::FilterOpt), Find(yazi_parser::mgr::FindOpt), @@ -34,6 +36,7 @@ pub enum Spark<'a> { Forward(yazi_parser::VoidOpt), Hardlink(yazi_parser::mgr::HardlinkOpt), Hidden(yazi_parser::mgr::HiddenOpt), + Ignore(yazi_parser::VoidOpt), Hover(yazi_parser::mgr::HoverOpt), Leave(yazi_parser::VoidOpt), Linemode(yazi_parser::mgr::LinemodeOpt), @@ -176,6 +179,8 @@ impl<'a> IntoLua for Spark<'a> { Self::EscapeSearch(b) => b.into_lua(lua), Self::EscapeSelect(b) => b.into_lua(lua), Self::EscapeVisual(b) => b.into_lua(lua), + Self::ExcludeAdd(b) => b.into_lua(lua), + Self::Excluded(b) => b.into_lua(lua), Self::Filter(b) => b.into_lua(lua), Self::FilterDo(b) => b.into_lua(lua), Self::Find(b) => b.into_lua(lua), @@ -185,6 +190,7 @@ impl<'a> IntoLua for Spark<'a> { Self::Forward(b) => b.into_lua(lua), Self::Hardlink(b) => b.into_lua(lua), Self::Hidden(b) => b.into_lua(lua), + Self::Ignore(b) => b.into_lua(lua), Self::Hover(b) => b.into_lua(lua), Self::Leave(b) => b.into_lua(lua), Self::Linemode(b) => b.into_lua(lua), @@ -300,6 +306,7 @@ try_from_spark!( mgr:escape_visual, mgr:follow, mgr:forward, + mgr:ignore, mgr:leave, mgr:refresh, mgr:search_stop, @@ -332,6 +339,7 @@ try_from_spark!(mgr::CreateOpt, mgr:create); try_from_spark!(mgr::DisplaceDoOpt, mgr:displace_do); try_from_spark!(mgr::DownloadOpt, mgr:download); try_from_spark!(mgr::EscapeOpt, mgr:escape); +try_from_spark!(mgr::ExcludeAddOpt, mgr:exclude_add); try_from_spark!(mgr::FilterOpt, mgr:filter, mgr:filter_do); try_from_spark!(mgr::FindArrowOpt, mgr:find_arrow); try_from_spark!(mgr::FindDoOpt, mgr:find_do); @@ -352,6 +360,7 @@ try_from_spark!(mgr::RevealOpt, mgr:reveal); try_from_spark!(mgr::SearchOpt, mgr:search, mgr:search_do); try_from_spark!(mgr::SeekOpt, mgr:seek); try_from_spark!(mgr::ShellOpt, mgr:shell); +try_from_spark!(mgr::ExcludedOpt, mgr:excluded); try_from_spark!(mgr::SortOpt, mgr:sort); try_from_spark!(mgr::SpotOpt, mgr:spot); try_from_spark!(mgr::StashOpt, mgr:stash); diff --git a/yazi-fm/src/executor.rs b/yazi-fm/src/executor.rs index 684ed033a..ccbcf88a5 100644 --- a/yazi-fm/src/executor.rs +++ b/yazi-fm/src/executor.rs @@ -111,6 +111,8 @@ impl<'a> Executor<'a> { on!(copy); on!(shell); on!(hidden); + on!(excluded); + on!(ignore); on!(linemode); on!(search); on!(search_do); @@ -120,6 +122,9 @@ impl<'a> Executor<'a> { on!(filter); on!(filter_do); + // Exclude + on!(exclude_add); + // Find on!(find); on!(find_do); diff --git a/yazi-fs/src/files.rs b/yazi-fs/src/files.rs index da21656ed..573d2cf09 100644 --- a/yazi-fs/src/files.rs +++ b/yazi-fs/src/files.rs @@ -3,7 +3,7 @@ use std::{mem, ops::{Deref, DerefMut, Not}}; use hashbrown::{HashMap, HashSet}; use yazi_shared::{Id, path::{PathBufDyn, PathDyn}}; -use super::{FilesSorter, Filter}; +use super::{FilesSorter, Filter, IgnoreFilter}; use crate::{FILES_TICKET, File, SortBy}; #[derive(Default)] @@ -16,9 +16,11 @@ pub struct Files { pub sizes: HashMap, - sorter: FilesSorter, - filter: Option, - show_hidden: bool, + sorter: FilesSorter, + filter: Option, + show_hidden: bool, + show_excluded: bool, + ignore_filter: Option, } impl Deref for Files { @@ -256,9 +258,26 @@ impl Files { fn split_files(&self, files: impl IntoIterator) -> (Vec, Vec) { if let Some(filter) = &self.filter { - files - .into_iter() - .partition(|f| (f.is_hidden() && !self.show_hidden) || !filter.matches(f.urn())) + files.into_iter().partition(|f| { + (f.is_hidden() && !self.show_hidden) + || !filter.matches(f.urn()) + || (!self.show_excluded + && self.ignore_filter.as_ref().is_some_and(|ig| ig.matches_url(&f.url))) + }) + } else if let Some(ignore_filter) = &self.ignore_filter { + if self.show_excluded { + // Show excluded files - only hide based on hidden status + if self.show_hidden { + (vec![], files.into_iter().collect()) + } else { + files.into_iter().partition(|f| f.is_hidden()) + } + } else { + // Hide excluded files - apply ignore filter + files + .into_iter() + .partition(|f| (f.is_hidden() && !self.show_hidden) || ignore_filter.matches_url(&f.url)) + } } else if self.show_hidden { (vec![], files.into_iter().collect()) } else { @@ -315,6 +334,45 @@ impl Files { true } + // --- Ignore filter + #[inline] + pub fn ignore_filter(&self) -> Option<&IgnoreFilter> { self.ignore_filter.as_ref() } + + pub fn set_ignore_filter(&mut self, ignore_filter: Option) -> bool { + if self.ignore_filter.is_none() && ignore_filter.is_none() { + return false; + } + + self.ignore_filter = ignore_filter; + + // Re-split files with the new ignore filter + let it = mem::take(&mut self.items).into_iter().chain(mem::take(&mut self.hidden)); + (self.hidden, self.items) = self.split_files(it); + self.sorter.sort(&mut self.items, &self.sizes); + self.revision += 1; + true + } + + // --- Show excluded + #[inline] + pub fn show_excluded(&self) -> bool { self.show_excluded } + + pub fn set_show_excluded(&mut self, state: bool) -> bool { + if self.show_excluded == state { + return false; + } + + self.show_excluded = state; + + // Re-split files with the new state + let it = mem::take(&mut self.items).into_iter().chain(mem::take(&mut self.hidden)); + (self.hidden, self.items) = self.split_files(it); + self.sorter.sort(&mut self.items, &self.sizes); + self.revision += 1; + + true + } + // --- Show hidden pub fn set_show_hidden(&mut self, state: bool) { if self.show_hidden == state { diff --git a/yazi-fs/src/ignore.rs b/yazi-fs/src/ignore.rs new file mode 100644 index 000000000..d332f5258 --- /dev/null +++ b/yazi-fs/src/ignore.rs @@ -0,0 +1,73 @@ +use std::{path::Path, sync::Arc}; + +use yazi_shared::url::AsUrl; + +/// Filter for ignoring files based on custom exclude patterns. +/// +/// Exclude patterns can be context-specific and support negation using the `!` +/// prefix for whitelisting. +#[derive(Clone)] +pub struct IgnoreFilter { + /// Custom glob-based matcher function for pattern matching + /// Returns Some(true) if should be ignored, Some(false) if whitelisted, None + /// if no match + glob_matcher: Option Option + Send + Sync>>, +} + +impl std::fmt::Debug for IgnoreFilter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("IgnoreFilter") + .field("glob_matcher", &self.glob_matcher.as_ref().map(|_| "Some(...)")) + .finish() + } +} + +impl IgnoreFilter { + /// Creates a new `IgnoreFilter` from exclude patterns. + /// + /// # Arguments + /// + /// * `glob_matcher` - Custom glob-based matcher function for advanced pattern + /// matching + /// + /// # Returns + /// + /// `Some(IgnoreFilter)` if glob_matcher is provided, `None` otherwise. + pub fn from_patterns( + glob_matcher: Option Option + Send + Sync>>, + ) -> Option { + if glob_matcher.is_none() { + return None; + } + + Some(Self { glob_matcher }) + } + + /// Checks if a file should be ignored based on its URL. + /// + /// # Arguments + /// + /// * `url` - URL of the file to check + /// + /// # Returns + /// + /// `true` if the file should be ignored, `false` otherwise. + /// + /// # Matching Logic + /// + /// Uses the glob matcher to check if the path should be ignored. + /// Returns true if matched as ignore, false if whitelisted or no match. + pub fn matches_url(&self, url: impl AsUrl) -> bool { + let url = url.as_url(); + let Ok(path) = url.loc().as_os() else { return false }; + + // Check glob matcher + if let Some(ref matcher) = self.glob_matcher { + if let Some(should_ignore) = matcher(path) { + return should_ignore; + } + } + + false + } +} diff --git a/yazi-fs/src/lib.rs b/yazi-fs/src/lib.rs index 36d5962ab..096bd524a 100644 --- a/yazi-fs/src/lib.rs +++ b/yazi-fs/src/lib.rs @@ -1,6 +1,6 @@ yazi_macro::mod_pub!(cha error mounts path provider); -yazi_macro::mod_flat!(cwd file files filter fns hash op scheme sorter sorting splatter stage url xdg); +yazi_macro::mod_flat!(cwd file files filter fns hash ignore op scheme sorter sorting splatter stage url xdg); pub fn init() { CWD.init(<_>::default()); diff --git a/yazi-parser/src/mgr/exclude_add.rs b/yazi-parser/src/mgr/exclude_add.rs new file mode 100644 index 000000000..416ade7a7 --- /dev/null +++ b/yazi-parser/src/mgr/exclude_add.rs @@ -0,0 +1,23 @@ +use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; +use yazi_shared::{SStr, event::CmdCow}; + +#[derive(Debug)] +pub struct ExcludeAddOpt { + pub patterns: Vec, +} + +impl TryFrom for ExcludeAddOpt { + type Error = anyhow::Error; + + fn try_from(mut c: CmdCow) -> Result { + Ok(Self { patterns: c.take_seq::().into_iter().map(|s| s.to_string()).collect() }) + } +} + +impl FromLua for ExcludeAddOpt { + fn from_lua(_: Value, _: &Lua) -> mlua::Result { Err("unsupported".into_lua_err()) } +} + +impl IntoLua for ExcludeAddOpt { + fn into_lua(self, _: &Lua) -> mlua::Result { Err("unsupported".into_lua_err()) } +} diff --git a/yazi-parser/src/mgr/excluded.rs b/yazi-parser/src/mgr/excluded.rs new file mode 100644 index 000000000..5b784b890 --- /dev/null +++ b/yazi-parser/src/mgr/excluded.rs @@ -0,0 +1,56 @@ +use std::str::FromStr; + +use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; +use serde::{Deserialize, Serialize}; +use yazi_shared::event::CmdCow; + +#[derive(Debug, Default)] +pub struct ExcludedOpt { + pub state: ExcludedOptState, +} + +impl TryFrom for ExcludedOpt { + type Error = anyhow::Error; + + fn try_from(c: CmdCow) -> Result { + Ok(Self { state: c.str(0).parse().unwrap_or_default() }) + } +} + +impl FromLua for ExcludedOpt { + fn from_lua(_: Value, _: &Lua) -> mlua::Result { Err("unsupported".into_lua_err()) } +} + +impl IntoLua for ExcludedOpt { + fn into_lua(self, _: &Lua) -> mlua::Result { Err("unsupported".into_lua_err()) } +} + +// --- State +#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum ExcludedOptState { + #[default] + None, + Show, + Hide, + Toggle, +} + +impl FromStr for ExcludedOptState { + type Err = serde::de::value::Error; + + fn from_str(s: &str) -> Result { + Self::deserialize(serde::de::value::StrDeserializer::new(s)) + } +} + +impl ExcludedOptState { + pub fn bool(self, old: bool) -> bool { + match self { + Self::None => old, + Self::Show => true, + Self::Hide => false, + Self::Toggle => !old, + } + } +} diff --git a/yazi-parser/src/mgr/mod.rs b/yazi-parser/src/mgr/mod.rs index 085ae19ea..9c210e881 100644 --- a/yazi-parser/src/mgr/mod.rs +++ b/yazi-parser/src/mgr/mod.rs @@ -6,6 +6,8 @@ yazi_macro::mod_flat!( displace_do download escape + exclude_add + excluded filter find find_arrow diff --git a/yazi-proxy/src/mgr.rs b/yazi-proxy/src/mgr.rs index 757b0c6b4..cc66e5748 100644 --- a/yazi-proxy/src/mgr.rs +++ b/yazi-proxy/src/mgr.rs @@ -73,6 +73,10 @@ impl MgrProxy { emit!(Call(relay!(mgr:upload).with_seq(urls))); } + pub fn exclude_add(patterns: Vec) { + emit!(Call(relay!(mgr:exclude_add).with_seq(patterns))); + } + pub fn watch() { emit!(Call(relay!(mgr:watch))); } diff --git a/yazi-shared/src/url/buf.rs b/yazi-shared/src/url/buf.rs index 3acfca563..3f8ed6a3e 100644 --- a/yazi-shared/src/url/buf.rs +++ b/yazi-shared/src/url/buf.rs @@ -162,7 +162,7 @@ impl UrlBuf { Self::Archive { loc, domain } => { Self::Archive { loc: loc.rebase(base), domain: domain.clone() } } - Self::Sftp { loc, domain } => { + Self::Sftp { loc: _, domain: _ } => { todo!(); // Self::Sftp { loc: loc.rebase(base), domain: domain.clone() } }