feat: dynamic open/opener Lua API (#3901)

This commit is contained in:
三咲雅 misaki masa 2026-04-19 16:58:57 +08:00 committed by GitHub
parent d1a27bca1f
commit 369c47539c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
58 changed files with 1431 additions and 702 deletions

View file

@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- New `--in` for `search` action to set search directory ([#3696])
- Hover cursor over the new file after copying/cutting/linking/hardlinking/extracting ([#3846], [#3854])
- Multi-file spotter ([#3733])
- Dynamic open/opener Lua API ([#3901])
- Dynamic previewer Lua API ([#3891])
- Vim-like `lua` action that runs an inline Lua snippet ([#3813])
- Certificate authentication for SFTP VFS provider ([#3716])
@ -1703,3 +1704,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
[#3862]: https://github.com/sxyazi/yazi/pull/3862
[#3891]: https://github.com/sxyazi/yazi/pull/3891
[#3894]: https://github.com/sxyazi/yazi/pull/3894
[#3901]: https://github.com/sxyazi/yazi/pull/3901

65
Cargo.lock generated
View file

@ -1310,9 +1310,9 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "ecdsa"
version = "0.17.0-rc.16"
version = "0.17.0-rc.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91bbdd377139884fafcad8dc43a760a3e1e681aa26db910257fa6535b70e1829"
checksum = "dc4bf51f0534ed6e59a0f2f26272b64ba55c470133f8424c2adfd1c4d59d9988"
dependencies = [
"der",
"digest 0.11.2",
@ -1357,9 +1357,9 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "elliptic-curve"
version = "0.14.0-rc.30"
version = "0.14.0-rc.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d7a0bfd012613a7bcfe02cbfccf2b846e9ef9e1bccb641c48d461253cfb034d"
checksum = "b148a81cede8f4023248f980cffdf7611c46f2add469c6980e815b7c5b764ba5"
dependencies = [
"base16ct",
"crypto-bigint",
@ -2667,7 +2667,7 @@ dependencies = [
"num-integer",
"num-iter",
"num-traits",
"rand 0.8.5",
"rand 0.8.6",
"serde",
"smallvec",
]
@ -2860,7 +2860,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e"
dependencies = [
"num-traits",
"rand 0.8.5",
"rand 0.8.6",
"serde",
]
@ -2916,7 +2916,7 @@ dependencies = [
"delegate",
"futures",
"log",
"rand 0.8.5",
"rand 0.8.6",
"sha2 0.10.9",
"thiserror 1.0.69",
"tokio",
@ -3114,7 +3114,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
dependencies = [
"phf_shared",
"rand 0.8.5",
"rand 0.8.6",
]
[[package]]
@ -3283,9 +3283,9 @@ dependencies = [
[[package]]
name = "primefield"
version = "0.14.0-rc.8"
version = "0.14.0-rc.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6543f5eec854fbf74ba5ef651fbdc9408919b47c3e1526623687135c16d12e9"
checksum = "1b52e6ee42db392378a95622b463c9740631171d1efce43fa445a569c1600cb6"
dependencies = [
"crypto-bigint",
"crypto-common 0.2.1",
@ -3297,9 +3297,9 @@ dependencies = [
[[package]]
name = "primeorder"
version = "0.14.0-rc.8"
version = "0.14.0-rc.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "569d9ad6ef822bb0322c7e7d84e5e286244050bd5246cac4c013535ae91c2c90"
checksum = "0556580e42c19833f5d232aca11a7687a503ee41f937b54f5ae1d50fc2a6a36a"
dependencies = [
"elliptic-curve",
]
@ -3356,9 +3356,9 @@ dependencies = [
[[package]]
name = "pxfm"
version = "0.1.28"
version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d"
checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"
[[package]]
name = "qoi"
@ -3429,9 +3429,9 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "rand"
version = "0.8.5"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
dependencies = [
"libc",
"rand_chacha 0.3.1",
@ -4447,6 +4447,12 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "symlink"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a"
[[package]]
name = "syn"
version = "1.0.109"
@ -4688,9 +4694,9 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.52.0"
version = "1.52.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a91135f59b1cbf38c91e73cf3386fca9bb77915c45ce2771460c9d92f0f3d776"
checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
dependencies = [
"bytes",
"libc",
@ -4790,11 +4796,12 @@ dependencies = [
[[package]]
name = "tracing-appender"
version = "0.2.4"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf"
checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c"
dependencies = [
"crossbeam-channel",
"symlink",
"thiserror 2.0.18",
"time",
"tracing-subscriber",
@ -5086,11 +5093,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
version = "1.0.2+wasi-0.2.9"
version = "1.0.3+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
dependencies = [
"wit-bindgen",
"wit-bindgen 0.57.1",
]
[[package]]
@ -5099,7 +5106,7 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
dependencies = [
"wit-bindgen",
"wit-bindgen 0.51.0",
]
[[package]]
@ -5664,6 +5671,12 @@ dependencies = [
"wit-bindgen-rust-macro",
]
[[package]]
name = "wit-bindgen"
version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[package]]
name = "wit-bindgen-core"
version = "0.51.0"
@ -5767,6 +5780,7 @@ dependencies = [
"either",
"futures",
"hashbrown 0.17.0",
"indexmap 2.14.0",
"libc",
"mlua",
"paste",
@ -5909,6 +5923,7 @@ dependencies = [
name = "yazi-codegen"
version = "26.2.2"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
@ -6278,6 +6293,7 @@ dependencies = [
"paste",
"percent-encoding",
"serde",
"serde_with",
"strum 0.28.0",
"thiserror 2.0.18",
"tokio",
@ -6293,6 +6309,7 @@ version = "26.2.2"
dependencies = [
"arc-swap",
"crossterm 0.29.0",
"mlua",
"ratatui",
"serde",
"thiserror 2.0.18",

View file

@ -71,7 +71,7 @@ serde_with = "3.18.0"
strum = { version = "0.28.0", features = [ "derive" ] }
syntect = { version = "5.3.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] }
thiserror = "2.0.18"
tokio = { version = "1.52.0", features = [ "full" ] }
tokio = { version = "1.52.1", features = [ "full" ] }
tokio-stream = "0.1.18"
tokio-util = "0.7.18"
toml = { version = "1.1.2" }

View file

@ -44,6 +44,7 @@ crossterm = { workspace = true }
either = { workspace = true }
futures = { workspace = true }
hashbrown = { workspace = true }
indexmap = { workspace = true }
mlua = { workspace = true }
paste = { workspace = true }
ratatui = { workspace = true }

View file

@ -1,4 +1,4 @@
use std::{hash::Hash, io::{Read, Write}, ops::Deref, path::Path};
use std::{hash::Hash, io::{Read, Write}, ops::Deref, path::Path, sync::Arc};
use anyhow::{Result, anyhow};
use crossterm::{execute, style::Print};
@ -154,8 +154,11 @@ impl BulkRename {
Ok(())
}
fn opener() -> Option<&'static OpenerRule> {
YAZI.opener.block(YAZI.open.all_dummy(Path::new("bulk-rename.txt"), "text/plain"))
fn opener() -> Option<Arc<OpenerRule>> {
YAZI
.open
.match_dummy(Path::new("bulk-rename.txt"), "text/plain")
.and_then(|r| YAZI.opener.block(&r))
}
fn replace_url(url: &UrlBuf, take: usize, rep: &StrandBuf) -> Result<UrlBuf> {

View file

@ -1,5 +1,6 @@
use anyhow::Result;
use hashbrown::HashMap;
use indexmap::IndexSet;
use yazi_config::{YAZI, popup::PickCfg};
use yazi_macro::succ;
use yazi_parser::mgr::OpenDoForm;
@ -33,7 +34,8 @@ impl Actor for OpenDo {
succ!(Self::match_and_open(cx, opt.cwd, targets));
}
let openers: Vec<_> = YAZI.opener.all(YAZI.open.common(&targets).into_iter()).collect();
let openers: IndexSet<_> =
YAZI.open.match_common(&targets).flat_map(|r| YAZI.opener.all(r)).collect();
if openers.is_empty() {
succ!();
}
@ -64,7 +66,9 @@ impl OpenDo {
fn match_and_open(cx: &Ctx, cwd: UrlBuf, targets: Vec<(yazi_fs::File, &str)>) {
let mut openers = HashMap::new();
for (file, mime) in targets {
if let Some(opener) = YAZI.opener.first(YAZI.open.all(&file, mime)) {
if let Some(open) = YAZI.open.matches(&file, mime)
&& let Some(opener) = YAZI.opener.first(&open)
{
openers.entry(opener).or_insert_with(|| vec![UrlCow::default()]).push(file.url.into());
}
}

View file

@ -1,8 +1,9 @@
use std::{ops::Deref, sync::Arc};
use mlua::{UserData, UserDataFields, Value};
use mlua::{ExternalError, FromLua, IntoLua, Lua, Table, UserData, UserDataFields, Value};
use yazi_config::YAZI;
use crate::{Id, cached_field};
use crate::{FileRef, Id, Iter, cached_field};
#[derive(Clone)]
pub struct Fetcher {
@ -30,3 +31,45 @@ impl UserData for Fetcher {
cached_field!(fields, name, |lua, me| lua.create_string(&*me.name));
}
}
// --- Matcher
pub struct FetcherMatcher(yazi_config::plugin::FetcherMatcher<'static>);
impl FetcherMatcher {
pub fn new(inner: impl Into<yazi_config::plugin::FetcherMatcher<'static>>) -> Self {
Self(inner.into())
}
}
impl TryFrom<Table> for FetcherMatcher {
type Error = mlua::Error;
fn try_from(value: Table) -> Result<Self, Self::Error> {
let id: Id = value.raw_get("id").unwrap_or_default();
let file: Option<FileRef> = value.raw_get("file")?;
let mime: Option<String> = value.raw_get("mime")?;
Ok(Self(yazi_config::plugin::FetcherMatcher {
fetchers: YAZI.plugin.fetchers.load_full(),
id: id.0,
file: file.map(|f| f.inner.clone().into()),
mime: mime.map(Into::into),
..Default::default()
}))
}
}
impl FromLua for FetcherMatcher {
fn from_lua(value: Value, _: &Lua) -> mlua::Result<Self> {
match value {
Value::Table(t) => t.try_into(),
_ => return Err("expected a table of FetcherMatcher".into_lua_err()),
}
}
}
impl IntoLua for FetcherMatcher {
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
Iter::new(self.0.into_iter().map(Fetcher::new), None).into_lua(lua)
}
}

View file

@ -1,7 +1,7 @@
use mlua::{ExternalError, FromLua, IntoLua, Lua, MetaMethod, Table, UserData, UserDataMethods, Value};
use mlua::{IntoLua, MetaMethod, UserData, UserDataMethods};
use yazi_config::YAZI;
use crate::{FileRef, Id, Iter, config::Fetcher};
use crate::config::FetcherMatcher;
pub struct Fetchers;
@ -15,45 +15,3 @@ impl UserData for Fetchers {
methods.add_meta_method(MetaMethod::Len, |_, _, ()| Ok(YAZI.plugin.fetchers.load().len()));
}
}
// --- Matcher
pub struct FetcherMatcher(yazi_config::plugin::FetcherMatcher<'static>);
impl FetcherMatcher {
fn new(inner: impl Into<yazi_config::plugin::FetcherMatcher<'static>>) -> Self {
Self(inner.into())
}
}
impl TryFrom<Table> for FetcherMatcher {
type Error = mlua::Error;
fn try_from(value: Table) -> Result<Self, Self::Error> {
let id: Id = value.raw_get("id").unwrap_or_default();
let file: Option<FileRef> = value.raw_get("file")?;
let mime: Option<String> = value.raw_get("mime")?;
Ok(Self(yazi_config::plugin::FetcherMatcher {
fetchers: YAZI.plugin.fetchers.load_full(),
id: id.0,
file: file.map(|f| f.inner.clone().into()),
mime: mime.map(Into::into),
..Default::default()
}))
}
}
impl FromLua for FetcherMatcher {
fn from_lua(value: Value, _: &Lua) -> mlua::Result<Self> {
match value {
Value::Table(t) => t.try_into(),
_ => return Err("expected a table of FetcherMatcher".into_lua_err()),
}
}
}
impl IntoLua for FetcherMatcher {
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
Iter::new(self.0.into_iter().map(Fetcher::new), None).into_lua(lua)
}
}

View file

@ -1 +1 @@
yazi_macro::mod_flat!(fetcher fetchers preloader preloaders previewer previewers spotter spotters);
yazi_macro::mod_flat!(fetcher fetchers open_rule open_rules opener opener_rule opener_rules preloader preloaders previewer previewers spotter spotters);

View file

@ -0,0 +1,85 @@
use std::{ops::Deref, sync::Arc};
use mlua::{ExternalError, FromLua, IntoLua, Lua, LuaSerdeExt, Table, UserData, UserDataFields, Value};
use yazi_config::YAZI;
use crate::{FileRef, Id, Iter, cached_field};
pub struct OpenRule {
inner: Arc<yazi_config::open::OpenRule>,
v_use: Option<Value>,
}
impl Deref for OpenRule {
type Target = Arc<yazi_config::open::OpenRule>;
fn deref(&self) -> &Self::Target { &self.inner }
}
impl From<OpenRule> for Arc<yazi_config::open::OpenRule> {
fn from(value: OpenRule) -> Self { value.inner }
}
impl OpenRule {
pub fn new(inner: impl Into<Arc<yazi_config::open::OpenRule>>) -> Self {
Self { inner: inner.into(), v_use: None }
}
}
impl FromLua for OpenRule {
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> {
Ok(Self::new(lua.from_value::<yazi_config::open::OpenRule>(value)?))
}
}
impl UserData for OpenRule {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("id", |_, me| Ok(Id(me.id)));
cached_field!(fields, use, |lua, me| {
lua.create_sequence_from(me.r#use.iter().map(|s| s.as_str()))
});
}
}
// --- Matcher
pub struct OpenRuleMatcher(pub(super) yazi_config::open::OpenRuleMatcher<'static>);
impl OpenRuleMatcher {
pub fn new(inner: impl Into<yazi_config::open::OpenRuleMatcher<'static>>) -> Self {
Self(inner.into())
}
}
impl TryFrom<Table> for OpenRuleMatcher {
type Error = mlua::Error;
fn try_from(value: Table) -> Result<Self, Self::Error> {
let id: Id = value.raw_get("id").unwrap_or_default();
let file: Option<FileRef> = value.raw_get("file")?;
let mime: Option<String> = value.raw_get("mime")?;
Ok(Self(yazi_config::open::OpenRuleMatcher {
rules: YAZI.open.load_full(),
id: id.0,
file: file.map(|f| f.inner.clone().into()),
mime: mime.map(Into::into),
..Default::default()
}))
}
}
impl FromLua for OpenRuleMatcher {
fn from_lua(value: Value, _: &Lua) -> mlua::Result<Self> {
match value {
Value::Table(t) => t.try_into(),
_ => return Err("expected a table of OpenRuleMatcher".into_lua_err()),
}
}
}
impl IntoLua for OpenRuleMatcher {
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
Iter::new(self.0.into_iter().map(OpenRule::new), None).into_lua(lua)
}
}

View file

@ -0,0 +1,39 @@
use mlua::{ExternalError, ExternalResult, IntoLua, MetaMethod, Table, UserData, UserDataMethods};
use yazi_config::YAZI;
use yazi_shim::mlua::DeserializeOverLua;
use crate::config::{OpenRule, OpenRuleMatcher};
pub struct OpenRules;
impl UserData for OpenRules {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method("match", |lua, _, matcher: Option<OpenRuleMatcher>| match matcher {
Some(matcher) => matcher.into_lua(lua),
None => OpenRuleMatcher::new(&*YAZI.open).into_lua(lua),
});
methods.add_method("insert", |_, _, (index, rule): (isize, OpenRule)| {
let index = match index {
1.. => index - 1,
0 => return Err("index must be 1-based or negative".into_lua_err()),
_ => index,
};
YAZI.open.insert(index, rule.clone().into()).into_lua_err()?;
Ok(rule)
});
methods.add_method("remove", |_, _, matcher: OpenRuleMatcher| {
YAZI.open.remove(matcher.0);
Ok(())
});
methods.add_method("update", |_, _, (matcher, table): (OpenRuleMatcher, Table)| {
YAZI.open.update(matcher.0, |rule| rule.deserialize_over_lua(&table))?;
Ok(())
});
methods.add_meta_method(MetaMethod::Len, |_, _, ()| Ok(YAZI.open.load().len()));
}
}

View file

@ -0,0 +1,45 @@
use mlua::{ExternalError, FromLua, IntoLua, IntoLuaMulti, MetaMethod, UserData, UserDataMethods, Value};
use yazi_config::{YAZI, opener::OpenerRulesMatcher};
use crate::config::OpenerRules;
pub struct Opener;
impl UserData for Opener {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(MetaMethod::Index, |lua, _, key: mlua::String| {
let key = key.to_str()?;
match YAZI.opener.load().get(&*key) {
Some(rules) => OpenerRules::new(rules.clone()).into_lua(lua),
None => Ok(Value::Nil),
}
});
methods.add_meta_method(MetaMethod::NewIndex, |lua, _, (key, value): (mlua::String, Value)| {
let key = key.to_str()?;
match value {
t @ Value::Table(_) => {
YAZI.opener.insert(&key, &*OpenerRules::from_lua(t, lua)?);
}
Value::Nil => {
YAZI.opener.remove(&key);
}
_ => return Err("expected a table or nil".into_lua_err()),
}
Ok(())
});
methods.add_meta_method(MetaMethod::Pairs, |lua, _, ()| {
let mut matcher = OpenerRulesMatcher::from(&YAZI.opener);
let iter = lua.create_function_mut(move |lua, ()| {
if let Some((name, rules)) = matcher.next() {
(name, OpenerRules::new(rules)).into_lua_multi(lua)
} else {
().into_lua_multi(lua)
}
})?;
Ok(iter)
});
}
}

View file

@ -0,0 +1,81 @@
use std::{ops::Deref, sync::Arc};
use mlua::{ExternalError, FromLua, IntoLua, Lua, LuaSerdeExt, Table, UserData, UserDataFields, Value};
use crate::{Id, Iter, cached_field};
pub struct OpenerRule {
inner: Arc<yazi_config::opener::OpenerRule>,
v_run: Option<Value>,
v_desc: Option<Value>,
}
impl Deref for OpenerRule {
type Target = Arc<yazi_config::opener::OpenerRule>;
fn deref(&self) -> &Self::Target { &self.inner }
}
impl From<OpenerRule> for Arc<yazi_config::opener::OpenerRule> {
fn from(value: OpenerRule) -> Self { value.inner }
}
impl OpenerRule {
pub fn new(inner: impl Into<Arc<yazi_config::opener::OpenerRule>>) -> Self {
Self { inner: inner.into(), v_run: None, v_desc: None }
}
}
impl FromLua for OpenerRule {
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> {
let mut inner: yazi_config::opener::OpenerRule = lua.from_value(value)?;
inner.fill();
Ok(Self::new(inner))
}
}
impl UserData for OpenerRule {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("id", |_, me| Ok(Id(me.id)));
cached_field!(fields, run, |lua, me| lua.create_string(&*me.run));
fields.add_field_method_get("block", |_, me| Ok(me.block));
fields.add_field_method_get("orphan", |_, me| Ok(me.orphan));
cached_field!(fields, desc, |lua, me| lua.create_string(&*me.desc));
}
}
// --- Matcher
pub struct OpenerRuleMatcher(pub(super) yazi_config::opener::OpenerRuleMatcher);
impl OpenerRuleMatcher {
pub fn new(inner: impl Into<yazi_config::opener::OpenerRuleMatcher>) -> Self {
Self(inner.into())
}
}
impl TryFrom<Table> for OpenerRuleMatcher {
type Error = mlua::Error;
fn try_from(t: Table) -> Result<Self, Self::Error> {
let id: Id = t.raw_get("id").unwrap_or_default();
Ok(Self(yazi_config::opener::OpenerRuleMatcher { id: id.0, ..Default::default() }))
}
}
impl FromLua for OpenerRuleMatcher {
fn from_lua(value: Value, _: &Lua) -> mlua::Result<Self> {
match value {
Value::Table(t) => t.try_into(),
_ => Err("expected a table of OpenerRuleMatcher".into_lua_err()),
}
}
}
impl IntoLua for OpenerRuleMatcher {
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
Iter::new(self.0.into_iter().map(OpenerRule::new), None).into_lua(lua)
}
}

View file

@ -0,0 +1,57 @@
use std::{ops::Deref, sync::Arc};
use mlua::{ExternalError, ExternalResult, FromLua, IntoLua, Lua, LuaSerdeExt, MetaMethod, UserData, UserDataMethods, Value};
use yazi_shim::toml::DeserializeOverHook;
use crate::config::{OpenerRule, OpenerRuleMatcher};
pub struct OpenerRules {
inner: Arc<yazi_config::opener::OpenerRules>,
}
impl Deref for OpenerRules {
type Target = Arc<yazi_config::opener::OpenerRules>;
fn deref(&self) -> &Self::Target { &self.inner }
}
impl OpenerRules {
pub fn new(inner: impl Into<Arc<yazi_config::opener::OpenerRules>>) -> Self {
Self { inner: inner.into() }
}
}
impl FromLua for OpenerRules {
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> {
let inner: yazi_config::opener::OpenerRules = lua.from_value(value)?;
Ok(Self::new(inner.deserialize_over_hook().into_lua_err()?))
}
}
impl UserData for OpenerRules {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method("match", |lua, me, matcher: Option<OpenerRuleMatcher>| match matcher {
Some(matcher) => matcher.into_lua(lua),
None => OpenerRuleMatcher::new(&*me.inner).into_lua(lua),
});
methods.add_method("insert", |_, me, (index, rule): (isize, OpenerRule)| {
let index = match index {
1.. => index - 1,
0 => return Err("index must be 1-based or negative".into_lua_err()),
_ => index,
};
me.inner.insert(index, rule.clone().into()).into_lua_err()?;
Ok(rule)
});
methods.add_method("remove", |_, me, matcher: OpenerRuleMatcher| {
me.inner.remove(matcher.0);
Ok(())
});
methods.add_meta_method(MetaMethod::Len, |_, me, ()| Ok(me.inner.load().len()));
}
}

View file

@ -1,8 +1,9 @@
use std::{ops::Deref, sync::Arc};
use mlua::{UserData, UserDataFields, Value};
use mlua::{ExternalError, FromLua, IntoLua, Lua, Table, UserData, UserDataFields, Value};
use yazi_config::YAZI;
use crate::{Id, cached_field};
use crate::{FileRef, Id, Iter, cached_field};
#[derive(Clone)]
pub struct Preloader {
@ -30,3 +31,45 @@ impl UserData for Preloader {
cached_field!(fields, name, |lua, me| lua.create_string(&*me.name));
}
}
// --- Matcher
pub struct PreloaderMatcher(pub(super) yazi_config::plugin::PreloaderMatcher<'static>);
impl PreloaderMatcher {
pub fn new(inner: impl Into<yazi_config::plugin::PreloaderMatcher<'static>>) -> Self {
Self(inner.into())
}
}
impl TryFrom<Table> for PreloaderMatcher {
type Error = mlua::Error;
fn try_from(value: Table) -> Result<Self, Self::Error> {
let id: Id = value.raw_get("id").unwrap_or_default();
let file: Option<FileRef> = value.raw_get("file")?;
let mime: Option<String> = value.raw_get("mime")?;
Ok(Self(yazi_config::plugin::PreloaderMatcher {
preloaders: YAZI.plugin.preloaders.load_full(),
id: id.0,
file: file.map(|f| f.inner.clone().into()),
mime: mime.map(Into::into),
..Default::default()
}))
}
}
impl FromLua for PreloaderMatcher {
fn from_lua(value: Value, _: &Lua) -> mlua::Result<Self> {
match value {
Value::Table(t) => t.try_into(),
_ => return Err("expected a table of PreloaderMatcher".into_lua_err()),
}
}
}
impl IntoLua for PreloaderMatcher {
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
Iter::new(self.0.into_iter().map(Preloader::new), None).into_lua(lua)
}
}

View file

@ -1,7 +1,7 @@
use mlua::{ExternalError, FromLua, IntoLua, Lua, MetaMethod, Table, UserData, UserDataMethods, Value};
use mlua::{IntoLua, MetaMethod, UserData, UserDataMethods};
use yazi_config::YAZI;
use crate::{FileRef, Id, Iter, config::Preloader};
use crate::config::PreloaderMatcher;
pub struct Preloaders;
@ -15,45 +15,3 @@ impl UserData for Preloaders {
methods.add_meta_method(MetaMethod::Len, |_, _, ()| Ok(YAZI.plugin.preloaders.load().len()));
}
}
// --- Matcher
pub struct PreloaderMatcher(yazi_config::plugin::PreloaderMatcher<'static>);
impl PreloaderMatcher {
fn new(inner: impl Into<yazi_config::plugin::PreloaderMatcher<'static>>) -> Self {
Self(inner.into())
}
}
impl TryFrom<Table> for PreloaderMatcher {
type Error = mlua::Error;
fn try_from(value: Table) -> Result<Self, Self::Error> {
let id: Id = value.raw_get("id").unwrap_or_default();
let file: Option<FileRef> = value.raw_get("file")?;
let mime: Option<String> = value.raw_get("mime")?;
Ok(Self(yazi_config::plugin::PreloaderMatcher {
preloaders: YAZI.plugin.preloaders.load_full(),
id: id.0,
file: file.map(|f| f.inner.clone().into()),
mime: mime.map(Into::into),
..Default::default()
}))
}
}
impl FromLua for PreloaderMatcher {
fn from_lua(value: Value, _: &Lua) -> mlua::Result<Self> {
match value {
Value::Table(t) => t.try_into(),
_ => return Err("expected a table of PreloaderMatcher".into_lua_err()),
}
}
}
impl IntoLua for PreloaderMatcher {
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
Iter::new(self.0.into_iter().map(Preloader::new), None).into_lua(lua)
}
}

View file

@ -1,11 +1,9 @@
use std::{ops::Deref, sync::Arc};
use mlua::{AnyUserData, ExternalError, FromLua, Lua, UserData, UserDataFields, Value};
use yazi_config::plugin::previewer_id;
use mlua::{ExternalError, FromLua, IntoLua, Lua, LuaSerdeExt, Table, UserData, UserDataFields, Value};
use yazi_config::YAZI;
use crate::{Id, Selector, cached_field};
const EXPECTED: &str = "expected a table or Previewer";
use crate::{FileRef, Id, Iter, cached_field};
#[derive(Clone)]
pub struct Previewer {
@ -30,33 +28,9 @@ impl Previewer {
}
}
impl TryFrom<mlua::Table> for Previewer {
type Error = mlua::Error;
fn try_from(t: mlua::Table) -> Result<Self, Self::Error> {
Ok(Self::new(yazi_config::plugin::Previewer {
id: previewer_id(),
selector: Selector::try_from(&t)?.into(),
run: t.raw_get::<mlua::String>("run")?.to_str()?.parse()?,
}))
}
}
impl TryFrom<AnyUserData> for Previewer {
type Error = mlua::Error;
fn try_from(value: AnyUserData) -> Result<Self, Self::Error> {
Ok(Self::new(value.borrow::<Self>()?.clone()))
}
}
impl FromLua for Previewer {
fn from_lua(value: Value, _: &Lua) -> mlua::Result<Self> {
match value {
Value::Table(tbl) => Self::try_from(tbl),
Value::UserData(ud) => Self::try_from(ud),
_ => Err(EXPECTED.into_lua_err())?,
}
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> {
Ok(Self::new(lua.from_value::<yazi_config::plugin::Previewer>(value)?))
}
}
@ -67,3 +41,45 @@ impl UserData for Previewer {
cached_field!(fields, name, |lua, me| lua.create_string(&*me.name));
}
}
// --- Matcher
pub struct PreviewerMatcher(pub(super) yazi_config::plugin::PreviewerMatcher<'static>);
impl PreviewerMatcher {
pub fn new(inner: impl Into<yazi_config::plugin::PreviewerMatcher<'static>>) -> Self {
Self(inner.into())
}
}
impl TryFrom<Table> for PreviewerMatcher {
type Error = mlua::Error;
fn try_from(value: Table) -> Result<Self, Self::Error> {
let id: Id = value.raw_get("id").unwrap_or_default();
let file: Option<FileRef> = value.raw_get("file")?;
let mime: Option<String> = value.raw_get("mime")?;
Ok(Self(yazi_config::plugin::PreviewerMatcher {
previewers: YAZI.plugin.previewers.load_full(),
id: id.0,
file: file.map(|f| f.inner.clone().into()),
mime: mime.map(Into::into),
..Default::default()
}))
}
}
impl FromLua for PreviewerMatcher {
fn from_lua(value: Value, _: &Lua) -> mlua::Result<Self> {
match value {
Value::Table(t) => t.try_into(),
_ => return Err("expected a table of PreviewerMatcher".into_lua_err()),
}
}
}
impl IntoLua for PreviewerMatcher {
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
Iter::new(self.0.into_iter().map(Previewer::new), None).into_lua(lua)
}
}

View file

@ -1,7 +1,7 @@
use mlua::{ExternalError, ExternalResult, FromLua, IntoLua, Lua, MetaMethod, Table, UserData, UserDataMethods, Value};
use mlua::{ExternalError, ExternalResult, IntoLua, MetaMethod, UserData, UserDataMethods};
use yazi_config::YAZI;
use crate::{FileRef, Id, Iter, config::Previewer};
use crate::config::{Previewer, PreviewerMatcher};
pub struct Previewers;
@ -31,45 +31,3 @@ impl UserData for Previewers {
methods.add_meta_method(MetaMethod::Len, |_, _, ()| Ok(YAZI.plugin.previewers.load().len()));
}
}
// --- Matcher
pub struct PreviewerMatcher(yazi_config::plugin::PreviewerMatcher<'static>);
impl PreviewerMatcher {
fn new(inner: impl Into<yazi_config::plugin::PreviewerMatcher<'static>>) -> Self {
Self(inner.into())
}
}
impl TryFrom<Table> for PreviewerMatcher {
type Error = mlua::Error;
fn try_from(value: Table) -> Result<Self, Self::Error> {
let id: Id = value.raw_get("id").unwrap_or_default();
let file: Option<FileRef> = value.raw_get("file")?;
let mime: Option<String> = value.raw_get("mime")?;
Ok(Self(yazi_config::plugin::PreviewerMatcher {
previewers: YAZI.plugin.previewers.load_full(),
id: id.0,
file: file.map(|f| f.inner.clone().into()),
mime: mime.map(Into::into),
..Default::default()
}))
}
}
impl FromLua for PreviewerMatcher {
fn from_lua(value: Value, _: &Lua) -> mlua::Result<Self> {
match value {
Value::Table(t) => t.try_into(),
_ => return Err("expected a table of PreviewerMatcher".into_lua_err()),
}
}
}
impl IntoLua for PreviewerMatcher {
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
Iter::new(self.0.into_iter().map(Previewer::new), None).into_lua(lua)
}
}

View file

@ -1,8 +1,9 @@
use std::{ops::Deref, sync::Arc};
use mlua::{UserData, UserDataFields, Value};
use mlua::{ExternalError, FromLua, IntoLua, Lua, Table, UserData, UserDataFields, Value};
use yazi_config::YAZI;
use crate::{Id, cached_field};
use crate::{FileRef, Id, Iter, cached_field};
#[derive(Clone)]
pub struct Spotter {
@ -30,3 +31,45 @@ impl UserData for Spotter {
cached_field!(fields, name, |lua, me| lua.create_string(&*me.name));
}
}
// --- Matcher
pub struct SpotterMatcher(pub(super) yazi_config::plugin::SpotterMatcher<'static>);
impl SpotterMatcher {
pub fn new(inner: impl Into<yazi_config::plugin::SpotterMatcher<'static>>) -> Self {
Self(inner.into())
}
}
impl TryFrom<Table> for SpotterMatcher {
type Error = mlua::Error;
fn try_from(value: Table) -> Result<Self, Self::Error> {
let id: Id = value.raw_get("id").unwrap_or_default();
let file: Option<FileRef> = value.raw_get("file")?;
let mime: Option<String> = value.raw_get("mime")?;
Ok(Self(yazi_config::plugin::SpotterMatcher {
spotters: YAZI.plugin.spotters.load_full(),
id: id.0,
file: file.map(|f| f.inner.clone().into()),
mime: mime.map(Into::into),
..Default::default()
}))
}
}
impl FromLua for SpotterMatcher {
fn from_lua(value: Value, _: &Lua) -> mlua::Result<Self> {
match value {
Value::Table(t) => t.try_into(),
_ => return Err("expected a table of SpotterMatcher".into_lua_err()),
}
}
}
impl IntoLua for SpotterMatcher {
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
Iter::new(self.0.into_iter().map(Spotter::new), None).into_lua(lua)
}
}

View file

@ -1,7 +1,7 @@
use mlua::{ExternalError, FromLua, IntoLua, Lua, MetaMethod, Table, UserData, UserDataMethods, Value};
use mlua::{IntoLua, MetaMethod, UserData, UserDataMethods};
use yazi_config::YAZI;
use crate::{FileRef, Id, Iter, config::Spotter};
use crate::config::SpotterMatcher;
pub struct Spotters;
@ -15,45 +15,3 @@ impl UserData for Spotters {
methods.add_meta_method(MetaMethod::Len, |_, _, ()| Ok(YAZI.plugin.spotters.load().len()));
}
}
// --- Matcher
pub struct SpotterMatcher(yazi_config::plugin::SpotterMatcher<'static>);
impl SpotterMatcher {
fn new(inner: impl Into<yazi_config::plugin::SpotterMatcher<'static>>) -> Self {
Self(inner.into())
}
}
impl TryFrom<Table> for SpotterMatcher {
type Error = mlua::Error;
fn try_from(value: Table) -> Result<Self, Self::Error> {
let id: Id = value.raw_get("id").unwrap_or_default();
let file: Option<FileRef> = value.raw_get("file")?;
let mime: Option<String> = value.raw_get("mime")?;
Ok(Self(yazi_config::plugin::SpotterMatcher {
spotters: YAZI.plugin.spotters.load_full(),
id: id.0,
file: file.map(|f| f.inner.clone().into()),
mime: mime.map(Into::into),
..Default::default()
}))
}
}
impl FromLua for SpotterMatcher {
fn from_lua(value: Value, _: &Lua) -> mlua::Result<Self> {
match value {
Value::Table(t) => t.try_into(),
_ => return Err("expected a table of SpotterMatcher".into_lua_err()),
}
}
}
impl IntoLua for SpotterMatcher {
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
Iter::new(self.0.into_iter().map(Spotter::new), None).into_lua(lua)
}
}

View file

@ -1,6 +1,6 @@
use std::ops::Deref;
use mlua::{ExternalError, FromLua, Lua, Table, UserData, Value};
use mlua::{FromLua, Lua, LuaSerdeExt, UserData, Value};
pub struct Selector(yazi_config::Selector);
@ -14,30 +14,8 @@ impl From<Selector> for yazi_config::Selector {
fn from(value: Selector) -> Self { value.0 }
}
impl TryFrom<&Table> for Selector {
type Error = mlua::Error;
fn try_from(t: &Table) -> Result<Self, Self::Error> {
Ok(Self(yazi_config::Selector::new(
t.raw_get::<Option<mlua::String>>("url")?.map(|s| s.to_str()?.parse()).transpose()?,
t.raw_get::<Option<mlua::String>>("mime")?.map(|s| s.to_str()?.parse()).transpose()?,
)?))
}
}
impl TryFrom<Table> for Selector {
type Error = mlua::Error;
fn try_from(value: Table) -> Result<Self, Self::Error> { Self::try_from(&value) }
}
impl FromLua for Selector {
fn from_lua(value: Value, _: &Lua) -> mlua::Result<Self> {
Ok(match value {
Value::Table(t) => t.try_into()?,
_ => Err("expected a table of Selector".into_lua_err())?,
})
}
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> { Ok(Self(lua.from_value(value)?)) }
}
impl UserData for Selector {}

View file

@ -68,20 +68,20 @@ impl Actions {
writeln!(s, " FZF_DEFAULT_COMMAND: {:?}", env::var_os("FZF_DEFAULT_COMMAND"))?;
writeln!(s, "\nText Opener")?;
#[rustfmt::skip]
writeln!(
s,
" default : {:?}",
YAZI.opener.first(YAZI.open.all_dummy(Path::new("f75a.txt"), "text/plain"))
s, " default : {:?}",
YAZI.open.match_dummy(Path::new("f75a.txt"), "text/plain").and_then(|r| YAZI.opener.first(&r))
)?;
#[rustfmt::skip]
writeln!(
s,
" block-create: {:?}",
YAZI.opener.block(YAZI.open.all_dummy(Path::new("bulk-create.txt"), "text/plain"))
s, " block-create: {:?}",
YAZI.open.match_dummy(Path::new("bulk-create.txt"), "text/plain").and_then(|r| YAZI.opener.block(&r))
)?;
#[rustfmt::skip]
writeln!(
s,
" block-rename: {:?}",
YAZI.opener.block(YAZI.open.all_dummy(Path::new("bulk-rename.txt"), "text/plain"))
s, " block-rename: {:?}",
YAZI.open.match_dummy(Path::new("bulk-rename.txt"), "text/plain").and_then(|r| YAZI.opener.block(&r))
)?;
writeln!(s, "\nMultiplexers")?;

View file

@ -17,5 +17,6 @@ proc-macro = true
[dependencies]
# External dependencies
syn = { version = "2.0.117", features = [ "full" ] }
quote = "1.0.45"
proc-macro2 = "1"
quote = "1.0.45"
syn = { version = "2.0.117", features = [ "full" ] }

View file

@ -0,0 +1,38 @@
use proc_macro2::Span;
use syn::{Attribute, Data, Field, Fields, GenericParam, Ident, Lifetime, LifetimeParam, Meta, punctuated::Punctuated, token::Comma};
pub(super) fn named_fields(data: Data) -> Punctuated<Field, Comma> {
match data {
Data::Struct(s) => match s.fields {
Fields::Named(f) => f.named,
_ => panic!("expected named fields"),
},
_ => panic!("expected struct"),
}
}
pub(super) fn ident_name(ident: &Ident) -> String {
let name = ident.to_string();
match name.strip_prefix("r#") {
Some(s) => s.to_owned(),
None => name,
}
}
pub(super) fn generics_with_de(generics: &syn::Generics) -> syn::Generics {
let mut g = generics.clone();
g.params.insert(
0,
GenericParam::Lifetime(LifetimeParam::new(Lifetime::new("'__de", Span::call_site()))),
);
g
}
pub(super) fn has_serde_attr(attrs: &[Attribute], name: &str) -> bool {
attrs.iter().any(|a| {
a.path().is_ident("serde")
&& a
.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated)
.map_or(false, |n| n.iter().any(|m| m.path().is_ident(name)))
})
}

View file

@ -1,6 +1,9 @@
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{Data, DeriveInput, Fields, parse_macro_input};
use quote::quote;
mod helper;
use syn::{DeriveInput, parse_macro_input};
use crate::helper::{generics_with_de, has_serde_attr, ident_name, named_fields};
#[proc_macro_derive(DeserializeOver)]
pub fn deserialize_over(input: TokenStream) -> TokenStream {
@ -18,47 +21,48 @@ pub fn deserialize_over1(input: TokenStream) -> TokenStream {
let DeriveInput { ident, generics, data, .. } = parse_macro_input!(input as DeriveInput);
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let assignments = match data {
Data::Struct(struct_) => match struct_.fields {
Fields::Named(fields) => {
let mut assignments = Vec::with_capacity(fields.named.len());
let visitor_generics = generics_with_de(&generics);
let (impl_visitor_generics, ..) = visitor_generics.split_for_impl();
for field in fields.named {
let field_ident = &field.ident;
let field_name = field_ident.as_ref().unwrap().to_string();
let fields: Vec<_> = named_fields(data).into_iter().map(|f| f.ident.unwrap()).collect();
let match_arms = fields.iter().map(|f| {
let name = ident_name(f);
quote! { #name => self.0.#f = map.next_value_seed(DeserializeOverSeed(self.0.#f))?, }
});
assignments.push(quote! {
if let Some(value) = table.remove(#field_name) {
if !matches!(value.get_ref(), toml::de::DeValue::Table(_)) {
_ = deserialize_spanned::<toml::Table>(value)?;
return Err(serde::de::Error::custom(format!("expected top-level `{}` to be a TOML table", #field_name)));
}
let span = value.span();
if let toml::de::DeValue::Table(table) = value.into_inner() {
self.#field_ident = self.#field_ident.deserialize_over_with(toml::Spanned::new(span, table))?;
}
}
self.#field_ident = self.#field_ident.deserialize_over_hook()?;
});
}
assignments
}
_ => panic!("DeserializeOver1 only supports structs with named fields"),
},
_ => panic!("DeserializeOver1 only supports structs"),
};
let hook_fields = fields.iter().map(|f| {
quote! { #f: deserialized.#f.deserialize_over_hook().map_err(Error::custom)? }
});
quote! {
impl #impl_generics yazi_shim::toml::DeserializeOverWith for #ident #ty_generics #where_clause {
fn deserialize_over_with<'de>(mut self, table: toml::Spanned<toml::de::DeTable<'de>>) -> Result<Self, toml::de::Error> {
use yazi_shim::toml::{DeserializeOverHook, DeserializeOverWith, deserialize_spanned};
fn deserialize_over_with<'__de, __D: serde::Deserializer<'__de>>(self, de: __D) -> Result<Self, __D::Error> {
use std::borrow::Cow;
use serde::de::{Error, IgnoredAny, MapAccess, Visitor};
use yazi_shim::toml::{DeserializeOverHook, DeserializeOverSeed};
let mut table = table.into_inner();
#(#assignments)*
struct V #impl_generics (#ident #ty_generics) #where_clause;
self.deserialize_over_hook()
impl #impl_visitor_generics Visitor<'__de> for V #ty_generics #where_clause {
type Value = #ident #ty_generics;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("a map")
}
fn visit_map<__M: MapAccess<'__de>>(mut self, mut map: __M) -> Result<Self::Value, __M::Error> {
while let Some(key) = map.next_key::<Cow<str>>()? {
match key.as_ref() {
#(#match_arms)*
_ => { map.next_value::<IgnoredAny>()?; }
}
}
Ok(self.0)
}
}
let deserialized = de.deserialize_map(V(self))?;
Ok(Self { #(#hook_fields,)* })
}
}
}
@ -70,47 +74,79 @@ pub fn deserialize_over2(input: TokenStream) -> TokenStream {
let DeriveInput { ident, generics, data, .. } = parse_macro_input!(input as DeriveInput);
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let assignments = match data {
Data::Struct(struct_) => match struct_.fields {
Fields::Named(fields) => {
let mut assignments = Vec::with_capacity(fields.named.len());
let visitor_generics = generics_with_de(&generics);
let (impl_visitor_generics, ..) = visitor_generics.split_for_impl();
for (index, field) in fields.named.into_iter().enumerate() {
let field_ident = field.ident;
let field_name = field_ident.as_ref().unwrap().to_string();
let field_ty = field.ty;
let field_attrs = field.attrs;
let field_helper = format_ident!("__DeserializeOver2Field{index}");
let (mut match_arms, mut post_fields) = (vec![], vec![]);
for field in named_fields(data) {
let (field_ident, field_ty) = (field.ident, field.ty);
let field_name = ident_name(field_ident.as_ref().unwrap());
assignments.push(quote! {
#[derive(serde::Deserialize)]
struct #field_helper #impl_generics (
#(#field_attrs)*
#field_ty
) #where_clause;
if has_serde_attr(&field.attrs, "skip") {
continue;
}
if let Some(value) = table.remove(#field_name) {
self.#field_ident = deserialize_spanned::<#field_helper #ty_generics>(value)?.0;
}
});
if field_name == "selector" && has_serde_attr(&field.attrs, "flatten") {
match_arms.push(quote! {
"url" => selector_url = Some(map.next_value()?),
"mime" => selector_mime = Some(map.next_value()?),
});
post_fields.push(quote! {
self.0.#field_ident = Selector::new(
selector_url.or(self.0.#field_ident.url),
selector_mime.or(self.0.#field_ident.mime)
).map_err(Error::custom)?;
});
continue;
}
let serde_attrs: Vec<_> = field.attrs.iter().filter(|a| a.path().is_ident("serde")).collect();
if serde_attrs.is_empty() {
match_arms.push(quote! { #field_name => self.0.#field_ident = map.next_value()?, });
} else {
match_arms.push(quote! {
#field_name => {
#[derive(serde::Deserialize)]
struct H #impl_generics(#(#serde_attrs)* #field_ty,) #where_clause;
self.0.#field_ident = map.next_value::<H #ty_generics>()?.0;
}
assignments
}
_ => panic!("DeserializeOver2 only supports structs with named fields"),
},
_ => panic!("DeserializeOver2 only supports structs"),
};
});
}
}
quote! {
impl #impl_generics yazi_shim::toml::DeserializeOverWith for #ident #ty_generics #where_clause {
fn deserialize_over_with<'de>(mut self, table: toml::Spanned<toml::de::DeTable<'de>>) -> Result<Self, toml::de::Error> {
use yazi_shim::toml::deserialize_spanned;
fn deserialize_over_with<'__de, __D: serde::Deserializer<'__de>>(self, de: __D) -> Result<Self, __D::Error> {
use std::borrow::Cow;
use serde::de::{Error, IgnoredAny, MapAccess, Visitor};
use crate::Selector;
let mut table = table.into_inner();
#(#assignments)*
struct V #impl_generics (#ident #ty_generics) #where_clause;
Ok(self)
impl #impl_visitor_generics Visitor<'__de> for V #ty_generics #where_clause {
type Value = #ident #ty_generics;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("a map")
}
fn visit_map<__M: MapAccess<'__de>>(mut self, mut map: __M) -> Result<Self::Value, __M::Error> {
let mut selector_url: Option<crate::Pattern> = None;
let mut selector_mime: Option<crate::Pattern> = None;
while let Some(key) = map.next_key::<Cow<str>>()? {
match key.as_ref() {
#(#match_arms)*
_ => { map.next_value::<IgnoredAny>()?; }
}
}
#(#post_fields)*
Ok(self.0)
}
}
de.deserialize_map(V(self))
}
}
}

View file

@ -53,7 +53,10 @@ fn try_init_flavor(light: bool, merge: bool) -> anyhow::Result<()> {
let flavor_str = theme::Flavor::from_theme(&theme, &theme_str)?.read(light)?;
preset = preset.deserialize_over(&flavor_str)?;
preset = error_with_input(preset.deserialize_over_with(theme), &theme_str)?;
preset = error_with_input(
preset.deserialize_over_with(toml::de::Deserializer::from(theme)),
&theme_str,
)?;
}
THEME.init(preset.reshape(light)?);

View file

@ -1 +1 @@
yazi_macro::mod_flat!(open rule);
yazi_macro::mod_flat!(open rule rules);

View file

@ -1,18 +1,18 @@
use std::ops::Deref;
use std::{ops::Deref, sync::Arc};
use anyhow::Result;
use indexmap::IndexSet;
use indexmap::IndexMap;
use serde::Deserialize;
use yazi_codegen::DeserializeOver2;
use yazi_fs::{File, cha::ChaType};
use yazi_shared::url::AsUrl;
use yazi_shim::toml::DeserializeOverHook;
use crate::{Selectable, mix, open::OpenRule};
use crate::{mix, open::{OpenRule, OpenRules}};
#[derive(Default, Deserialize, DeserializeOver2)]
pub struct Open {
rules: Vec<OpenRule>,
rules: OpenRules,
#[serde(default)]
prepend_rules: Vec<OpenRule>,
#[serde(default)]
@ -20,23 +20,13 @@ pub struct Open {
}
impl Deref for Open {
type Target = Vec<OpenRule>;
type Target = OpenRules;
fn deref(&self) -> &Self::Target { &self.rules }
}
impl Open {
pub fn all<'a>(&'a self, file: &File, mime: &str) -> impl Iterator<Item = &'a str> {
self
.rules
.iter()
.find(move |&rule| rule.matches(file, mime))
.into_iter()
.flat_map(|r| &r.r#use)
.map(String::as_str)
}
pub fn all_dummy<'a, U, M>(&'a self, url: U, mime: M) -> impl Iterator<Item = &'a str>
pub fn match_dummy<U, M>(&self, url: U, mime: M) -> Option<Arc<OpenRule>>
where
U: AsUrl,
M: AsRef<str>,
@ -47,30 +37,26 @@ impl Open {
Some(if mime.starts_with("folder/") { ChaType::Dir } else { ChaType::File }),
);
self
.rules
.iter()
.find(move |&rule| rule.matches(&file, mime))
.into_iter()
.flat_map(|r| &r.r#use)
.map(String::as_str)
self.matches(&file, mime)
}
pub fn common<'a>(&'a self, targets: &[(File, &str)]) -> IndexSet<&'a str> {
let each: Vec<IndexSet<&str>> = targets
.iter()
.map(|(file, mime)| self.all(file, mime).collect::<IndexSet<_>>())
.filter(|s| !s.is_empty())
.collect();
pub fn match_common(&self, targets: &[(File, &str)]) -> impl Iterator<Item = Arc<OpenRule>> {
let mut seen: IndexMap<Arc<OpenRule>, usize> = IndexMap::new();
for (file, mime) in targets {
if let Some(rule) = self.matches(file, mime) {
*seen.entry(rule).or_default() += 1;
}
}
let mut flat: IndexSet<_> = each.iter().flatten().copied().collect();
flat.retain(|use_| each.iter().all(|e| e.contains(use_)));
flat
seen.into_iter().filter(|&(_, count)| count == targets.len()).map(|(rule, _)| rule)
}
}
impl DeserializeOverHook for Open {
fn deserialize_over_hook(self) -> Result<Self, toml::de::Error> {
Ok(Self { rules: mix(self.prepend_rules, self.rules, self.append_rules), ..Default::default() })
let rules: Vec<Arc<OpenRule>> =
mix(self.prepend_rules, self.rules.unwrap_unchecked(), self.append_rules);
Ok(Self { rules: rules.into(), ..Default::default() })
}
}

View file

@ -1,17 +1,33 @@
use std::hash::{Hash, Hasher};
use serde::Deserialize;
use serde_with::{OneOrMany, formats::PreferOne, serde_as};
use yazi_codegen::DeserializeOver2;
use yazi_shared::Id;
use crate::{Mixable, Pattern, Selectable, Selector};
use crate::{Mixable, Pattern, Selectable, Selector, plugin::open_rule_id};
#[serde_as]
#[derive(Deserialize)]
#[derive(Clone, Debug, Deserialize, DeserializeOver2)]
pub struct OpenRule {
#[serde(skip, default = "open_rule_id")]
pub id: Id,
#[serde(flatten)]
pub selector: Selector,
#[serde_as(as = "OneOrMany<_, PreferOne>")]
pub r#use: Vec<String>,
}
impl PartialEq for OpenRule {
fn eq(&self, other: &Self) -> bool { self.id == other.id }
}
impl Eq for OpenRule {}
impl Hash for OpenRule {
fn hash<H: Hasher>(&self, state: &mut H) { self.id.hash(state); }
}
impl Selectable for OpenRule {
fn url_pat(&self) -> Option<&Pattern> { self.selector.url_pat() }

View file

@ -0,0 +1,133 @@
use std::{borrow::Cow, ops::Deref, sync::Arc};
use arc_swap::ArcSwap;
use serde::Deserialize;
use yazi_fs::File;
use yazi_shared::Id;
use yazi_shim::{arc_swap::{ArcSwapExt, IntoPointee}, vec::{IndexAtError, VecExt}};
use super::OpenRule;
use crate::{Selectable, mix};
#[derive(Debug, Default, Deserialize)]
pub struct OpenRules(ArcSwap<Vec<Arc<OpenRule>>>);
impl Deref for OpenRules {
type Target = ArcSwap<Vec<Arc<OpenRule>>>;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl From<Vec<Arc<OpenRule>>> for OpenRules {
fn from(inner: Vec<Arc<OpenRule>>) -> Self { Self(inner.into_pointee()) }
}
impl OpenRules {
pub fn matches(&self, file: &File, mime: &str) -> Option<Arc<OpenRule>> {
self.matcher(Some(file), Some(mime)).next()
}
pub fn matcher<'a, F, M>(&self, file: Option<F>, mime: Option<M>) -> OpenRuleMatcher<'a>
where
F: Into<Cow<'a, File>>,
M: Into<Cow<'a, str>>,
{
OpenRuleMatcher {
rules: self.0.load_full(),
file: file.map(Into::into),
mime: mime.map(Into::into),
..Default::default()
}
}
pub fn insert(&self, index: isize, rule: Arc<OpenRule>) -> Result<(), IndexAtError> {
self.0.try_rcu(|rules| {
let i = rules.index_at(index)?;
Ok(if i == rules.len() {
mix(Vec::<OpenRule>::new(), rules.iter().cloned(), [rule.clone()])
} else {
let (before, after) = rules.split_at(i);
mix(
Vec::<OpenRule>::new(),
before.iter().cloned().chain([rule.clone()]).chain(after.iter().cloned()),
Vec::<OpenRule>::new(),
)
})
})?;
Ok(())
}
pub fn remove(&self, matcher: OpenRuleMatcher) {
self.0.rcu(|rules| {
let mut next = Vec::clone(rules);
next.retain(|rule| !matcher.matches(rule));
next
});
}
pub fn update<E>(
&self,
matcher: OpenRuleMatcher,
f: impl Fn(OpenRule) -> Result<OpenRule, E>,
) -> Result<(), E> {
self.0.try_rcu(|rules| {
let mut next = Vec::clone(rules);
for rule in &mut next {
if matcher.matches(rule) {
*rule = f(OpenRule::clone(rule))?.into();
}
}
Ok(Arc::new(next))
})?;
Ok(())
}
pub(crate) fn unwrap_unchecked(self) -> Vec<Arc<OpenRule>> {
Arc::try_unwrap(self.0.into_inner()).expect("unique open rules arc")
}
}
// --- Matcher
#[derive(Default)]
pub struct OpenRuleMatcher<'a> {
pub rules: Arc<Vec<Arc<OpenRule>>>,
pub id: Id,
pub file: Option<Cow<'a, File>>,
pub mime: Option<Cow<'a, str>>,
pub all: bool,
pub offset: usize,
}
impl From<&OpenRules> for OpenRuleMatcher<'_> {
fn from(rules: &OpenRules) -> Self {
Self { rules: rules.load_full(), all: true, ..Default::default() }
}
}
impl OpenRuleMatcher<'_> {
pub fn matches(&self, rule: &OpenRule) -> bool {
if self.all {
true
} else if self.id != Id::ZERO {
rule.id == self.id
} else {
rule.match_with(self.file.as_deref(), self.mime.as_deref())
}
}
}
impl Iterator for OpenRuleMatcher<'_> {
type Item = Arc<OpenRule>;
fn next(&mut self) -> Option<Self::Item> {
while let Some(rule) = self.rules.get(self.offset) {
self.offset += 1;
if self.matches(rule) {
return Some(rule.clone());
}
}
None
}
}

View file

@ -1 +1 @@
yazi_macro::mod_flat!(opener rule);
yazi_macro::mod_flat!(opener rule rules);

View file

@ -1,64 +1,102 @@
use std::ops::Deref;
use std::{mem, ops::Deref, sync::Arc};
use arc_swap::ArcSwap;
use hashbrown::HashMap;
use serde::Deserialize;
use toml::{Spanned, de::DeTable};
use yazi_shim::toml::{DeserializeOverHook, DeserializeOverWith, deserialize_spanned};
use serde::{Deserialize, Deserializer, de::{MapAccess, Visitor}};
use yazi_shim::{arc_swap::IntoPointee, toml::{DeserializeOverHook, DeserializeOverWith}};
use super::OpenerRule;
use super::{OpenerRule, OpenerRules};
use crate::{open::OpenRule, opener::OpenerRuleMatcher};
#[derive(Debug, Deserialize)]
pub struct Opener(HashMap<String, Vec<OpenerRule>>);
pub struct Opener(ArcSwap<HashMap<String, Arc<OpenerRules>>>);
impl Deref for Opener {
type Target = HashMap<String, Vec<OpenerRule>>;
type Target = ArcSwap<HashMap<String, Arc<OpenerRules>>>;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl Opener {
pub fn all<'a, I>(&self, uses: I) -> impl Iterator<Item = &OpenerRule>
where
I: Iterator<Item = &'a str>,
{
uses.flat_map(|use_| self.get(use_)).flatten()
pub fn all(&self, open: Arc<OpenRule>) -> impl Iterator<Item = Arc<OpenerRule>> + use<> {
let inner = self.0.load_full();
(0..open.r#use.len())
.filter_map(move |i| inner.get(&open.r#use[i]).cloned())
.flat_map(|rules| OpenerRuleMatcher::from(&*rules))
}
pub fn first<'a, I>(&self, uses: I) -> Option<&OpenerRule>
where
I: Iterator<Item = &'a str>,
{
uses.flat_map(|use_| self.get(use_)).flatten().next()
pub fn first(&self, open: &OpenRule) -> Option<Arc<OpenerRule>> {
let inner = self.0.load();
open
.r#use
.iter()
.filter_map(|name| inner.get(name))
.find_map(|rules| rules.load().first().cloned())
}
pub fn block<'a, I>(&self, uses: I) -> Option<&OpenerRule>
where
I: Iterator<Item = &'a str>,
{
uses.flat_map(|use_| self.get(use_)).flatten().find(|&o| o.block)
pub fn block(&self, open: &OpenRule) -> Option<Arc<OpenerRule>> {
let inner = self.0.load();
open
.r#use
.iter()
.filter_map(|name| inner.get(name))
.find_map(|rules| rules.load().iter().find(|r| r.block).cloned())
}
pub fn insert(&self, name: &str, rules: &Arc<OpenerRules>) {
self.0.rcu(|inner| {
let mut next = HashMap::clone(inner);
next.insert(name.to_owned(), rules.clone());
next
});
}
pub fn remove(&self, name: &str) {
self.0.rcu(|inner| {
let mut next = HashMap::clone(inner);
next.remove(name);
next
});
}
pub(crate) fn unwrap_unchecked(self) -> HashMap<String, Arc<OpenerRules>> {
Arc::try_unwrap(self.0.into_inner()).expect("unique opener arc")
}
}
impl DeserializeOverHook for Opener {
fn deserialize_over_hook(mut self) -> Result<Self, toml::de::Error> {
for rules in self.0.values_mut() {
rules.retain(|r| r.r#for.matches());
rules.iter_mut().for_each(|r| r.fill());
fn deserialize_over_hook(self) -> Result<Self, toml::de::Error> {
let mut inner = self.unwrap_unchecked();
for mut rules in inner.values_mut() {
*rules = Arc::try_unwrap(mem::take(&mut rules))
.expect("unique opener value arc")
.deserialize_over_hook()?
.into();
}
Ok(self)
Ok(Self(inner.into_pointee()))
}
}
impl DeserializeOverWith for Opener {
fn deserialize_over_with<'de>(
mut self,
table: Spanned<DeTable<'de>>,
) -> Result<Self, toml::de::Error> {
for (key, value) in table.into_inner() {
self.0.insert(key.into_inner().into_owned(), deserialize_spanned(value)?);
fn deserialize_over_with<'de, D: Deserializer<'de>>(self, de: D) -> Result<Self, D::Error> {
struct V(HashMap<String, Arc<OpenerRules>>);
impl<'de> Visitor<'de> for V {
type Value = Opener;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("a map of opener rules")
}
fn visit_map<M: MapAccess<'de>>(mut self, mut map: M) -> Result<Self::Value, M::Error> {
while let Some(key) = map.next_key()? {
self.0.insert(key, map.next_value()?);
}
Ok(Opener(self.0.into_pointee()))
}
}
Ok(self)
de.deserialize_map(V(self.unwrap_unchecked()))
}
}

View file

@ -1,11 +1,15 @@
use std::sync::Arc;
use serde::Deserialize;
use yazi_fs::Splatter;
use yazi_shared::NonEmptyString;
use yazi_shared::{Id, NonEmptyString};
use crate::Platform;
use crate::{Platform, opener::OpenerRules, plugin::opener_rule_id};
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct OpenerRule {
#[serde(skip, default = "opener_rule_id")]
pub id: Id,
pub run: NonEmptyString,
#[serde(default)]
pub block: bool,
@ -30,7 +34,7 @@ impl OpenerRule {
}
}
pub(super) fn fill(&mut self) {
pub fn fill(&mut self) {
#[cfg(unix)]
{
self.spread =
@ -42,3 +46,44 @@ impl OpenerRule {
}
}
}
// --- Matcher
#[derive(Default)]
pub struct OpenerRuleMatcher {
pub rules: Arc<Vec<Arc<OpenerRule>>>,
pub id: Id,
pub all: bool,
pub offset: usize,
}
impl From<&OpenerRules> for OpenerRuleMatcher {
fn from(rules: &OpenerRules) -> Self {
Self { rules: rules.load_full(), all: true, ..Default::default() }
}
}
impl OpenerRuleMatcher {
pub fn matches(&self, rule: &OpenerRule) -> bool {
if self.all {
true
} else if self.id != Id::ZERO {
rule.id == self.id
} else {
false
}
}
}
impl Iterator for OpenerRuleMatcher {
type Item = Arc<OpenerRule>;
fn next(&mut self) -> Option<Self::Item> {
while let Some(rule) = self.rules.get(self.offset) {
self.offset += 1;
if self.matches(rule) {
return Some(rule.clone());
}
}
None
}
}

View file

@ -0,0 +1,86 @@
use std::{mem, ops::Deref, sync::Arc};
use arc_swap::ArcSwap;
use hashbrown::{HashMap, hash_map};
use serde::Deserialize;
use yazi_shim::{arc_swap::{ArcSwapExt, IntoPointee}, toml::DeserializeOverHook, vec::{IndexAtError, VecExt}};
use super::OpenerRule;
use crate::opener::{Opener, OpenerRuleMatcher};
#[derive(Debug, Default, Deserialize)]
pub struct OpenerRules(ArcSwap<Vec<Arc<OpenerRule>>>);
impl Deref for OpenerRules {
type Target = ArcSwap<Vec<Arc<OpenerRule>>>;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl From<Vec<Arc<OpenerRule>>> for OpenerRules {
fn from(inner: Vec<Arc<OpenerRule>>) -> Self { Self(inner.into_pointee()) }
}
impl OpenerRules {
pub fn insert(&self, index: isize, rule: Arc<OpenerRule>) -> Result<(), IndexAtError> {
self.0.try_rcu(|rules| {
let (before, after) = rules.split_at(rules.index_at(index)?);
Ok(
before
.iter()
.cloned()
.chain([rule.clone()])
.chain(after.iter().cloned())
.collect::<Vec<_>>(),
)
})?;
Ok(())
}
pub fn remove(&self, matcher: OpenerRuleMatcher) {
self.0.rcu(|rules| {
let mut next = Vec::clone(rules);
next.retain(|rule| !matcher.matches(rule));
next
});
}
pub(crate) fn unwrap_unchecked(self) -> Vec<Arc<OpenerRule>> {
Arc::try_unwrap(self.0.into_inner()).expect("unique opener rules arc")
}
}
impl DeserializeOverHook for OpenerRules {
fn deserialize_over_hook(self) -> Result<Self, toml::de::Error> {
let mut inner = self.unwrap_unchecked();
inner.retain(|r| r.r#for.matches());
inner.iter_mut().for_each(|r| Arc::get_mut(r).expect("unique opener rule arc").fill());
Ok(Self(inner.into_pointee()))
}
}
// --- Matcher
pub struct OpenerRulesMatcher {
iter: hash_map::Iter<'static, String, Arc<OpenerRules>>,
_opener: Arc<HashMap<String, Arc<OpenerRules>>>,
}
impl From<&Opener> for OpenerRulesMatcher {
fn from(opener: &Opener) -> Self {
let opener = opener.load_full();
let iter: hash_map::Iter<String, Arc<OpenerRules>> = opener.iter();
Self { iter: unsafe { mem::transmute(iter) }, _opener: opener }
}
}
impl Iterator for OpenerRulesMatcher {
type Item = (String, Arc<OpenerRules>);
fn next(&mut self) -> Option<Self::Item> {
self.iter.next().map(|(name, rules)| (name.clone(), rules.clone()))
}
}

View file

@ -7,7 +7,7 @@ use yazi_shared::{scheme::SchemeKind, url::AsUrl};
use crate::Mixable;
#[derive(Deserialize)]
#[derive(Clone, Deserialize)]
#[serde(try_from = "String")]
pub struct Pattern {
inner: globset::GlobMatcher,

View file

@ -1,9 +1,11 @@
use std::ops::Deref;
use std::{borrow::Cow, ops::Deref, sync::Arc};
use hashbrown::HashSet;
use serde::Deserialize;
use yazi_fs::File;
use yazi_shared::{Id, event::Action};
use crate::{Mixable, Pattern, Priority, Selectable, Selector, plugin::fetcher_id};
use crate::{Mixable, Pattern, Priority, Selectable, Selector, plugin::{Fetchers, fetcher_id}};
#[derive(Debug, Deserialize)]
pub struct Fetcher {
@ -32,3 +34,50 @@ impl Selectable for Fetcher {
}
impl Mixable for Fetcher {}
// --- Matcher
#[derive(Default)]
pub struct FetcherMatcher<'a> {
pub fetchers: Arc<Vec<Arc<Fetcher>>>,
pub id: Id,
pub file: Option<Cow<'a, File>>,
pub mime: Option<Cow<'a, str>>,
pub all: bool,
pub offset: usize,
pub seen: HashSet<String>,
}
impl From<&Fetchers> for FetcherMatcher<'_> {
fn from(fetchers: &Fetchers) -> Self {
Self { fetchers: fetchers.load_full(), all: true, ..Default::default() }
}
}
impl FetcherMatcher<'_> {
pub fn matches(&self, fetcher: &Fetcher) -> bool {
if self.all {
true
} else if self.id != Id::ZERO {
fetcher.id == self.id
} else {
fetcher.match_with(self.file.as_deref(), self.mime.as_deref())
}
}
}
impl Iterator for FetcherMatcher<'_> {
type Item = Arc<Fetcher>;
fn next(&mut self) -> Option<Self::Item> {
while let Some(fetcher) = self.fetchers.get(self.offset) {
self.offset += 1;
if !self.matches(fetcher) {
continue;
}
if self.all || self.seen.insert(fetcher.group.clone()) {
return Some(fetcher.clone());
}
}
None
}
}

View file

@ -1,15 +1,13 @@
use std::{borrow::Cow, ops::Deref, sync::Arc};
use arc_swap::ArcSwap;
use hashbrown::HashSet;
use serde::Deserialize;
use tracing::warn;
use yazi_fs::File;
use yazi_shared::Id;
use yazi_shim::arc_swap::IntoPointee;
use super::{Fetcher, MAX_FETCHERS};
use crate::Selectable;
use crate::plugin::FetcherMatcher;
#[derive(Debug, Default, Deserialize)]
pub struct Fetchers(ArcSwap<Vec<Arc<Fetcher>>>);
@ -68,50 +66,3 @@ impl Fetchers {
Arc::try_unwrap(self.0.into_inner()).expect("unique fetchers arc")
}
}
// --- Matcher
#[derive(Default)]
pub struct FetcherMatcher<'a> {
pub fetchers: Arc<Vec<Arc<Fetcher>>>,
pub id: Id,
pub file: Option<Cow<'a, File>>,
pub mime: Option<Cow<'a, str>>,
pub all: bool,
pub offset: usize,
pub seen: HashSet<String>,
}
impl From<&Fetchers> for FetcherMatcher<'_> {
fn from(fetchers: &Fetchers) -> Self {
Self { fetchers: fetchers.load_full(), all: true, ..Default::default() }
}
}
impl FetcherMatcher<'_> {
pub fn matches(&self, fetcher: &Fetcher) -> bool {
if self.all {
true
} else if self.id != Id::ZERO {
fetcher.id == self.id
} else {
fetcher.match_with(self.file.as_deref(), self.mime.as_deref())
}
}
}
impl Iterator for FetcherMatcher<'_> {
type Item = Arc<Fetcher>;
fn next(&mut self) -> Option<Self::Item> {
while let Some(fetcher) = self.fetchers.get(self.offset) {
self.offset += 1;
if !self.matches(fetcher) {
continue;
}
if self.all || self.seen.insert(fetcher.group.clone()) {
return Some(fetcher.clone());
}
}
None
}
}

View file

@ -19,3 +19,13 @@ pub fn spotter_id() -> Id {
static IDS: Ids = Ids::new();
IDS.next()
}
pub fn open_rule_id() -> Id {
static IDS: Ids = Ids::new();
IDS.next()
}
pub fn opener_rule_id() -> Id {
static IDS: Ids = Ids::new();
IDS.next()
}

View file

@ -1,9 +1,10 @@
use std::ops::Deref;
use std::{borrow::Cow, ops::Deref, sync::Arc};
use serde::Deserialize;
use yazi_fs::File;
use yazi_shared::{Id, event::Action};
use crate::{Mixable, Pattern, Priority, Selectable, Selector, plugin::preloader_id};
use crate::{Mixable, Pattern, Priority, Selectable, Selector, plugin::{Preloaders, preloader_id}};
#[derive(Debug, Deserialize)]
pub struct Preloader {
@ -33,3 +34,52 @@ impl Selectable for Preloader {
}
impl Mixable for Preloader {}
// --- Matcher
#[derive(Default)]
pub struct PreloaderMatcher<'a> {
pub preloaders: Arc<Vec<Arc<Preloader>>>,
pub id: Id,
pub file: Option<Cow<'a, File>>,
pub mime: Option<Cow<'a, str>>,
pub all: bool,
pub offset: usize,
pub stop: bool,
}
impl From<&Preloaders> for PreloaderMatcher<'_> {
fn from(preloaders: &Preloaders) -> Self {
Self { preloaders: preloaders.load_full(), all: true, ..Default::default() }
}
}
impl PreloaderMatcher<'_> {
pub fn matches(&self, preloader: &Preloader) -> bool {
if self.all {
true
} else if self.id != Id::ZERO {
preloader.id == self.id
} else {
preloader.match_with(self.file.as_deref(), self.mime.as_deref())
}
}
}
impl Iterator for PreloaderMatcher<'_> {
type Item = Arc<Preloader>;
fn next(&mut self) -> Option<Self::Item> {
if self.stop && !self.all {
return None;
}
while let Some(preloader) = self.preloaders.get(self.offset) {
self.offset += 1;
if self.matches(preloader) {
self.stop = !preloader.next;
return Some(preloader.clone());
}
}
None
}
}

View file

@ -3,11 +3,10 @@ use std::{borrow::Cow, ops::Deref, sync::Arc};
use arc_swap::ArcSwap;
use serde::Deserialize;
use yazi_fs::File;
use yazi_shared::Id;
use yazi_shim::arc_swap::IntoPointee;
use super::Preloader;
use crate::Selectable;
use crate::plugin::PreloaderMatcher;
#[derive(Debug, Default, Deserialize)]
pub struct Preloaders(ArcSwap<Vec<Arc<Preloader>>>);
@ -48,52 +47,3 @@ impl Preloaders {
Arc::try_unwrap(self.0.into_inner()).expect("unique preloaders arc")
}
}
// --- Matcher
#[derive(Default)]
pub struct PreloaderMatcher<'a> {
pub preloaders: Arc<Vec<Arc<Preloader>>>,
pub id: Id,
pub file: Option<Cow<'a, File>>,
pub mime: Option<Cow<'a, str>>,
pub all: bool,
pub offset: usize,
pub stop: bool,
}
impl From<&Preloaders> for PreloaderMatcher<'_> {
fn from(preloaders: &Preloaders) -> Self {
Self { preloaders: preloaders.load_full(), all: true, ..Default::default() }
}
}
impl PreloaderMatcher<'_> {
pub fn matches(&self, preloader: &Preloader) -> bool {
if self.all {
true
} else if self.id != Id::ZERO {
preloader.id == self.id
} else {
preloader.match_with(self.file.as_deref(), self.mime.as_deref())
}
}
}
impl Iterator for PreloaderMatcher<'_> {
type Item = Arc<Preloader>;
fn next(&mut self) -> Option<Self::Item> {
if self.stop && !self.all {
return None;
}
while let Some(preloader) = self.preloaders.get(self.offset) {
self.offset += 1;
if self.matches(preloader) {
self.stop = !preloader.next;
return Some(preloader.clone());
}
}
None
}
}

View file

@ -1,9 +1,10 @@
use std::ops::Deref;
use std::{borrow::Cow, ops::Deref, sync::Arc};
use serde::Deserialize;
use yazi_fs::File;
use yazi_shared::{Id, event::Action};
use crate::{Mixable, Pattern, Selectable, Selector, plugin::previewer_id};
use crate::{Mixable, Pattern, Selectable, Selector, plugin::{Previewers, previewer_id}};
#[derive(Debug, Deserialize)]
pub struct Previewer {
@ -32,3 +33,46 @@ impl Mixable for Previewer {
fn any_dir(&self) -> bool { self.selector.any_dir() }
}
// --- Matcher
#[derive(Default)]
pub struct PreviewerMatcher<'a> {
pub previewers: Arc<Vec<Arc<Previewer>>>,
pub id: Id,
pub file: Option<Cow<'a, File>>,
pub mime: Option<Cow<'a, str>>,
pub all: bool,
pub offset: usize,
}
impl From<&Previewers> for PreviewerMatcher<'_> {
fn from(previewers: &Previewers) -> Self {
Self { previewers: previewers.load_full(), all: true, ..Default::default() }
}
}
impl PreviewerMatcher<'_> {
pub fn matches(&self, previewer: &Previewer) -> bool {
if self.all {
true
} else if self.id != Id::ZERO {
previewer.id == self.id
} else {
previewer.match_with(self.file.as_deref(), self.mime.as_deref())
}
}
}
impl Iterator for PreviewerMatcher<'_> {
type Item = Arc<Previewer>;
fn next(&mut self) -> Option<Self::Item> {
while let Some(previewer) = self.previewers.get(self.offset) {
self.offset += 1;
if self.matches(previewer) {
return Some(previewer.clone());
}
}
None
}
}

View file

@ -3,11 +3,10 @@ use std::{borrow::Cow, ops::Deref, sync::Arc};
use arc_swap::ArcSwap;
use serde::Deserialize;
use yazi_fs::File;
use yazi_shared::Id;
use yazi_shim::{arc_swap::IntoPointee, vec::{IndexAtError, VecExt}};
use yazi_shim::{arc_swap::{ArcSwapExt, IntoPointee}, vec::{IndexAtError, VecExt}};
use super::Previewer;
use crate::{Selectable, mix};
use crate::{mix, plugin::PreviewerMatcher};
#[derive(Debug, Default, Deserialize)]
pub struct Previewers(ArcSwap<Vec<Arc<Previewer>>>);
@ -41,27 +40,21 @@ impl Previewers {
}
pub fn insert(&self, index: isize, previewer: Arc<Previewer>) -> Result<(), IndexAtError> {
let mut err = None;
self.0.rcu(|previewers| match previewers.index_at(index) {
Ok(n) if n == previewers.len() => {
mix(Vec::<Previewer>::new(), previewers.iter().cloned(), [previewer.clone()])
}
Ok(n) => {
let (before, after) = previewers.split_at(n);
mix(
self.0.try_rcu(|previewers| {
let i = previewers.index_at(index)?;
if i == previewers.len() {
Ok(mix(Vec::<Previewer>::new(), previewers.iter().cloned(), [previewer.clone()]))
} else {
let (before, after) = previewers.split_at(i);
Ok(mix(
Vec::<Previewer>::new(),
before.iter().cloned().chain([previewer.clone()]).chain(after.iter().cloned()),
Vec::<Previewer>::new(),
)
))
}
Err(e) => {
err = Some(e);
Vec::clone(previewers)
}
});
})?;
err.map_or(Ok(()), Err)
Ok(())
}
pub fn remove(&self, matcher: PreviewerMatcher) {
@ -76,46 +69,3 @@ impl Previewers {
Arc::try_unwrap(self.0.into_inner()).expect("unique previewers arc")
}
}
// --- Matcher
#[derive(Default)]
pub struct PreviewerMatcher<'a> {
pub previewers: Arc<Vec<Arc<Previewer>>>,
pub id: Id,
pub file: Option<Cow<'a, File>>,
pub mime: Option<Cow<'a, str>>,
pub all: bool,
pub offset: usize,
}
impl From<&Previewers> for PreviewerMatcher<'_> {
fn from(previewers: &Previewers) -> Self {
Self { previewers: previewers.load_full(), all: true, ..Default::default() }
}
}
impl PreviewerMatcher<'_> {
pub fn matches(&self, previewer: &Previewer) -> bool {
if self.all {
true
} else if self.id != Id::ZERO {
previewer.id == self.id
} else {
previewer.match_with(self.file.as_deref(), self.mime.as_deref())
}
}
}
impl Iterator for PreviewerMatcher<'_> {
type Item = Arc<Previewer>;
fn next(&mut self) -> Option<Self::Item> {
while let Some(previewer) = self.previewers.get(self.offset) {
self.offset += 1;
if self.matches(previewer) {
return Some(previewer.clone());
}
}
None
}
}

View file

@ -1,9 +1,10 @@
use std::ops::Deref;
use std::{borrow::Cow, ops::Deref, sync::Arc};
use serde::Deserialize;
use yazi_fs::File;
use yazi_shared::{Id, event::Action};
use crate::{Mixable, Pattern, Selectable, Selector, plugin::spotter_id};
use crate::{Mixable, Pattern, Selectable, Selector, plugin::{Spotters, spotter_id}};
#[derive(Debug, Deserialize)]
pub struct Spotter {
@ -32,3 +33,46 @@ impl Mixable for Spotter {
fn any_dir(&self) -> bool { self.selector.any_dir() }
}
// --- Matcher
#[derive(Default)]
pub struct SpotterMatcher<'a> {
pub spotters: Arc<Vec<Arc<Spotter>>>,
pub id: Id,
pub file: Option<Cow<'a, File>>,
pub mime: Option<Cow<'a, str>>,
pub all: bool,
pub offset: usize,
}
impl From<&Spotters> for SpotterMatcher<'_> {
fn from(spotters: &Spotters) -> Self {
Self { spotters: spotters.load_full(), all: true, ..Default::default() }
}
}
impl SpotterMatcher<'_> {
pub fn matches(&self, spotter: &Spotter) -> bool {
if self.all {
true
} else if self.id != Id::ZERO {
spotter.id == self.id
} else {
spotter.match_with(self.file.as_deref(), self.mime.as_deref())
}
}
}
impl Iterator for SpotterMatcher<'_> {
type Item = Arc<Spotter>;
fn next(&mut self) -> Option<Self::Item> {
while let Some(spotter) = self.spotters.get(self.offset) {
self.offset += 1;
if self.matches(spotter) {
return Some(spotter.clone());
}
}
None
}
}

View file

@ -7,7 +7,7 @@ use yazi_shared::Id;
use yazi_shim::arc_swap::IntoPointee;
use super::Spotter;
use crate::Selectable;
use crate::plugin::SpotterMatcher;
#[derive(Debug, Default, Deserialize)]
pub struct Spotters(ArcSwap<Vec<Arc<Spotter>>>);
@ -45,46 +45,3 @@ impl Spotters {
Arc::try_unwrap(self.0.into_inner()).expect("unique spotters arc")
}
}
// --- Matcher
#[derive(Default)]
pub struct SpotterMatcher<'a> {
pub spotters: Arc<Vec<Arc<Spotter>>>,
pub id: Id,
pub file: Option<Cow<'a, File>>,
pub mime: Option<Cow<'a, str>>,
pub all: bool,
pub offset: usize,
}
impl From<&Spotters> for SpotterMatcher<'_> {
fn from(spotters: &Spotters) -> Self {
Self { spotters: spotters.load_full(), all: true, ..Default::default() }
}
}
impl SpotterMatcher<'_> {
pub fn matches(&self, spotter: &Spotter) -> bool {
if self.all {
true
} else if self.id != Id::ZERO {
spotter.id == self.id
} else {
spotter.match_with(self.file.as_deref(), self.mime.as_deref())
}
}
}
impl Iterator for SpotterMatcher<'_> {
type Item = Arc<Spotter>;
fn next(&mut self) -> Option<Self::Item> {
while let Some(spotter) = self.spotters.get(self.offset) {
self.offset += 1;
if self.matches(spotter) {
return Some(spotter.clone());
}
}
None
}
}

View file

@ -3,10 +3,10 @@ use serde::{Deserialize, Deserializer, de};
use crate::{Mixable, Pattern, Selectable};
#[derive(Debug)]
#[derive(Clone, Debug)]
pub struct Selector {
url: Option<Pattern>,
mime: Option<Pattern>,
pub url: Option<Pattern>,
pub mime: Option<Pattern>,
}
impl<'de> Deserialize<'de> for Selector {

View file

@ -2,10 +2,9 @@ use std::ops::Deref;
use anyhow::Result;
use hashbrown::HashMap;
use serde::{Deserialize, Serialize, de};
use toml::{Spanned, de::DeTable};
use serde::{Deserialize, Deserializer, Serialize, de, de::{MapAccess, Visitor}};
use yazi_codegen::DeserializeOver;
use yazi_shim::toml::{DeserializeOverWith, deserialize_spanned};
use yazi_shim::toml::DeserializeOverWith;
use crate::vfs::Service;
@ -19,21 +18,31 @@ impl Deref for Services {
}
impl DeserializeOverWith for Services {
fn deserialize_over_with<'de>(
mut self,
table: Spanned<DeTable<'de>>,
) -> Result<Self, toml::de::Error> {
for (key, value) in table.into_inner() {
let key = key.into_inner();
if key.is_empty() || key.len() > 20 {
Err(de::Error::custom(format!("VFS name `{key}` must be between 1 and 20 characters")))?;
} else if !key.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'-')) {
Err(de::Error::custom(format!("VFS name `{key}` must be in kebab-case")))?;
} else {
self.0.insert(key.into_owned(), deserialize_spanned(value)?);
fn deserialize_over_with<'de, D: Deserializer<'de>>(self, de: D) -> Result<Self, D::Error> {
struct V(Services);
impl<'de> Visitor<'de> for V {
type Value = Services;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("a map of VFS services")
}
fn visit_map<M: MapAccess<'de>>(mut self, mut map: M) -> Result<Self::Value, M::Error> {
while let Some(key) = map.next_key::<String>()? {
if key.is_empty() || key.len() > 20 {
return Err(de::Error::custom(format!(
"VFS name `{key}` must be between 1 and 20 characters"
)));
} else if !key.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'-')) {
return Err(de::Error::custom(format!("VFS name `{key}` must be in kebab-case")));
}
self.0.0.insert(key, map.next_value()?);
}
Ok(self.0)
}
}
Ok(self)
de.deserialize_map(V(self))
}
}

View file

@ -62,7 +62,7 @@ scopeguard = { workspace = true }
tokio = { workspace = true }
tokio-stream = { workspace = true }
tracing = { workspace = true }
tracing-appender = "0.2.4"
tracing-appender = "0.2.5"
tracing-subscriber = { version = "0.3.23", features = [ "env-filter" ] }
[target."cfg(unix)".dependencies]

View file

@ -1,5 +1,5 @@
use mlua::{IntoLua, Lua, LuaSerdeExt, Value};
use yazi_binding::{Composer, ComposerGet, ComposerSet, SER_OPT, Url, elements::Wrap};
use yazi_binding::{Composer, ComposerGet, ComposerSet, SER_OPT, Url, config::{OpenRules, Opener}, elements::Wrap};
use yazi_boot::ARGS;
use yazi_config::YAZI;
@ -9,6 +9,8 @@ pub fn compose() -> Composer<ComposerGet, ComposerSet> {
b"args" => args().into_lua(lua)?,
b"term" => super::term().into_lua(lua)?,
b"mgr" => mgr().into_lua(lua)?,
b"open" => open().into_lua(lua)?,
b"opener" => Opener.into_lua(lua)?,
b"plugin" => super::plugin().into_lua(lua)?,
b"preview" => preview().into_lua(lua)?,
b"tasks" => tasks().into_lua(lua)?,
@ -22,6 +24,19 @@ pub fn compose() -> Composer<ComposerGet, ComposerSet> {
Composer::new(get, set)
}
fn open() -> Composer<ComposerGet, ComposerSet> {
fn get(lua: &Lua, key: &[u8]) -> mlua::Result<Value> {
match key {
b"rules" => OpenRules.into_lua(lua),
_ => Ok(Value::Nil),
}
}
fn set(_: &Lua, _: &[u8], value: Value) -> mlua::Result<Value> { Ok(value) }
Composer::new(get, set)
}
fn args() -> Composer<ComposerGet, ComposerSet> {
fn get(lua: &Lua, key: &[u8]) -> mlua::Result<Value> {
match key {

View file

@ -28,6 +28,7 @@ parking_lot = { workspace = true }
paste = { workspace = true }
percent-encoding = { workspace = true }
serde = { workspace = true }
serde_with = { workspace = true }
strum = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }

View file

@ -1,7 +1,7 @@
use std::str::FromStr;
use anyhow::bail;
use serde::{Deserialize, Deserializer, de};
use serde_with::DeserializeFromStr;
#[derive(Debug, PartialEq, Eq)]
pub enum ConditionOp {
@ -44,7 +44,7 @@ impl ConditionOp {
}
}
#[derive(Debug)]
#[derive(Debug, DeserializeFromStr)]
pub struct Condition {
ops: Vec<ConditionOp>,
}
@ -62,16 +62,6 @@ impl FromStr for Condition {
}
}
impl<'de> Deserialize<'de> for Condition {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
FromStr::from_str(&s).map_err(de::Error::custom)
}
}
impl Condition {
fn build(expr: &str) -> Self {
let mut stack: Vec<ConditionOp> = vec![];

View file

@ -2,11 +2,11 @@ use std::{borrow::Cow, fmt::{self, Display}, mem, str::FromStr};
use anyhow::{Result, anyhow, bail};
use hashbrown::HashMap;
use serde::{Deserialize, de};
use serde_with::DeserializeFromStr;
use crate::{Layer, SStr, Source, data::{Data, DataAny, DataKey}, event::Replier};
#[derive(Clone, Debug, Default)]
#[derive(Clone, Debug, Default, DeserializeFromStr)]
pub struct Action {
pub name: SStr,
pub args: HashMap<DataKey, Data>,
@ -281,12 +281,3 @@ impl FromStr for Action {
Ok(me)
}
}
impl<'de> Deserialize<'de> for Action {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
<_>::from_str(&String::deserialize(deserializer)?).map_err(de::Error::custom)
}
}

View file

@ -18,6 +18,7 @@ yazi-macro = { path = "../yazi-macro", version = "26.2.2" }
# External dependencies
arc-swap = { workspace = true }
crossterm = { workspace = true }
mlua = { workspace = true }
ratatui = { workspace = true }
serde = { workspace = true }
thiserror = { workspace = true }

View file

@ -1,4 +1,4 @@
use arc_swap::ArcSwap;
use arc_swap::{ArcSwap, ArcSwapAny, Guard, RefCnt, strategy::CaS};
// --- IntoPointee
pub trait IntoPointee: Sized {
@ -9,3 +9,34 @@ impl<T> IntoPointee for T {
#[inline]
fn into_pointee(self) -> ArcSwap<Self> { ArcSwap::from_pointee(self) }
}
// --- ArcSwapExt
pub trait ArcSwapExt<T: RefCnt> {
fn try_rcu<R, E, F>(&self, f: F) -> Result<T, E>
where
F: FnMut(&T) -> Result<R, E>,
R: Into<T>;
}
impl<T, S> ArcSwapExt<T> for ArcSwapAny<T, S>
where
T: RefCnt,
S: CaS<T>,
{
fn try_rcu<R, E, F>(&self, mut f: F) -> Result<T, E>
where
F: FnMut(&T) -> Result<R, E>,
R: Into<T>,
{
let mut cur = self.load();
loop {
let new = f(&cur)?.into();
let prev = self.compare_and_swap(&*cur, new);
if T::as_ptr(&*cur) == T::as_ptr(&*prev) {
return Ok(Guard::into_inner(prev));
}
cur = prev;
}
}
}

View file

@ -1,3 +1,3 @@
yazi_macro::mod_pub!(arc_swap crossterm ratatui strum toml vec);
yazi_macro::mod_pub!(arc_swap crossterm mlua ratatui strum toml vec);
yazi_macro::mod_flat!(twox);

View file

@ -0,0 +1 @@
yazi_macro::mod_flat!(traits);

View file

@ -0,0 +1,12 @@
use mlua::{ExternalError, Table, Value};
use crate::toml::DeserializeOverWith;
pub trait DeserializeOverLua: DeserializeOverWith {
fn deserialize_over_lua(self, table: &Table) -> mlua::Result<Self> {
let de = mlua::serde::Deserializer::new(Value::Table(table.clone()));
self.deserialize_over_with(de).map_err(|e| e.into_lua_err())
}
}
impl<T: DeserializeOverWith> DeserializeOverLua for T {}

View file

@ -1,20 +1,36 @@
use toml::{Spanned, de::{self, DeTable}};
use serde::{Deserializer, de::DeserializeSeed};
use toml::de;
pub trait DeserializeOver: DeserializeOverWith + DeserializeOverHook {
fn deserialize_over(self, input: &str) -> Result<Self, de::Error> {
self.deserialize_over_with(DeTable::parse(input)?).map_err(|mut err| {
let table = de::DeTable::parse(input)?;
let de = de::Deserializer::from(table);
self.deserialize_over_with(de).map_err(|mut err| {
err.set_input(Some(input));
err
})
}
}
impl<T> DeserializeOver for T where T: DeserializeOverWith + DeserializeOverHook {}
// --- DeserializeOverWith
pub trait DeserializeOverWith: Sized {
fn deserialize_over_with<'de>(self, table: Spanned<DeTable<'de>>) -> Result<Self, de::Error>;
fn deserialize_over_with<'de, D: Deserializer<'de>>(self, de: D) -> Result<Self, D::Error>;
}
// --- DeserializeOverHook
pub trait DeserializeOverHook: Sized {
fn deserialize_over_hook(self) -> Result<Self, de::Error> { Ok(self) }
}
impl<T> DeserializeOver for T where T: DeserializeOverWith + DeserializeOverHook {}
// --- DeserializeOverSeed
pub struct DeserializeOverSeed<T>(pub T);
impl<'de, T: DeserializeOverWith> DeserializeSeed<'de> for DeserializeOverSeed<T> {
type Value = T;
fn deserialize<D: Deserializer<'de>>(self, de: D) -> Result<T, D::Error> {
self.0.deserialize_over_with(de)
}
}

View file

@ -56,20 +56,6 @@ impl<'de> Deserialize<'de> for Step {
value.parse().map_err(E::custom)
}
fn visit_borrowed_str<E>(self, value: &'_ str) -> Result<Self::Value, E>
where
E: de::Error,
{
self.visit_str(value)
}
fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
where
E: de::Error,
{
self.visit_str(&value)
}
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
where
E: de::Error,