diff --git a/CHANGELOG.md b/CHANGELOG.md index a123698a..7f5a10ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Cargo.lock b/Cargo.lock index e0b5c110..02671f1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index d66c7f49..45da0f1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/yazi-actor/Cargo.toml b/yazi-actor/Cargo.toml index 747dd84a..4dc090d0 100644 --- a/yazi-actor/Cargo.toml +++ b/yazi-actor/Cargo.toml @@ -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 } diff --git a/yazi-actor/src/mgr/bulk_rename.rs b/yazi-actor/src/mgr/bulk_rename.rs index 938b2e1c..c581140b 100644 --- a/yazi-actor/src/mgr/bulk_rename.rs +++ b/yazi-actor/src/mgr/bulk_rename.rs @@ -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> { + 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 { diff --git a/yazi-actor/src/mgr/open_do.rs b/yazi-actor/src/mgr/open_do.rs index 3af6eac8..defae85b 100644 --- a/yazi-actor/src/mgr/open_do.rs +++ b/yazi-actor/src/mgr/open_do.rs @@ -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()); } } diff --git a/yazi-binding/src/config/fetcher.rs b/yazi-binding/src/config/fetcher.rs index eb2d799e..c87edba2 100644 --- a/yazi-binding/src/config/fetcher.rs +++ b/yazi-binding/src/config/fetcher.rs @@ -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>) -> Self { + Self(inner.into()) + } +} + +impl TryFrom for FetcherMatcher { + type Error = mlua::Error; + + fn try_from(value: Table) -> Result { + let id: Id = value.raw_get("id").unwrap_or_default(); + let file: Option = value.raw_get("file")?; + let mime: Option = 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 { + 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 { + Iter::new(self.0.into_iter().map(Fetcher::new), None).into_lua(lua) + } +} diff --git a/yazi-binding/src/config/fetchers.rs b/yazi-binding/src/config/fetchers.rs index 922efc34..66bc3fdb 100644 --- a/yazi-binding/src/config/fetchers.rs +++ b/yazi-binding/src/config/fetchers.rs @@ -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>) -> Self { - Self(inner.into()) - } -} - -impl TryFrom
for FetcherMatcher { - type Error = mlua::Error; - - fn try_from(value: Table) -> Result { - let id: Id = value.raw_get("id").unwrap_or_default(); - let file: Option = value.raw_get("file")?; - let mime: Option = 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 { - 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 { - Iter::new(self.0.into_iter().map(Fetcher::new), None).into_lua(lua) - } -} diff --git a/yazi-binding/src/config/mod.rs b/yazi-binding/src/config/mod.rs index 5d106eb4..06de0823 100644 --- a/yazi-binding/src/config/mod.rs +++ b/yazi-binding/src/config/mod.rs @@ -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); diff --git a/yazi-binding/src/config/open_rule.rs b/yazi-binding/src/config/open_rule.rs new file mode 100644 index 00000000..9fb902fb --- /dev/null +++ b/yazi-binding/src/config/open_rule.rs @@ -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, + + v_use: Option, +} + +impl Deref for OpenRule { + type Target = Arc; + + fn deref(&self) -> &Self::Target { &self.inner } +} + +impl From for Arc { + fn from(value: OpenRule) -> Self { value.inner } +} + +impl OpenRule { + pub fn new(inner: impl Into>) -> Self { + Self { inner: inner.into(), v_use: None } + } +} + +impl FromLua for OpenRule { + fn from_lua(value: Value, lua: &Lua) -> mlua::Result { + Ok(Self::new(lua.from_value::(value)?)) + } +} + +impl UserData for OpenRule { + fn add_fields>(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>) -> Self { + Self(inner.into()) + } +} + +impl TryFrom
for OpenRuleMatcher { + type Error = mlua::Error; + + fn try_from(value: Table) -> Result { + let id: Id = value.raw_get("id").unwrap_or_default(); + let file: Option = value.raw_get("file")?; + let mime: Option = 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 { + 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 { + Iter::new(self.0.into_iter().map(OpenRule::new), None).into_lua(lua) + } +} diff --git a/yazi-binding/src/config/open_rules.rs b/yazi-binding/src/config/open_rules.rs new file mode 100644 index 00000000..d164b7b8 --- /dev/null +++ b/yazi-binding/src/config/open_rules.rs @@ -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>(methods: &mut M) { + methods.add_method("match", |lua, _, matcher: Option| 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())); + } +} diff --git a/yazi-binding/src/config/opener.rs b/yazi-binding/src/config/opener.rs new file mode 100644 index 00000000..f3bbd76c --- /dev/null +++ b/yazi-binding/src/config/opener.rs @@ -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>(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) + }); + } +} diff --git a/yazi-binding/src/config/opener_rule.rs b/yazi-binding/src/config/opener_rule.rs new file mode 100644 index 00000000..c5ef38b1 --- /dev/null +++ b/yazi-binding/src/config/opener_rule.rs @@ -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, + + v_run: Option, + v_desc: Option, +} + +impl Deref for OpenerRule { + type Target = Arc; + + fn deref(&self) -> &Self::Target { &self.inner } +} + +impl From for Arc { + fn from(value: OpenerRule) -> Self { value.inner } +} + +impl OpenerRule { + pub fn new(inner: impl Into>) -> Self { + Self { inner: inner.into(), v_run: None, v_desc: None } + } +} + +impl FromLua for OpenerRule { + fn from_lua(value: Value, lua: &Lua) -> mlua::Result { + let mut inner: yazi_config::opener::OpenerRule = lua.from_value(value)?; + inner.fill(); + + Ok(Self::new(inner)) + } +} + +impl UserData for OpenerRule { + fn add_fields>(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) -> Self { + Self(inner.into()) + } +} + +impl TryFrom
for OpenerRuleMatcher { + type Error = mlua::Error; + + fn try_from(t: Table) -> Result { + 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 { + 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 { + Iter::new(self.0.into_iter().map(OpenerRule::new), None).into_lua(lua) + } +} diff --git a/yazi-binding/src/config/opener_rules.rs b/yazi-binding/src/config/opener_rules.rs new file mode 100644 index 00000000..39a0631c --- /dev/null +++ b/yazi-binding/src/config/opener_rules.rs @@ -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, +} + +impl Deref for OpenerRules { + type Target = Arc; + + fn deref(&self) -> &Self::Target { &self.inner } +} + +impl OpenerRules { + pub fn new(inner: impl Into>) -> Self { + Self { inner: inner.into() } + } +} + +impl FromLua for OpenerRules { + fn from_lua(value: Value, lua: &Lua) -> mlua::Result { + 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>(methods: &mut M) { + methods.add_method("match", |lua, me, matcher: Option| 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())); + } +} diff --git a/yazi-binding/src/config/preloader.rs b/yazi-binding/src/config/preloader.rs index 08403d5a..6564158a 100644 --- a/yazi-binding/src/config/preloader.rs +++ b/yazi-binding/src/config/preloader.rs @@ -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>) -> Self { + Self(inner.into()) + } +} + +impl TryFrom
for PreloaderMatcher { + type Error = mlua::Error; + + fn try_from(value: Table) -> Result { + let id: Id = value.raw_get("id").unwrap_or_default(); + let file: Option = value.raw_get("file")?; + let mime: Option = 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 { + 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 { + Iter::new(self.0.into_iter().map(Preloader::new), None).into_lua(lua) + } +} diff --git a/yazi-binding/src/config/preloaders.rs b/yazi-binding/src/config/preloaders.rs index dfa87eaf..f1c13769 100644 --- a/yazi-binding/src/config/preloaders.rs +++ b/yazi-binding/src/config/preloaders.rs @@ -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>) -> Self { - Self(inner.into()) - } -} - -impl TryFrom
for PreloaderMatcher { - type Error = mlua::Error; - - fn try_from(value: Table) -> Result { - let id: Id = value.raw_get("id").unwrap_or_default(); - let file: Option = value.raw_get("file")?; - let mime: Option = 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 { - 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 { - Iter::new(self.0.into_iter().map(Preloader::new), None).into_lua(lua) - } -} diff --git a/yazi-binding/src/config/previewer.rs b/yazi-binding/src/config/previewer.rs index 8530c949..2aa4f463 100644 --- a/yazi-binding/src/config/previewer.rs +++ b/yazi-binding/src/config/previewer.rs @@ -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 for Previewer { - type Error = mlua::Error; - - fn try_from(t: mlua::Table) -> Result { - Ok(Self::new(yazi_config::plugin::Previewer { - id: previewer_id(), - selector: Selector::try_from(&t)?.into(), - run: t.raw_get::("run")?.to_str()?.parse()?, - })) - } -} - -impl TryFrom for Previewer { - type Error = mlua::Error; - - fn try_from(value: AnyUserData) -> Result { - Ok(Self::new(value.borrow::()?.clone())) - } -} - impl FromLua for Previewer { - fn from_lua(value: Value, _: &Lua) -> mlua::Result { - 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 { + Ok(Self::new(lua.from_value::(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>) -> Self { + Self(inner.into()) + } +} + +impl TryFrom
for PreviewerMatcher { + type Error = mlua::Error; + + fn try_from(value: Table) -> Result { + let id: Id = value.raw_get("id").unwrap_or_default(); + let file: Option = value.raw_get("file")?; + let mime: Option = 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 { + 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 { + Iter::new(self.0.into_iter().map(Previewer::new), None).into_lua(lua) + } +} diff --git a/yazi-binding/src/config/previewers.rs b/yazi-binding/src/config/previewers.rs index df5835d6..66934839 100644 --- a/yazi-binding/src/config/previewers.rs +++ b/yazi-binding/src/config/previewers.rs @@ -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>) -> Self { - Self(inner.into()) - } -} - -impl TryFrom
for PreviewerMatcher { - type Error = mlua::Error; - - fn try_from(value: Table) -> Result { - let id: Id = value.raw_get("id").unwrap_or_default(); - let file: Option = value.raw_get("file")?; - let mime: Option = 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 { - 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 { - Iter::new(self.0.into_iter().map(Previewer::new), None).into_lua(lua) - } -} diff --git a/yazi-binding/src/config/spotter.rs b/yazi-binding/src/config/spotter.rs index be0ea1c4..9a425496 100644 --- a/yazi-binding/src/config/spotter.rs +++ b/yazi-binding/src/config/spotter.rs @@ -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>) -> Self { + Self(inner.into()) + } +} + +impl TryFrom
for SpotterMatcher { + type Error = mlua::Error; + + fn try_from(value: Table) -> Result { + let id: Id = value.raw_get("id").unwrap_or_default(); + let file: Option = value.raw_get("file")?; + let mime: Option = 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 { + 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 { + Iter::new(self.0.into_iter().map(Spotter::new), None).into_lua(lua) + } +} diff --git a/yazi-binding/src/config/spotters.rs b/yazi-binding/src/config/spotters.rs index 5e4a2d4b..8afda780 100644 --- a/yazi-binding/src/config/spotters.rs +++ b/yazi-binding/src/config/spotters.rs @@ -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>) -> Self { - Self(inner.into()) - } -} - -impl TryFrom
for SpotterMatcher { - type Error = mlua::Error; - - fn try_from(value: Table) -> Result { - let id: Id = value.raw_get("id").unwrap_or_default(); - let file: Option = value.raw_get("file")?; - let mime: Option = 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 { - 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 { - Iter::new(self.0.into_iter().map(Spotter::new), None).into_lua(lua) - } -} diff --git a/yazi-binding/src/selector.rs b/yazi-binding/src/selector.rs index 9b511599..d5d281ef 100644 --- a/yazi-binding/src/selector.rs +++ b/yazi-binding/src/selector.rs @@ -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 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 { - Ok(Self(yazi_config::Selector::new( - t.raw_get::>("url")?.map(|s| s.to_str()?.parse()).transpose()?, - t.raw_get::>("mime")?.map(|s| s.to_str()?.parse()).transpose()?, - )?)) - } -} - -impl TryFrom
for Selector { - type Error = mlua::Error; - - fn try_from(value: Table) -> Result { Self::try_from(&value) } -} - impl FromLua for Selector { - fn from_lua(value: Value, _: &Lua) -> mlua::Result { - 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 { Ok(Self(lua.from_value(value)?)) } } impl UserData for Selector {} diff --git a/yazi-boot/src/actions/debug.rs b/yazi-boot/src/actions/debug.rs index d80a9c31..66080b89 100644 --- a/yazi-boot/src/actions/debug.rs +++ b/yazi-boot/src/actions/debug.rs @@ -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")?; diff --git a/yazi-codegen/Cargo.toml b/yazi-codegen/Cargo.toml index 0634bfd3..01cb062f 100644 --- a/yazi-codegen/Cargo.toml +++ b/yazi-codegen/Cargo.toml @@ -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" ] } diff --git a/yazi-codegen/src/helper.rs b/yazi-codegen/src/helper.rs new file mode 100644 index 00000000..9a2089e5 --- /dev/null +++ b/yazi-codegen/src/helper.rs @@ -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 { + 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::::parse_terminated) + .map_or(false, |n| n.iter().any(|m| m.path().is_ident(name))) + }) +} diff --git a/yazi-codegen/src/lib.rs b/yazi-codegen/src/lib.rs index fc9e0a1c..0c50d3a0 100644 --- a/yazi-codegen/src/lib.rs +++ b/yazi-codegen/src/lib.rs @@ -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::(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>) -> Result { - use yazi_shim::toml::{DeserializeOverHook, DeserializeOverWith, deserialize_spanned}; + fn deserialize_over_with<'__de, __D: serde::Deserializer<'__de>>(self, de: __D) -> Result { + 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 { + while let Some(key) = map.next_key::>()? { + match key.as_ref() { + #(#match_arms)* + _ => { map.next_value::()?; } + } + } + 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::()?.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>) -> Result { - use yazi_shim::toml::deserialize_spanned; + fn deserialize_over_with<'__de, __D: serde::Deserializer<'__de>>(self, de: __D) -> Result { + 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 { + let mut selector_url: Option = None; + let mut selector_mime: Option = None; + + while let Some(key) = map.next_key::>()? { + match key.as_ref() { + #(#match_arms)* + _ => { map.next_value::()?; } + } + } + + #(#post_fields)* + Ok(self.0) + } + } + + de.deserialize_map(V(self)) } } } diff --git a/yazi-config/src/lib.rs b/yazi-config/src/lib.rs index dbf6a763..f7dcaff9 100644 --- a/yazi-config/src/lib.rs +++ b/yazi-config/src/lib.rs @@ -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)?); diff --git a/yazi-config/src/open/mod.rs b/yazi-config/src/open/mod.rs index 37a5e305..5008c64b 100644 --- a/yazi-config/src/open/mod.rs +++ b/yazi-config/src/open/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(open rule); +yazi_macro::mod_flat!(open rule rules); diff --git a/yazi-config/src/open/open.rs b/yazi-config/src/open/open.rs index 7ab027f3..33ef791e 100644 --- a/yazi-config/src/open/open.rs +++ b/yazi-config/src/open/open.rs @@ -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, + rules: OpenRules, #[serde(default)] prepend_rules: Vec, #[serde(default)] @@ -20,23 +20,13 @@ pub struct Open { } impl Deref for Open { - type Target = Vec; + type Target = OpenRules; fn deref(&self) -> &Self::Target { &self.rules } } impl Open { - pub fn all<'a>(&'a self, file: &File, mime: &str) -> impl Iterator { - 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 + pub fn match_dummy(&self, url: U, mime: M) -> Option> where U: AsUrl, M: AsRef, @@ -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> = targets - .iter() - .map(|(file, mime)| self.all(file, mime).collect::>()) - .filter(|s| !s.is_empty()) - .collect(); + pub fn match_common(&self, targets: &[(File, &str)]) -> impl Iterator> { + let mut seen: IndexMap, 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 { - Ok(Self { rules: mix(self.prepend_rules, self.rules, self.append_rules), ..Default::default() }) + let rules: Vec> = + mix(self.prepend_rules, self.rules.unwrap_unchecked(), self.append_rules); + + Ok(Self { rules: rules.into(), ..Default::default() }) } } diff --git a/yazi-config/src/open/rule.rs b/yazi-config/src/open/rule.rs index 28e0df4e..afe44831 100644 --- a/yazi-config/src/open/rule.rs +++ b/yazi-config/src/open/rule.rs @@ -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, } +impl PartialEq for OpenRule { + fn eq(&self, other: &Self) -> bool { self.id == other.id } +} + +impl Eq for OpenRule {} + +impl Hash for OpenRule { + fn hash(&self, state: &mut H) { self.id.hash(state); } +} + impl Selectable for OpenRule { fn url_pat(&self) -> Option<&Pattern> { self.selector.url_pat() } diff --git a/yazi-config/src/open/rules.rs b/yazi-config/src/open/rules.rs new file mode 100644 index 00000000..753a524d --- /dev/null +++ b/yazi-config/src/open/rules.rs @@ -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>>); + +impl Deref for OpenRules { + type Target = ArcSwap>>; + + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl From>> for OpenRules { + fn from(inner: Vec>) -> Self { Self(inner.into_pointee()) } +} + +impl OpenRules { + pub fn matches(&self, file: &File, mime: &str) -> Option> { + self.matcher(Some(file), Some(mime)).next() + } + + pub fn matcher<'a, F, M>(&self, file: Option, mime: Option) -> OpenRuleMatcher<'a> + where + F: Into>, + M: Into>, + { + 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) -> Result<(), IndexAtError> { + self.0.try_rcu(|rules| { + let i = rules.index_at(index)?; + Ok(if i == rules.len() { + mix(Vec::::new(), rules.iter().cloned(), [rule.clone()]) + } else { + let (before, after) = rules.split_at(i); + mix( + Vec::::new(), + before.iter().cloned().chain([rule.clone()]).chain(after.iter().cloned()), + Vec::::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( + &self, + matcher: OpenRuleMatcher, + f: impl Fn(OpenRule) -> Result, + ) -> 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::try_unwrap(self.0.into_inner()).expect("unique open rules arc") + } +} + +// --- Matcher +#[derive(Default)] +pub struct OpenRuleMatcher<'a> { + pub rules: Arc>>, + pub id: Id, + pub file: Option>, + pub mime: Option>, + 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; + + fn next(&mut self) -> Option { + while let Some(rule) = self.rules.get(self.offset) { + self.offset += 1; + if self.matches(rule) { + return Some(rule.clone()); + } + } + None + } +} diff --git a/yazi-config/src/opener/mod.rs b/yazi-config/src/opener/mod.rs index 3e0ae9f0..d8c2dd4b 100644 --- a/yazi-config/src/opener/mod.rs +++ b/yazi-config/src/opener/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(opener rule); +yazi_macro::mod_flat!(opener rule rules); diff --git a/yazi-config/src/opener/opener.rs b/yazi-config/src/opener/opener.rs index 3b2494da..01e939f1 100644 --- a/yazi-config/src/opener/opener.rs +++ b/yazi-config/src/opener/opener.rs @@ -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>); +pub struct Opener(ArcSwap>>); impl Deref for Opener { - type Target = HashMap>; + type Target = ArcSwap>>; fn deref(&self) -> &Self::Target { &self.0 } } impl Opener { - pub fn all<'a, I>(&self, uses: I) -> impl Iterator - where - I: Iterator, - { - uses.flat_map(|use_| self.get(use_)).flatten() + pub fn all(&self, open: Arc) -> impl Iterator> + 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, - { - uses.flat_map(|use_| self.get(use_)).flatten().next() + pub fn first(&self, open: &OpenRule) -> Option> { + 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, - { - uses.flat_map(|use_| self.get(use_)).flatten().find(|&o| o.block) + pub fn block(&self, open: &OpenRule) -> Option> { + 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) { + 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> { + Arc::try_unwrap(self.0.into_inner()).expect("unique opener arc") } } impl DeserializeOverHook for Opener { - fn deserialize_over_hook(mut self) -> Result { - 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 { + 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>, - ) -> Result { - 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 { + struct V(HashMap>); + + 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>(mut self, mut map: M) -> Result { + 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())) } } diff --git a/yazi-config/src/opener/rule.rs b/yazi-config/src/opener/rule.rs index 9b8fc8ea..a38570fa 100644 --- a/yazi-config/src/opener/rule.rs +++ b/yazi-config/src/opener/rule.rs @@ -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>>, + 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; + + fn next(&mut self) -> Option { + while let Some(rule) = self.rules.get(self.offset) { + self.offset += 1; + if self.matches(rule) { + return Some(rule.clone()); + } + } + None + } +} diff --git a/yazi-config/src/opener/rules.rs b/yazi-config/src/opener/rules.rs new file mode 100644 index 00000000..9849832f --- /dev/null +++ b/yazi-config/src/opener/rules.rs @@ -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>>); + +impl Deref for OpenerRules { + type Target = ArcSwap>>; + + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl From>> for OpenerRules { + fn from(inner: Vec>) -> Self { Self(inner.into_pointee()) } +} + +impl OpenerRules { + pub fn insert(&self, index: isize, rule: Arc) -> 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::>(), + ) + })?; + + 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::try_unwrap(self.0.into_inner()).expect("unique opener rules arc") + } +} + +impl DeserializeOverHook for OpenerRules { + fn deserialize_over_hook(self) -> Result { + 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>, + _opener: Arc>>, +} + +impl From<&Opener> for OpenerRulesMatcher { + fn from(opener: &Opener) -> Self { + let opener = opener.load_full(); + let iter: hash_map::Iter> = opener.iter(); + + Self { iter: unsafe { mem::transmute(iter) }, _opener: opener } + } +} + +impl Iterator for OpenerRulesMatcher { + type Item = (String, Arc); + + fn next(&mut self) -> Option { + self.iter.next().map(|(name, rules)| (name.clone(), rules.clone())) + } +} diff --git a/yazi-config/src/pattern.rs b/yazi-config/src/pattern.rs index d601c73a..37fd6dc3 100644 --- a/yazi-config/src/pattern.rs +++ b/yazi-config/src/pattern.rs @@ -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, diff --git a/yazi-config/src/plugin/fetcher.rs b/yazi-config/src/plugin/fetcher.rs index e1aa6ad8..da5210cf 100644 --- a/yazi-config/src/plugin/fetcher.rs +++ b/yazi-config/src/plugin/fetcher.rs @@ -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>>, + pub id: Id, + pub file: Option>, + pub mime: Option>, + pub all: bool, + pub offset: usize, + pub seen: HashSet, +} + +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; + + fn next(&mut self) -> Option { + 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 + } +} diff --git a/yazi-config/src/plugin/fetchers.rs b/yazi-config/src/plugin/fetchers.rs index de8ae869..cab71a4b 100644 --- a/yazi-config/src/plugin/fetchers.rs +++ b/yazi-config/src/plugin/fetchers.rs @@ -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>>); @@ -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>>, - pub id: Id, - pub file: Option>, - pub mime: Option>, - pub all: bool, - pub offset: usize, - pub seen: HashSet, -} - -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; - - fn next(&mut self) -> Option { - 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 - } -} diff --git a/yazi-config/src/plugin/ids.rs b/yazi-config/src/plugin/ids.rs index 9f5830ca..64ae9912 100644 --- a/yazi-config/src/plugin/ids.rs +++ b/yazi-config/src/plugin/ids.rs @@ -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() +} diff --git a/yazi-config/src/plugin/preloader.rs b/yazi-config/src/plugin/preloader.rs index dc165c30..0e5c08ab 100644 --- a/yazi-config/src/plugin/preloader.rs +++ b/yazi-config/src/plugin/preloader.rs @@ -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>>, + pub id: Id, + pub file: Option>, + pub mime: Option>, + 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; + + fn next(&mut self) -> Option { + 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 + } +} diff --git a/yazi-config/src/plugin/preloaders.rs b/yazi-config/src/plugin/preloaders.rs index 4c259138..5e57163f 100644 --- a/yazi-config/src/plugin/preloaders.rs +++ b/yazi-config/src/plugin/preloaders.rs @@ -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>>); @@ -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>>, - pub id: Id, - pub file: Option>, - pub mime: Option>, - 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; - - fn next(&mut self) -> Option { - 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 - } -} diff --git a/yazi-config/src/plugin/previewer.rs b/yazi-config/src/plugin/previewer.rs index 62fd6ee4..5aed6685 100644 --- a/yazi-config/src/plugin/previewer.rs +++ b/yazi-config/src/plugin/previewer.rs @@ -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>>, + pub id: Id, + pub file: Option>, + pub mime: Option>, + 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; + + fn next(&mut self) -> Option { + while let Some(previewer) = self.previewers.get(self.offset) { + self.offset += 1; + if self.matches(previewer) { + return Some(previewer.clone()); + } + } + None + } +} diff --git a/yazi-config/src/plugin/previewers.rs b/yazi-config/src/plugin/previewers.rs index 082c94aa..d02a47e5 100644 --- a/yazi-config/src/plugin/previewers.rs +++ b/yazi-config/src/plugin/previewers.rs @@ -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>>); @@ -41,27 +40,21 @@ impl Previewers { } pub fn insert(&self, index: isize, previewer: Arc) -> Result<(), IndexAtError> { - let mut err = None; - - self.0.rcu(|previewers| match previewers.index_at(index) { - Ok(n) if n == previewers.len() => { - mix(Vec::::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::::new(), previewers.iter().cloned(), [previewer.clone()])) + } else { + let (before, after) = previewers.split_at(i); + Ok(mix( Vec::::new(), before.iter().cloned().chain([previewer.clone()]).chain(after.iter().cloned()), Vec::::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>>, - pub id: Id, - pub file: Option>, - pub mime: Option>, - 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; - - fn next(&mut self) -> Option { - while let Some(previewer) = self.previewers.get(self.offset) { - self.offset += 1; - if self.matches(previewer) { - return Some(previewer.clone()); - } - } - None - } -} diff --git a/yazi-config/src/plugin/spotter.rs b/yazi-config/src/plugin/spotter.rs index 9f2c6eee..73ca207a 100644 --- a/yazi-config/src/plugin/spotter.rs +++ b/yazi-config/src/plugin/spotter.rs @@ -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>>, + pub id: Id, + pub file: Option>, + pub mime: Option>, + 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; + + fn next(&mut self) -> Option { + while let Some(spotter) = self.spotters.get(self.offset) { + self.offset += 1; + if self.matches(spotter) { + return Some(spotter.clone()); + } + } + None + } +} diff --git a/yazi-config/src/plugin/spotters.rs b/yazi-config/src/plugin/spotters.rs index d61340b3..4c092ffa 100644 --- a/yazi-config/src/plugin/spotters.rs +++ b/yazi-config/src/plugin/spotters.rs @@ -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>>); @@ -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>>, - pub id: Id, - pub file: Option>, - pub mime: Option>, - 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; - - fn next(&mut self) -> Option { - while let Some(spotter) = self.spotters.get(self.offset) { - self.offset += 1; - if self.matches(spotter) { - return Some(spotter.clone()); - } - } - None - } -} diff --git a/yazi-config/src/selector.rs b/yazi-config/src/selector.rs index 22e4ac45..cdbb8c7b 100644 --- a/yazi-config/src/selector.rs +++ b/yazi-config/src/selector.rs @@ -3,10 +3,10 @@ use serde::{Deserialize, Deserializer, de}; use crate::{Mixable, Pattern, Selectable}; -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct Selector { - url: Option, - mime: Option, + pub url: Option, + pub mime: Option, } impl<'de> Deserialize<'de> for Selector { diff --git a/yazi-config/src/vfs/services.rs b/yazi-config/src/vfs/services.rs index 32a929ea..7e7666e4 100644 --- a/yazi-config/src/vfs/services.rs +++ b/yazi-config/src/vfs/services.rs @@ -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>, - ) -> Result { - 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 { + 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>(mut self, mut map: M) -> Result { + while let Some(key) = map.next_key::()? { + 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)) } } diff --git a/yazi-fm/Cargo.toml b/yazi-fm/Cargo.toml index 79599465..96a118fc 100644 --- a/yazi-fm/Cargo.toml +++ b/yazi-fm/Cargo.toml @@ -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] diff --git a/yazi-plugin/src/runtime/runtime.rs b/yazi-plugin/src/runtime/runtime.rs index 9519001b..f5e80c39 100644 --- a/yazi-plugin/src/runtime/runtime.rs +++ b/yazi-plugin/src/runtime/runtime.rs @@ -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 { 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 { Composer::new(get, set) } +fn open() -> Composer { + fn get(lua: &Lua, key: &[u8]) -> mlua::Result { + match key { + b"rules" => OpenRules.into_lua(lua), + _ => Ok(Value::Nil), + } + } + + fn set(_: &Lua, _: &[u8], value: Value) -> mlua::Result { Ok(value) } + + Composer::new(get, set) +} + fn args() -> Composer { fn get(lua: &Lua, key: &[u8]) -> mlua::Result { match key { diff --git a/yazi-shared/Cargo.toml b/yazi-shared/Cargo.toml index 9ee73233..8d1f5539 100644 --- a/yazi-shared/Cargo.toml +++ b/yazi-shared/Cargo.toml @@ -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 } diff --git a/yazi-shared/src/condition.rs b/yazi-shared/src/condition.rs index 4c94dce2..e348045e 100644 --- a/yazi-shared/src/condition.rs +++ b/yazi-shared/src/condition.rs @@ -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, } @@ -62,16 +62,6 @@ impl FromStr for Condition { } } -impl<'de> Deserialize<'de> for Condition { - fn deserialize(deserializer: D) -> Result - 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 = vec![]; diff --git a/yazi-shared/src/event/action.rs b/yazi-shared/src/event/action.rs index b7e31fd0..65849610 100644 --- a/yazi-shared/src/event/action.rs +++ b/yazi-shared/src/event/action.rs @@ -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, @@ -281,12 +281,3 @@ impl FromStr for Action { Ok(me) } } - -impl<'de> Deserialize<'de> for Action { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - <_>::from_str(&String::deserialize(deserializer)?).map_err(de::Error::custom) - } -} diff --git a/yazi-shim/Cargo.toml b/yazi-shim/Cargo.toml index a6ac2ccb..3e9b20dc 100644 --- a/yazi-shim/Cargo.toml +++ b/yazi-shim/Cargo.toml @@ -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 } diff --git a/yazi-shim/src/arc_swap/traits.rs b/yazi-shim/src/arc_swap/traits.rs index 4a095e28..9d391701 100644 --- a/yazi-shim/src/arc_swap/traits.rs +++ b/yazi-shim/src/arc_swap/traits.rs @@ -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 IntoPointee for T { #[inline] fn into_pointee(self) -> ArcSwap { ArcSwap::from_pointee(self) } } + +// --- ArcSwapExt +pub trait ArcSwapExt { + fn try_rcu(&self, f: F) -> Result + where + F: FnMut(&T) -> Result, + R: Into; +} + +impl ArcSwapExt for ArcSwapAny +where + T: RefCnt, + S: CaS, +{ + fn try_rcu(&self, mut f: F) -> Result + where + F: FnMut(&T) -> Result, + R: Into, + { + 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; + } + } +} diff --git a/yazi-shim/src/lib.rs b/yazi-shim/src/lib.rs index 26d742f5..7fe0c54e 100644 --- a/yazi-shim/src/lib.rs +++ b/yazi-shim/src/lib.rs @@ -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); diff --git a/yazi-shim/src/mlua/mod.rs b/yazi-shim/src/mlua/mod.rs new file mode 100644 index 00000000..ff9b6f21 --- /dev/null +++ b/yazi-shim/src/mlua/mod.rs @@ -0,0 +1 @@ +yazi_macro::mod_flat!(traits); diff --git a/yazi-shim/src/mlua/traits.rs b/yazi-shim/src/mlua/traits.rs new file mode 100644 index 00000000..992430b0 --- /dev/null +++ b/yazi-shim/src/mlua/traits.rs @@ -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 { + let de = mlua::serde::Deserializer::new(Value::Table(table.clone())); + self.deserialize_over_with(de).map_err(|e| e.into_lua_err()) + } +} + +impl DeserializeOverLua for T {} diff --git a/yazi-shim/src/toml/traits.rs b/yazi-shim/src/toml/traits.rs index 01b4ca69..177c1dd8 100644 --- a/yazi-shim/src/toml/traits.rs +++ b/yazi-shim/src/toml/traits.rs @@ -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.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 DeserializeOver for T where T: DeserializeOverWith + DeserializeOverHook {} + +// --- DeserializeOverWith pub trait DeserializeOverWith: Sized { - fn deserialize_over_with<'de>(self, table: Spanned>) -> Result; + fn deserialize_over_with<'de, D: Deserializer<'de>>(self, de: D) -> Result; } +// --- DeserializeOverHook pub trait DeserializeOverHook: Sized { fn deserialize_over_hook(self) -> Result { Ok(self) } } -impl DeserializeOver for T where T: DeserializeOverWith + DeserializeOverHook {} +// --- DeserializeOverSeed +pub struct DeserializeOverSeed(pub T); + +impl<'de, T: DeserializeOverWith> DeserializeSeed<'de> for DeserializeOverSeed { + type Value = T; + + fn deserialize>(self, de: D) -> Result { + self.0.deserialize_over_with(de) + } +} diff --git a/yazi-widgets/src/step.rs b/yazi-widgets/src/step.rs index 9ac5c1b9..a9ec6d86 100644 --- a/yazi-widgets/src/step.rs +++ b/yazi-widgets/src/step.rs @@ -56,20 +56,6 @@ impl<'de> Deserialize<'de> for Step { value.parse().map_err(E::custom) } - fn visit_borrowed_str(self, value: &'_ str) -> Result - where - E: de::Error, - { - self.visit_str(value) - } - - fn visit_string(self, value: String) -> Result - where - E: de::Error, - { - self.visit_str(&value) - } - fn visit_i64(self, value: i64) -> Result where E: de::Error,