mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
Add support for an exclude API for Lua plugins allowing the used of exclude patterns externally
This commit is contained in:
parent
d7d9ef12b3
commit
cb11bdbe31
9 changed files with 217 additions and 0 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -4644,6 +4644,7 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"crossterm 0.29.0",
|
"crossterm 0.29.0",
|
||||||
"futures",
|
"futures",
|
||||||
|
"globset",
|
||||||
"hashbrown 0.16.0",
|
"hashbrown 0.16.0",
|
||||||
"libc",
|
"libc",
|
||||||
"mlua",
|
"mlua",
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ yazi-widgets = { path = "../yazi-widgets", version = "25.9.15" }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
crossterm = { workspace = true }
|
crossterm = { workspace = true }
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
|
globset = { workspace = true }
|
||||||
hashbrown = { workspace = true }
|
hashbrown = { workspace = true }
|
||||||
mlua = { workspace = true }
|
mlua = { workspace = true }
|
||||||
paste = { workspace = true }
|
paste = { workspace = true }
|
||||||
|
|
|
||||||
180
yazi-actor/src/mgr/exclude_add.rs
Normal file
180
yazi-actor/src/mgr/exclude_add.rs
Normal 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!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ yazi_macro::mod_flat!(
|
||||||
download
|
download
|
||||||
enter
|
enter
|
||||||
escape
|
escape
|
||||||
|
exclude_add
|
||||||
filter
|
filter
|
||||||
filter_do
|
filter_do
|
||||||
find
|
find
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ pub enum Spark<'a> {
|
||||||
EscapeSearch(yazi_parser::VoidOpt),
|
EscapeSearch(yazi_parser::VoidOpt),
|
||||||
EscapeSelect(yazi_parser::VoidOpt),
|
EscapeSelect(yazi_parser::VoidOpt),
|
||||||
EscapeVisual(yazi_parser::VoidOpt),
|
EscapeVisual(yazi_parser::VoidOpt),
|
||||||
|
ExcludeAdd(yazi_parser::mgr::ExcludeAddOpt),
|
||||||
Filter(yazi_parser::mgr::FilterOpt),
|
Filter(yazi_parser::mgr::FilterOpt),
|
||||||
FilterDo(yazi_parser::mgr::FilterOpt),
|
FilterDo(yazi_parser::mgr::FilterOpt),
|
||||||
Find(yazi_parser::mgr::FindOpt),
|
Find(yazi_parser::mgr::FindOpt),
|
||||||
|
|
@ -147,6 +148,7 @@ impl<'a> IntoLua for Spark<'a> {
|
||||||
Self::EscapeSearch(b) => b.into_lua(lua),
|
Self::EscapeSearch(b) => b.into_lua(lua),
|
||||||
Self::EscapeSelect(b) => b.into_lua(lua),
|
Self::EscapeSelect(b) => b.into_lua(lua),
|
||||||
Self::EscapeVisual(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::Filter(b) => b.into_lua(lua),
|
||||||
Self::FilterDo(b) => b.into_lua(lua),
|
Self::FilterDo(b) => b.into_lua(lua),
|
||||||
Self::Find(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::CreateOpt, mgr:create);
|
||||||
try_from_spark!(mgr::DownloadOpt, mgr:download);
|
try_from_spark!(mgr::DownloadOpt, mgr:download);
|
||||||
try_from_spark!(mgr::EscapeOpt, mgr:escape);
|
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::FilterOpt, mgr:filter, mgr:filter_do);
|
||||||
try_from_spark!(mgr::FindArrowOpt, mgr:find_arrow);
|
try_from_spark!(mgr::FindArrowOpt, mgr:find_arrow);
|
||||||
try_from_spark!(mgr::FindDoOpt, mgr:find_do);
|
try_from_spark!(mgr::FindDoOpt, mgr:find_do);
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,9 @@ impl<'a> Executor<'a> {
|
||||||
on!(filter);
|
on!(filter);
|
||||||
on!(filter_do);
|
on!(filter_do);
|
||||||
|
|
||||||
|
// Exclude
|
||||||
|
on!(exclude_add);
|
||||||
|
|
||||||
// Find
|
// Find
|
||||||
on!(find);
|
on!(find);
|
||||||
on!(find_do);
|
on!(find_do);
|
||||||
|
|
|
||||||
23
yazi-parser/src/mgr/exclude_add.rs
Normal file
23
yazi-parser/src/mgr/exclude_add.rs
Normal 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()) }
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ yazi_macro::mod_flat!(
|
||||||
create
|
create
|
||||||
download
|
download
|
||||||
escape
|
escape
|
||||||
|
exclude_add
|
||||||
filter
|
filter
|
||||||
find
|
find
|
||||||
find_arrow
|
find_arrow
|
||||||
|
|
|
||||||
|
|
@ -72,4 +72,8 @@ impl MgrProxy {
|
||||||
{
|
{
|
||||||
emit!(Call(relay!(mgr:upload).with_seq(urls)));
|
emit!(Call(relay!(mgr:upload).with_seq(urls)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn exclude_add(patterns: Vec<String>) {
|
||||||
|
emit!(Call(relay!(mgr:exclude_add).with_seq(patterns)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue