From 37134787532aecba20a239b137a216b06e76f596 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Mon, 8 Sep 2025 23:38:10 +0800 Subject: [PATCH] feat: multi-entry support for plugin system (#3154) --- yazi-cli/src/args.rs | 23 ---------------- yazi-cli/src/main.rs | 18 ------------ yazi-cli/src/package/delete.rs | 13 ++++----- yazi-cli/src/package/dependency.rs | 31 +++++++++++++++++++-- yazi-cli/src/package/deploy.rs | 9 ++---- yazi-cli/src/package/hash.rs | 17 ++---------- yazi-dds/src/ember/ember.rs | 4 +-- yazi-parser/src/app/plugin.rs | 21 ++++++++++++-- yazi-plugin/src/isolate/peek.rs | 12 ++++---- yazi-plugin/src/loader/loader.rs | 44 ++++++++++++++++++++---------- yazi-plugin/src/loader/require.rs | 37 ++++++++++++++++--------- yazi-plugin/src/utils/utils.rs | 1 - yazi-shared/src/bytes.rs | 5 ++++ 13 files changed, 124 insertions(+), 111 deletions(-) diff --git a/yazi-cli/src/args.rs b/yazi-cli/src/args.rs index 684af4dd..c99e9648 100644 --- a/yazi-cli/src/args.rs +++ b/yazi-cli/src/args.rs @@ -24,9 +24,6 @@ pub(super) enum Command { /// Manage packages. #[command(subcommand)] Pkg(CommandPkg), - #[command(hide = true)] - /// Manage packages. - Pack(CommandPack), // TODO: remove /// Publish a message to the current instance. Pub(CommandPub), /// Publish a message to the specified instance. @@ -83,26 +80,6 @@ pub(super) enum CommandPkg { }, } -#[derive(clap::Args)] -#[command(arg_required_else_help = true)] -pub(super) struct CommandPack { - /// Add packages. - #[arg(short = 'a', long, num_args = 1..)] - pub(super) add: Option>, - /// Delete packages. - #[arg(short = 'd', long, num_args = 1..)] - pub(super) delete: Option>, - /// Install all packages. - #[arg(short = 'i', long)] - pub(super) install: bool, - /// List all packages. - #[arg(short = 'l', long)] - pub(super) list: bool, - /// Upgrade all packages. - #[arg(short = 'u', long)] - pub(super) upgrade: bool, -} - #[derive(clap::Args)] pub(super) struct CommandPub { /// Kind of message. diff --git a/yazi-cli/src/main.rs b/yazi-cli/src/main.rs index 9902447b..06776ea7 100644 --- a/yazi-cli/src/main.rs +++ b/yazi-cli/src/main.rs @@ -73,24 +73,6 @@ async fn run() -> anyhow::Result<()> { } } - Command::Pack(cmd) => { - package::init()?; - outln!( - "WARNING: `ya pack` is deprecated, use the new `ya pkg` instead. See https://github.com/sxyazi/yazi/pull/2770 for more details." - )?; - if cmd.install { - package::Package::load().await?.install().await?; - } else if cmd.list { - package::Package::load().await?.print()?; - } else if cmd.upgrade { - package::Package::load().await?.upgrade_many(&[]).await?; - } else if let Some(uses) = cmd.add { - package::Package::load().await?.add_many(&uses).await?; - } else if let Some(uses) = cmd.delete { - package::Package::load().await?.delete_many(&uses).await?; - } - } - Command::Pub(cmd) => { yazi_boot::init_default(); yazi_dds::init(); diff --git a/yazi-cli/src/package/delete.rs b/yazi-cli/src/package/delete.rs index 92299fd0..3bdc6a90 100644 --- a/yazi-cli/src/package/delete.rs +++ b/yazi-cli/src/package/delete.rs @@ -41,15 +41,12 @@ impl Dependency { pub(super) async fn delete_sources(&self) -> Result<()> { let dir = self.target(); - let files = if self.is_flavor { - &["flavor.toml", "tmtheme.xml", "README.md", "preview.png", "LICENSE", "LICENSE-tmtheme"][..] - } else { - &["main.lua", "README.md", "LICENSE"][..] - }; + let files = + if self.is_flavor { Self::flavor_files() } else { Self::plugin_files(&dir).await? }; - for p in files.iter().map(|&f| dir.join(f)) { - ok_or_not_found(remove_sealed(&p).await) - .with_context(|| format!("failed to delete `{}`", p.display()))?; + for path in files.iter().map(|s| dir.join(s)) { + ok_or_not_found(remove_sealed(&path).await) + .with_context(|| format!("failed to delete `{}`", path.display()))?; } if ok_or_not_found(Local::remove_dir(&dir).await).is_ok() { diff --git a/yazi-cli/src/package/dependency.rs b/yazi-cli/src/package/dependency.rs index dd376aef..0d6e2c3c 100644 --- a/yazi-cli/src/package/dependency.rs +++ b/yazi-cli/src/package/dependency.rs @@ -1,9 +1,10 @@ -use std::{io::BufWriter, path::PathBuf, str::FromStr}; +use std::{io::BufWriter, path::{Path, PathBuf}, str::FromStr}; use anyhow::{Result, bail}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use twox_hash::XxHash3_128; -use yazi_fs::Xdg; +use yazi_fs::{Xdg, provider::local::Local}; +use yazi_shared::BytesExt; #[derive(Clone, Default)] pub(crate) struct Dependency { @@ -59,6 +60,30 @@ impl Dependency { )?; Ok(()) } + + pub(super) async fn plugin_files(dir: &Path) -> std::io::Result> { + let mut it = Local::read_dir(dir).await?; + let mut files: Vec = + ["LICENSE", "README.md", "main.lua"].into_iter().map(Into::into).collect(); + while let Some(entry) = it.next_entry().await? { + if let Ok(name) = entry.file_name().into_string() + && let Some(stripped) = name.strip_suffix(".lua") + && stripped != "main" + && stripped.as_bytes().kebab_cased() + { + files.push(name); + } + } + files.sort_unstable(); + Ok(files) + } + + pub(super) fn flavor_files() -> Vec { + ["LICENSE", "LICENSE-tmtheme", "README.md", "flavor.toml", "preview.png", "tmtheme.xml"] + .into_iter() + .map(Into::into) + .collect() + } } impl FromStr for Dependency { @@ -75,7 +100,7 @@ impl FromStr for Dependency { }; let name = if child.is_empty() { repo } else { child }; - if !name.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'-')) { + if !name.as_bytes().kebab_cased() { bail!("Package name `{name}` must be in kebab-case") } diff --git a/yazi-cli/src/package/deploy.rs b/yazi-cli/src/package/deploy.rs index 62d5ee7b..7f75030d 100644 --- a/yazi-cli/src/package/deploy.rs +++ b/yazi-cli/src/package/deploy.rs @@ -57,14 +57,9 @@ impl Dependency { } async fn deploy_sources(from: &Path, to: &Path, is_flavor: bool) -> Result<()> { - let files = if is_flavor { - &["flavor.toml", "tmtheme.xml", "README.md", "preview.png", "LICENSE", "LICENSE-tmtheme"][..] - } else { - &["main.lua", "README.md", "LICENSE"][..] - }; - + let files = if is_flavor { Self::flavor_files() } else { Self::plugin_files(from).await? }; for file in files { - let (from, to) = (from.join(file), to.join(file)); + let (from, to) = (from.join(&file), to.join(&file)); copy_and_seal(&from, &to) .await .with_context(|| format!("failed to copy `{}` to `{}`", from.display(), to.display()))?; diff --git a/yazi-cli/src/package/hash.rs b/yazi-cli/src/package/hash.rs index 10a48c53..687e7303 100644 --- a/yazi-cli/src/package/hash.rs +++ b/yazi-cli/src/package/hash.rs @@ -7,22 +7,11 @@ use super::Dependency; impl Dependency { pub(crate) async fn hash(&self) -> Result { let dir = self.target(); - let files = if self.is_flavor { - &[ - "LICENSE", - "LICENSE-tmtheme", - "README.md", - "filestyle.toml", - "flavor.toml", - "preview.png", - "tmtheme.xml", - ][..] - } else { - &["LICENSE", "README.md", "main.lua"][..] - }; + let files = + if self.is_flavor { Self::flavor_files() } else { Self::plugin_files(&dir).await? }; let mut h = XxHash3_128::new(); - for &file in files { + for file in files { h.write(file.as_bytes()); h.write(b"VpvFw9Atb7cWGOdqhZCra634CcJJRlsRl72RbZeV0vpG1\0"); h.write(&ok_or_not_found(Local::read(dir.join(file)).await)?); diff --git a/yazi-dds/src/ember/ember.rs b/yazi-dds/src/ember/ember.rs index 62984956..b71d7d6a 100644 --- a/yazi-dds/src/ember/ember.rs +++ b/yazi-dds/src/ember/ember.rs @@ -81,8 +81,8 @@ impl Ember<'static> { if it.peek() == Some(&b'@') { it.next(); // Skip `@` as it's a prefix for static messages } - if !it.all(|b| b.is_ascii_alphanumeric() || b == b'-') { - bail!("Kind must be alphanumeric with dashes"); + if !it.all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'-')) { + bail!("Kind `{kind}` must be in kebab-case"); } Ok(()) diff --git a/yazi-parser/src/app/plugin.rs b/yazi-parser/src/app/plugin.rs index 3b313401..75068fd2 100644 --- a/yazi-parser/src/app/plugin.rs +++ b/yazi-parser/src/app/plugin.rs @@ -1,4 +1,4 @@ -use std::fmt::Debug; +use std::{borrow::Cow, fmt::Debug}; use anyhow::bail; use hashbrown::HashMap; @@ -35,7 +35,7 @@ impl TryFrom for PluginOpt { }; let mode = c.str("mode").map(Into::into).unwrap_or_default(); - Ok(Self { id, args, mode, cb: c.take_any("callback") }) + Ok(Self { id: Self::normalize_id(id), args, mode, cb: c.take_any("callback") }) } } @@ -52,7 +52,22 @@ impl Debug for PluginOpt { impl PluginOpt { pub fn new_callback(id: impl Into, cb: PluginCallback) -> Self { - Self { id: id.into(), mode: PluginMode::Sync, cb: Some(cb), ..Default::default() } + Self { + id: Self::normalize_id(id.into()), + mode: PluginMode::Sync, + cb: Some(cb), + ..Default::default() + } + } + + fn normalize_id(s: SStr) -> SStr { + match s { + Cow::Borrowed(s) => s.trim_end_matches(".main").into(), + Cow::Owned(mut s) => { + s.truncate(s.trim_end_matches(".main").len()); + s.into() + } + } } } diff --git a/yazi-plugin/src/isolate/peek.rs b/yazi-plugin/src/isolate/peek.rs index 43e200ae..a712a2c4 100644 --- a/yazi-plugin/src/isolate/peek.rs +++ b/yazi-plugin/src/isolate/peek.rs @@ -10,7 +10,7 @@ use yazi_proxy::AppProxy; use yazi_shared::{event::Cmd, pool::Symbol}; use super::slim_lua; -use crate::loader::LOADER; +use crate::loader::{LOADER, Loader}; pub fn peek( cmd: &'static Cmd, @@ -18,8 +18,10 @@ pub fn peek( mime: Symbol, skip: usize, ) -> Option { + let (id, ..) = Loader::normalize_id(&cmd.name).ok()?; + let ct = CancellationToken::new(); - if let Some(c) = LOADER.read().get(cmd.name.as_ref()) { + if let Some(c) = LOADER.read().get(id) { if c.sync_peek { peek_sync(cmd, file, mime, skip); } else { @@ -32,11 +34,11 @@ pub fn peek( tokio::spawn(async move { select! { _ = ct_.cancelled() => {}, - Ok(b) = LOADER.ensure(&cmd.name, |c| c.sync_peek) => { + Ok(b) = LOADER.ensure(id, |c| c.sync_peek) => { if b { - peek_sync( cmd, file, mime, skip); + peek_sync(cmd, file, mime, skip); } else { - peek_async( cmd, file, mime, skip, ct_); + peek_async(cmd, file, mime, skip, ct_); } }, else => {} diff --git a/yazi-plugin/src/loader/loader.rs b/yazi-plugin/src/loader/loader.rs index 797ca1a4..6e6d599b 100644 --- a/yazi-plugin/src/loader/loader.rs +++ b/yazi-plugin/src/loader/loader.rs @@ -1,13 +1,13 @@ use std::{borrow::Cow, ops::Deref}; -use anyhow::{Context, Result, bail}; +use anyhow::{Context, Result, bail, ensure}; use hashbrown::HashMap; use mlua::{ChunkMode, ExternalError, Lua, Table}; use parking_lot::RwLock; use yazi_boot::BOOT; use yazi_fs::provider::local::Local; use yazi_macro::plugin_preset as preset; -use yazi_shared::{LOG_LEVEL, RoCell}; +use yazi_shared::{BytesExt, LOG_LEVEL, RoCell}; use super::Chunk; @@ -20,7 +20,6 @@ pub struct Loader { impl Deref for Loader { type Target = RwLock>; - #[inline] fn deref(&self) -> &Self::Target { &self.cache } } @@ -52,26 +51,29 @@ impl Default for Loader { } impl Loader { - pub async fn ensure(&self, name: &str, f: F) -> Result + pub async fn ensure(&self, id: &str, f: F) -> Result where F: FnOnce(&Chunk) -> T, { - if let Some(c) = self.cache.read().get(name) { - return Self::compatible_or_error(name, c).map(|_| f(c)); + let (id, plugin, entry) = Self::normalize_id(id)?; + if let Some(c) = self.cache.read().get(id) { + return Self::compatible_or_error(id, c).map(|_| f(c)); } - let p = BOOT.plugin_dir.join(format!("{name}.yazi/main.lua")); + let p = BOOT.plugin_dir.join(format!("{plugin}.yazi/{entry}.lua")); let chunk = Local::read(&p).await.with_context(|| format!("Failed to load plugin from {p:?}"))?.into(); - let result = Self::compatible_or_error(name, &chunk); + let result = Self::compatible_or_error(id, &chunk); let inspect = f(&chunk); - self.cache.write().insert(name.to_owned(), chunk); + self.cache.write().insert(id.to_owned(), chunk); result.map(|_| inspect) } pub fn load(&self, lua: &Lua, id: &str) -> mlua::Result { + let (id, ..) = Self::normalize_id(id)?; + let loaded: Table = lua.globals().raw_get::
("package")?.raw_get("loaded")?; if let Ok(t) = loaded.raw_get(id) { return Ok(t); @@ -85,18 +87,20 @@ impl Loader { } pub fn load_once(&self, lua: &Lua, id: &str) -> mlua::Result
{ + let (id, ..) = Self::normalize_id(id)?; + let mut mode = ChunkMode::Text; - let f = match self.read().get(id) { + let f = match self.cache.read().get(id) { Some(c) => { mode = c.mode; lua.load(c).set_name(id).into_function() } - None => Err(format!("plugin `{id}` not found").into_lua_err()), + None => Err(format!("Plugin `{id}` not found").into_lua_err()), }?; if mode != ChunkMode::Binary { let b = f.dump(LOG_LEVEL.get().is_none()); - if let Some(c) = self.write().get_mut(id) { + if let Some(c) = self.cache.write().get_mut(id) { c.mode = ChunkMode::Binary; c.bytes = Cow::Owned(b); } @@ -106,10 +110,13 @@ impl Loader { } pub fn try_load(&self, lua: &Lua, id: &str) -> mlua::Result
{ + let (id, ..) = Self::normalize_id(id)?; lua.globals().raw_get::
("package")?.raw_get::
("loaded")?.raw_get(id) } pub fn load_with(&self, lua: &Lua, id: &str, chunk: &Chunk) -> mlua::Result
{ + let (id, ..) = Self::normalize_id(id)?; + let loaded: Table = lua.globals().raw_get::
("package")?.raw_get("loaded")?; if let Ok(t) = loaded.raw_get(id) { return Ok(t); @@ -122,15 +129,24 @@ impl Loader { Ok(t) } - pub fn compatible_or_error(name: &str, chunk: &Chunk) -> Result<()> { + pub fn compatible_or_error(id: &str, chunk: &Chunk) -> Result<()> { if chunk.compatible() { return Ok(()); } bail!( - "Plugin `{name}` requires at least Yazi {}, but your current version is Yazi {}.", + "Plugin `{id}` requires at least Yazi {}, but your current version is Yazi {}.", chunk.since, yazi_boot::actions::Actions::version() ); } + + pub fn normalize_id(id: &str) -> anyhow::Result<(&str, &str, &str)> { + let id = id.trim_end_matches(".main"); + let (plugin, entry) = if let Some((a, b)) = id.split_once(".") { (a, b) } else { (id, "main") }; + + ensure!(plugin.as_bytes().kebab_cased(), "Plugin name `{plugin}` must be in kebab-case"); + ensure!(entry.as_bytes().kebab_cased(), "Entry name `{entry}` must be in kebab-case"); + Ok((id, plugin, entry)) + } } diff --git a/yazi-plugin/src/loader/require.rs b/yazi-plugin/src/loader/require.rs index 2daee6cb..89f1e20c 100644 --- a/yazi-plugin/src/loader/require.rs +++ b/yazi-plugin/src/loader/require.rs @@ -1,7 +1,7 @@ -use std::sync::Arc; +use std::{borrow::Cow, sync::Arc}; use mlua::{ExternalResult, Function, IntoLua, Lua, MetaMethod, MultiValue, ObjectLike, Table, Value}; -use yazi_binding::runtime_mut; +use yazi_binding::{runtime, runtime_mut}; use super::LOADER; @@ -12,14 +12,15 @@ impl Require { lua.globals().raw_set( "require", lua.create_function(|lua, id: mlua::String| { - let s = &id.to_str()?; - futures::executor::block_on(LOADER.ensure(s, |_| ())).into_lua_err()?; + let id = id.to_str()?; + let id = Self::absolute_id(lua, &id)?; + futures::executor::block_on(LOADER.ensure(&id, |_| ())).into_lua_err()?; - runtime_mut!(lua)?.push(s); - let mod_ = LOADER.load(lua, s); + runtime_mut!(lua)?.push(&id); + let mod_ = LOADER.load(lua, &id); runtime_mut!(lua)?.pop(); - Self::create_mt(lua, s, mod_?, true) + Self::create_mt(lua, id.into_owned(), mod_?, true) })?, ) } @@ -28,19 +29,20 @@ impl Require { lua.globals().raw_set( "require", lua.create_async_function(|lua, id: mlua::String| async move { - let s = &id.to_str()?; - LOADER.ensure(s, |_| ()).await.into_lua_err()?; + let id = id.to_str()?; + let id = Self::absolute_id(&lua, &id)?; + LOADER.ensure(&id, |_| ()).await.into_lua_err()?; - runtime_mut!(lua)?.push(s); - let mod_ = LOADER.load(&lua, s); + runtime_mut!(lua)?.push(&id); + let mod_ = LOADER.load(&lua, &id); runtime_mut!(lua)?.pop(); - Self::create_mt(&lua, s, mod_?, false) + Self::create_mt(&lua, id.into_owned(), mod_?, false) })?, ) } - fn create_mt(lua: &Lua, id: &str, r#mod: Table, sync: bool) -> mlua::Result
{ + fn create_mt(lua: &Lua, id: String, r#mod: Table, sync: bool) -> mlua::Result
{ let id: Arc = Arc::from(id); let mt = lua.create_table_from([ ( @@ -112,4 +114,13 @@ impl Require { (LOADER.try_load(lua, id)?, args) }) } + + fn absolute_id<'a>(lua: &Lua, id: &'a str) -> mlua::Result> { + let Some(stripped) = id.strip_prefix('.') else { return Ok(id.into()) }; + Ok(if let Some(cur) = runtime!(lua)?.current() { + format!("{}.{stripped}", cur.split('.').next().unwrap_or(cur)).into() + } else { + stripped.into() + }) + } } diff --git a/yazi-plugin/src/utils/utils.rs b/yazi-plugin/src/utils/utils.rs index 02dc188e..5e94aac8 100644 --- a/yazi-plugin/src/utils/utils.rs +++ b/yazi-plugin/src/utils/utils.rs @@ -19,7 +19,6 @@ pub fn compose( b"render" => Utils::render(lua)?, b"emit" => Utils::emit(lua)?, b"mgr_emit" => Utils::mgr_emit(lua)?, - b"manager_emit" => Utils::mgr_emit(lua)?, // TODO: remove this in the future // Image b"image_info" => Utils::image_info(lua)?, diff --git a/yazi-shared/src/bytes.rs b/yazi-shared/src/bytes.rs index aedcb47f..659c1969 100644 --- a/yazi-shared/src/bytes.rs +++ b/yazi-shared/src/bytes.rs @@ -1,8 +1,13 @@ pub trait BytesExt { + fn kebab_cased(&self) -> bool; fn split_by_seq(&self, sep: &[u8]) -> Option<(&[u8], &[u8])>; } impl BytesExt for [u8] { + fn kebab_cased(&self) -> bool { + self.iter().all(|&b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'-')) + } + fn split_by_seq(&self, sep: &[u8]) -> Option<(&[u8], &[u8])> { let idx = memchr::memmem::find(self, sep)?; let (left, right) = self.split_at(idx);