mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-21 23:01:05 +00:00
feat: multi-entry support for plugin system (#3154)
This commit is contained in:
parent
fa0e7e62c1
commit
3713478753
13 changed files with 124 additions and 111 deletions
|
|
@ -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<Vec<String>>,
|
||||
/// Delete packages.
|
||||
#[arg(short = 'd', long, num_args = 1..)]
|
||||
pub(super) delete: Option<Vec<String>>,
|
||||
/// 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.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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<Vec<String>> {
|
||||
let mut it = Local::read_dir(dir).await?;
|
||||
let mut files: Vec<String> =
|
||||
["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<String> {
|
||||
["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")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()))?;
|
||||
|
|
|
|||
|
|
@ -7,22 +7,11 @@ use super::Dependency;
|
|||
impl Dependency {
|
||||
pub(crate) async fn hash(&self) -> Result<String> {
|
||||
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)?);
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
|
|
|
|||
|
|
@ -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<CmdCow> 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<SStr>, 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<str>,
|
||||
skip: usize,
|
||||
) -> Option<CancellationToken> {
|
||||
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 => {}
|
||||
|
|
|
|||
|
|
@ -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<HashMap<String, Chunk>>;
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target { &self.cache }
|
||||
}
|
||||
|
||||
|
|
@ -52,26 +51,29 @@ impl Default for Loader {
|
|||
}
|
||||
|
||||
impl Loader {
|
||||
pub async fn ensure<F, T>(&self, name: &str, f: F) -> Result<T>
|
||||
pub async fn ensure<F, T>(&self, id: &str, f: F) -> Result<T>
|
||||
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<Table> {
|
||||
let (id, ..) = Self::normalize_id(id)?;
|
||||
|
||||
let loaded: Table = lua.globals().raw_get::<Table>("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<Table> {
|
||||
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<Table> {
|
||||
let (id, ..) = Self::normalize_id(id)?;
|
||||
lua.globals().raw_get::<Table>("package")?.raw_get::<Table>("loaded")?.raw_get(id)
|
||||
}
|
||||
|
||||
pub fn load_with(&self, lua: &Lua, id: &str, chunk: &Chunk) -> mlua::Result<Table> {
|
||||
let (id, ..) = Self::normalize_id(id)?;
|
||||
|
||||
let loaded: Table = lua.globals().raw_get::<Table>("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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Table> {
|
||||
fn create_mt(lua: &Lua, id: String, r#mod: Table, sync: bool) -> mlua::Result<Table> {
|
||||
let id: Arc<str> = 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<Cow<'a, str>> {
|
||||
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()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)?,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue