diff --git a/Cargo.lock b/Cargo.lock index ecd39549..21bc9c78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4644,6 +4644,7 @@ dependencies = [ "anyhow", "crossterm 0.29.0", "futures", + "globset", "hashbrown 0.16.0", "libc", "mlua", diff --git a/yazi-actor/Cargo.toml b/yazi-actor/Cargo.toml index 6cad1e14..e07c6841 100644 --- a/yazi-actor/Cargo.toml +++ b/yazi-actor/Cargo.toml @@ -34,6 +34,7 @@ yazi-widgets = { path = "../yazi-widgets", version = "25.9.15" } 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/exclude_add.rs b/yazi-actor/src/mgr/exclude_add.rs new file mode 100644 index 00000000..25d3827d --- /dev/null +++ b/yazi-actor/src/mgr/exclude_add.rs @@ -0,0 +1,180 @@ +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.as_path().map(|p| p.display().to_string()).unwrap_or_default() + }; + + // 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 + if apply(cx.current_mut(), ignore_filter.clone()) { + act!(mgr:hover, cx)?; + 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.as_path().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); + + let hovered_matcher: Option Option + Send + Sync>> = + if !hovered_all_patterns.is_empty() { + let context = hovered_str.clone(); + let patterns = hovered_all_patterns.clone(); + Some(Arc::new(move |path: &std::path::Path| { + if let Some(result) = YAZI.files.matches_path(path, &context) { + return Some(result); + } + for pattern in &patterns { + if let Some(negated) = pattern.strip_prefix('!') { + if path.to_str().map_or(false, |p| p.contains(negated)) { + return Some(false); + } + } else if path.to_str().map_or(false, |p| p.contains(pattern)) { + return Some(true); + } + } + 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 hovered.as_deref() != cx.hovered().map(|f| f.urn()) { + 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 49f646d0..ddb3bce8 100644 --- a/yazi-actor/src/mgr/mod.rs +++ b/yazi-actor/src/mgr/mod.rs @@ -9,6 +9,7 @@ yazi_macro::mod_flat!( download enter escape + exclude_add filter filter_do find diff --git a/yazi-dds/src/spark/spark.rs b/yazi-dds/src/spark/spark.rs index fe959eb6..1a76412d 100644 --- a/yazi-dds/src/spark/spark.rs +++ b/yazi-dds/src/spark/spark.rs @@ -23,6 +23,7 @@ pub enum Spark<'a> { EscapeSearch(yazi_parser::VoidOpt), EscapeSelect(yazi_parser::VoidOpt), EscapeVisual(yazi_parser::VoidOpt), + ExcludeAdd(yazi_parser::mgr::ExcludeAddOpt), Filter(yazi_parser::mgr::FilterOpt), FilterDo(yazi_parser::mgr::FilterOpt), Find(yazi_parser::mgr::FindOpt), @@ -147,6 +148,7 @@ 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::Filter(b) => b.into_lua(lua), Self::FilterDo(b) => b.into_lua(lua), Self::Find(b) => b.into_lua(lua), @@ -287,6 +289,7 @@ try_from_spark!(mgr::CopyOpt, mgr:copy); try_from_spark!(mgr::CreateOpt, mgr:create); 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); diff --git a/yazi-fm/src/executor.rs b/yazi-fm/src/executor.rs index 676ee11e..15e97118 100644 --- a/yazi-fm/src/executor.rs +++ b/yazi-fm/src/executor.rs @@ -121,6 +121,9 @@ impl<'a> Executor<'a> { on!(filter); on!(filter_do); + // Exclude + on!(exclude_add); + // Find on!(find); on!(find_do); diff --git a/yazi-parser/src/mgr/exclude_add.rs b/yazi-parser/src/mgr/exclude_add.rs new file mode 100644 index 00000000..416ade7a --- /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/mod.rs b/yazi-parser/src/mgr/mod.rs index caec442e..7d309192 100644 --- a/yazi-parser/src/mgr/mod.rs +++ b/yazi-parser/src/mgr/mod.rs @@ -5,6 +5,7 @@ yazi_macro::mod_flat!( create download escape + exclude_add filter find find_arrow diff --git a/yazi-proxy/src/mgr.rs b/yazi-proxy/src/mgr.rs index 1977d8c7..56842511 100644 --- a/yazi-proxy/src/mgr.rs +++ b/yazi-proxy/src/mgr.rs @@ -72,4 +72,8 @@ 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))); + } }