Add support for an exclude API for Lua plugins allowing the used of exclude patterns externally

This commit is contained in:
Carlos de Paula 2025-10-30 10:59:17 -03:00
parent d7d9ef12b3
commit cb11bdbe31
No known key found for this signature in database
9 changed files with 217 additions and 0 deletions

1
Cargo.lock generated
View file

@ -4644,6 +4644,7 @@ dependencies = [
"anyhow",
"crossterm 0.29.0",
"futures",
"globset",
"hashbrown 0.16.0",
"libc",
"mlua",

View file

@ -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 }

View file

@ -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<Data> {
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<Arc<dyn Fn(&std::path::Path) -> Option<bool> + 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<IgnoreFilter>| {
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<Arc<dyn Fn(&std::path::Path) -> Option<bool> + 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!();
}
}

View file

@ -9,6 +9,7 @@ yazi_macro::mod_flat!(
download
enter
escape
exclude_add
filter
filter_do
find

View file

@ -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);

View file

@ -121,6 +121,9 @@ impl<'a> Executor<'a> {
on!(filter);
on!(filter_do);
// Exclude
on!(exclude_add);
// Find
on!(find);
on!(find_do);

View file

@ -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<String>,
}
impl TryFrom<CmdCow> for ExcludeAddOpt {
type Error = anyhow::Error;
fn try_from(mut c: CmdCow) -> Result<Self, Self::Error> {
Ok(Self { patterns: c.take_seq::<SStr>().into_iter().map(|s| s.to_string()).collect() })
}
}
impl FromLua for ExcludeAddOpt {
fn from_lua(_: Value, _: &Lua) -> mlua::Result<Self> { Err("unsupported".into_lua_err()) }
}
impl IntoLua for ExcludeAddOpt {
fn into_lua(self, _: &Lua) -> mlua::Result<Value> { Err("unsupported".into_lua_err()) }
}

View file

@ -5,6 +5,7 @@ yazi_macro::mod_flat!(
create
download
escape
exclude_add
filter
find
find_arrow

View file

@ -72,4 +72,8 @@ impl MgrProxy {
{
emit!(Call(relay!(mgr:upload).with_seq(urls)));
}
pub fn exclude_add(patterns: Vec<String>) {
emit!(Call(relay!(mgr:exclude_add).with_seq(patterns)));
}
}