feat: new relay-notify-push DDS event to allow custom notification handlers (#3642)

This commit is contained in:
sxyazi 2026-01-31 09:25:17 +08:00
parent dfdb235d74
commit a015d4a7ef
No known key found for this signature in database
33 changed files with 117 additions and 143 deletions

View file

@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
### Added
- Allow using `ps.sub()` in `init.lua` directly without a plugin ([#3638])
- New `relay-notify-push` DDS event to allow custom notification handlers ([#3642])
- New `cx.which` API to access the which component state ([#3617])
- New `ind-which-activate` DDS event to change the which component behavior ([#3608])
@ -1636,3 +1637,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
[#3633]: https://github.com/sxyazi/yazi/pull/3633
[#3634]: https://github.com/sxyazi/yazi/pull/3634
[#3638]: https://github.com/sxyazi/yazi/pull/3638
[#3642]: https://github.com/sxyazi/yazi/pull/3642

4
Cargo.lock generated
View file

@ -5538,6 +5538,7 @@ version = "26.1.22"
dependencies = [
"anyhow",
"crossterm 0.29.0",
"either",
"futures",
"hashbrown 0.16.1",
"libc",
@ -5765,6 +5766,7 @@ version = "26.1.22"
dependencies = [
"anyhow",
"crossterm 0.29.0",
"either",
"scopeguard",
"tokio",
"tracing",
@ -5835,6 +5837,7 @@ dependencies = [
"bitflags 2.10.0",
"core-foundation-sys",
"dirs",
"either",
"foldhash",
"hashbrown 0.16.1",
"libc",
@ -6056,6 +6059,7 @@ version = "26.1.22"
dependencies = [
"anyhow",
"deadpool",
"either",
"futures",
"hashbrown 0.16.1",
"parking_lot",

View file

@ -35,6 +35,7 @@ core-foundation-sys = "0.8.7"
crossterm = { version = "0.29.0", features = [ "event-stream" ] }
dirs = "6.0.0"
dyn-clone = "1.0.20"
either = { version = "1.15.0" }
foldhash = "0.2.0"
futures = "0.3.31"
globset = "0.4.18"

View file

@ -38,6 +38,7 @@ yazi-widgets = { path = "../yazi-widgets", version = "26.1.22" }
# External dependencies
anyhow = { workspace = true }
crossterm = { workspace = true }
either = { workspace = true }
futures = { workspace = true }
hashbrown = { workspace = true }
mlua = { workspace = true }

View file

@ -2,7 +2,7 @@ use anyhow::Result;
use mlua::IntoLua;
use tracing::error;
use yazi_actor::lives::Lives;
use yazi_binding::runtime_mut;
use yazi_binding::runtime_scope;
use yazi_dds::{LOCAL, Payload, REMOTE};
use yazi_macro::succ;
use yazi_plugin::LUA;
@ -31,11 +31,9 @@ impl Actor for AcceptPayload {
succ!(Lives::scope(&cx.core, || {
let body = payload.body.into_lua(&LUA)?;
for (id, cb) in handlers {
let blocking = runtime_mut!(LUA)?.critical_push(&id, true);
if let Err(e) = cb.call::<()>(body.clone()) {
if let Err(e) = runtime_scope!(LUA, &id, cb.call::<()>(body.clone())) {
error!("Failed to run `{kind}` event handler in your `{id}` plugin: {e}");
}
runtime_mut!(LUA)?.critical_pop(blocking)?;
}
Ok(())
})?);

View file

@ -3,6 +3,7 @@ use crossterm::event::MouseEventKind;
use mlua::{ObjectLike, Table};
use tracing::error;
use yazi_actor::lives::Lives;
use yazi_binding::runtime_scope;
use yazi_config::YAZI;
use yazi_macro::succ;
use yazi_parser::app::MouseOpt;
@ -20,11 +21,14 @@ impl Actor for Mouse {
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let event = yazi_binding::MouseEvent::from(opt.event);
let Some(size) = cx.term.as_ref().and_then(|t| t.size().ok()) else { succ!() };
let area = yazi_binding::elements::Rect::from(size);
let result = Lives::scope(&cx.core, move || {
let area = yazi_binding::elements::Rect::from(size);
let root = LUA.globals().raw_get::<Table>("Root")?.call_method::<Table>("new", area)?;
let root = runtime_scope!(LUA, "root", {
LUA.globals().raw_get::<Table>("Root")?.call_method::<Table>("new", area)
})?;
if matches!(event.kind, MouseEventKind::Down(_) if YAZI.mgr.mouse_events.get().draggable()) {
root.raw_set("_drag_start", event)?;

View file

@ -1,6 +1,6 @@
use anyhow::Result;
use mlua::{ErrorContext, ExternalError, IntoLua, Value};
use yazi_binding::runtime_mut;
use yazi_binding::runtime_scope;
use yazi_dds::{LOCAL, spark::{Spark, SparkKind}};
use yazi_plugin::LUA;
@ -18,11 +18,7 @@ impl Preflight {
Ok(Lives::scope(cx.core, || {
let mut body = opt.1.into_lua(&LUA)?;
for (id, cb) in handlers {
let blocking = runtime_mut!(LUA)?.critical_push(&id, true);
let result = cb.call::<Value>(&body);
runtime_mut!(LUA)?.critical_pop(blocking)?;
match result {
match runtime_scope!(LUA, &id, cb.call::<Value>(&body)) {
Ok(Value::Nil) => {
Err(format!("`{kind}` event cancelled by `{id}` plugin on preflight").into_lua_err())?
}

View file

@ -16,7 +16,10 @@ pub(super) static FILE_CACHE: RoCell<RefCell<HashMap<PtrCell<yazi_fs::File>, Any
pub struct Lives;
impl Lives {
pub fn scope<T>(core: &yazi_core::Core, f: impl FnOnce() -> mlua::Result<T>) -> mlua::Result<T> {
pub fn scope<T, F>(core: &yazi_core::Core, f: F) -> mlua::Result<T>
where
F: FnOnce() -> mlua::Result<T>,
{
FILE_CACHE.init(Default::default());
defer! { FILE_CACHE.drop(); }

View file

@ -14,7 +14,7 @@ impl Actor for ToggleAll {
const NAME: &str = "toggle_all";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
use yazi_shared::Either::*;
use either::Either::*;
let tab = cx.tab_mut();
let it = tab.current.files.iter().map(|f| &f.url);

View file

@ -1,11 +1,11 @@
use std::time::{Duration, Instant};
use std::time::Instant;
use anyhow::Result;
use yazi_core::notify::Message;
use yazi_macro::succ;
use yazi_dds::spark::SparkKind;
use yazi_macro::{act, succ};
use yazi_parser::notify::PushOpt;
use yazi_proxy::NotifyProxy;
use yazi_shared::data::Data;
use yazi_shared::{Source, data::Data};
use crate::{Actor, Ctx};
@ -24,8 +24,15 @@ impl Actor for Push {
if cx.notify.messages.iter().all(|m| m != &msg) {
cx.notify.messages.push(msg);
NotifyProxy::tick(Duration::ZERO);
act!(notify:tick, cx)?;
}
succ!();
}
fn hook(cx: &Ctx, _: &Self::Options) -> Option<SparkKind> {
match cx.source() {
Source::Relay => Some(SparkKind::RelayNotifyPush),
_ => None,
}
}
}

View file

@ -4,7 +4,7 @@ use anyhow::Result;
use ratatui::layout::Rect;
use yazi_core::notify::Notify;
use yazi_emulator::Dimension;
use yazi_macro::succ;
use yazi_macro::{render, render_partial, succ};
use yazi_parser::notify::TickOpt;
use yazi_proxy::NotifyProxy;
use yazi_shared::data::Data;
@ -52,13 +52,13 @@ impl Actor for Tick {
} else if let Some(min) = timeouts.iter().min() {
*min
} else {
succ!();
succ!(render!());
};
cx.notify.tick_handle = Some(tokio::spawn(async move {
tokio::time::sleep(interval).await;
NotifyProxy::tick(interval);
}));
succ!();
succ!(render_partial!());
}
}

View file

@ -1,7 +1,5 @@
use std::ops::{Deref, DerefMut};
use yazi_shared::Either;
use crate::Adapter;
pub(super) struct Adapters(Vec<Adapter>);
@ -17,12 +15,7 @@ impl DerefMut for Adapters {
}
impl From<&yazi_emulator::Emulator> for Adapters {
fn from(value: &yazi_emulator::Emulator) -> Self {
match value.kind {
Either::Left(b) => b.into(),
Either::Right(u) => u.into(),
}
}
fn from(value: &yazi_emulator::Emulator) -> Self { value.kind.either_into() }
}
impl From<yazi_emulator::Brand> for Adapters {

View file

@ -19,7 +19,7 @@ pub fn init() -> anyhow::Result<()> {
// Emulator detection
let mut emulator = Emulator::detect().unwrap_or_default();
TMUX.set(emulator.kind.is_left_and(|&b| b == Brand::Tmux));
TMUX.set(emulator.kind.left() == Some(Brand::Tmux));
// Tmux support
if TMUX.get() {

View file

@ -14,6 +14,19 @@ macro_rules! runtime_mut {
}};
}
#[macro_export]
macro_rules! runtime_scope {
($lua:ident, $id:expr, $block:expr) => {{
let mut f = || {
let blocking = $crate::runtime_mut!($lua)?.critical_push($id, true);
let result = (|| $block)();
$crate::runtime_mut!($lua)?.critical_pop(blocking)?;
result
};
f()
}};
}
#[macro_export]
macro_rules! deprecate {
($lua:ident, $tt:tt) => {{

View file

@ -164,7 +164,7 @@ impl Actions {
fn file1_output() -> String {
use std::io::Write;
let p = env::temp_dir().join(format!("yazi-debug-{}", timestamp_us()));
let p = env::temp_dir().join(format!(".yazi-debug-{}.tmp", timestamp_us()));
std::fs::File::create_new(&p).map(|mut f| f.write_all(b"Hello, World!")).ok();
let cmd = env::var_os("YAZI_FILE_ONE").unwrap_or("file".into());

View file

@ -48,6 +48,7 @@ clap = { workspace = true }
clap_complete = "4.5.65"
clap_complete_fig = "4.5.2"
clap_complete_nushell = "4.5.10"
serde = { workspace = true }
serde_json = { workspace = true }
vergen-gitcl = { version = "9.1.0", features = [ "build" ] }

View file

@ -2,7 +2,7 @@ use std::{borrow::Cow, ffi::OsString};
use anyhow::{Result, bail};
use clap::{Parser, Subcommand};
use yazi_shared::{Either, Id};
use yazi_shared::Id;
#[derive(Parser)]
#[command(name = "Ya", about, long_about = None)]
@ -139,9 +139,16 @@ macro_rules! impl_emit_body {
impl $name {
#[allow(dead_code)]
pub(super) fn body(self) -> Result<String> {
let cmd: Vec<_> = [Either::Left(self.name)]
#[derive(serde::Serialize)]
#[serde(untagged)]
enum Elem {
Name(String),
Arg(Vec<u8>),
}
let cmd: Vec<_> = [Elem::Name(self.name)]
.into_iter()
.chain(self.args.into_iter().map(|s| Either::Right(s.into_encoded_bytes())))
.chain(self.args.into_iter().map(|s| Elem::Arg(s.into_encoded_bytes())))
.collect();
Ok(serde_json::to_string(&cmd)?)
}

View file

@ -12,6 +12,8 @@ pub enum SparkKind {
KeyQuit,
// which:activate
IndWhichActivate,
// notify:push
RelayNotifyPush,
}
impl AsRef<str> for SparkKind {
@ -27,6 +29,8 @@ impl AsRef<str> for SparkKind {
Self::KeyQuit => "key-quit",
// which:activate
Self::IndWhichActivate => "ind-which-activate",
// notify:push
Self::RelayNotifyPush => "relay-notify-push",
}
}
}

View file

@ -166,6 +166,8 @@ impl<'a> Spark<'a> {
KeyQuit => Self::Quit(<_>::from_lua(value, lua)?),
// which:activate
IndWhichActivate => Self::WhichActivate(<_>::from_lua(value, lua)?),
// notify:push
RelayNotifyPush => Self::NotifyPush(<_>::from_lua(value, lua)?),
})
}
}

View file

@ -19,6 +19,7 @@ yazi-tty = { path = "../yazi-tty", version = "26.1.22" }
# External dependencies
anyhow = { workspace = true }
crossterm = { workspace = true }
either = { workspace = true }
scopeguard = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }

View file

@ -2,10 +2,11 @@ use std::{io::BufWriter, time::Duration};
use anyhow::Result;
use crossterm::{cursor::{RestorePosition, SavePosition}, execute, style::Print, terminal::{disable_raw_mode, enable_raw_mode}};
use either::Either;
use scopeguard::defer;
use tokio::time::sleep;
use tracing::{debug, error, warn};
use yazi_shared::{Either, RoCell};
use yazi_shared::RoCell;
use yazi_tty::{Handle, TTY};
use crate::{Brand, Dimension, Mux, TMUX, Unknown};

View file

@ -4,7 +4,9 @@ use anyhow::Result;
use crossterm::{cursor::{MoveTo, SetCursorStyle, Show}, execute, queue, terminal::{BeginSynchronizedUpdate, EndSynchronizedUpdate}};
use ratatui::{CompletedFrame, backend::{Backend, CrosstermBackend}, buffer::Buffer, layout::Position};
use yazi_actor::{Ctx, lives::Lives};
use yazi_binding::runtime_scope;
use yazi_macro::{act, succ};
use yazi_plugin::LUA;
use yazi_shared::{data::Data, event::NEED_RENDER};
use yazi_tty::TTY;
use yazi_widgets::COLLISION;
@ -26,7 +28,9 @@ impl App {
let collision = COLLISION.swap(false, Ordering::Relaxed);
let frame = term
.draw(|f| {
_ = Lives::scope(&self.core, || Ok(f.render_widget(Root::new(&self.core), f.area())));
_ = Lives::scope(&self.core, || {
runtime_scope!(LUA, "root", Ok(f.render_widget(Root::new(&self.core), f.area())))
});
})
.unwrap();
@ -57,9 +61,11 @@ impl App {
let frame = term
.draw_partial(|f| {
_ = Lives::scope(&self.core, || {
f.render_widget(crate::tasks::Progress::new(&self.core), f.area());
f.render_widget(crate::notify::Notify::new(&self.core), f.area());
Ok(())
runtime_scope!(LUA, "root", {
f.render_widget(crate::tasks::Progress::new(&self.core), f.area());
f.render_widget(crate::notify::Notify::new(&self.core), f.area());
Ok(())
})
});
})
.unwrap();

View file

@ -22,6 +22,7 @@ anyhow = { workspace = true }
arc-swap = "1.8.0"
bitflags = { workspace = true }
dirs = { workspace = true }
either = { workspace = true }
foldhash = { workspace = true }
hashbrown = { workspace = true }
parking_lot = { workspace = true }

View file

@ -1,7 +1,7 @@
use std::{collections::VecDeque, future::poll_fn, io, mem, path::{Path, PathBuf}, pin::Pin, task::{Poll, ready}, time::{Duration, Instant}};
use either::Either;
use tokio::task::JoinHandle;
use yazi_shared::Either;
type Task = Either<PathBuf, std::fs::ReadDir>;
@ -84,7 +84,7 @@ impl SizeCalculator {
};
}
let Some(next) = front.right_mut()?.next() else {
let Some(next) = front.as_mut().right()?.next() else {
pop_and_continue!();
};

View file

@ -182,8 +182,8 @@ fn final_path(path: &Path) -> io::Result<PathBuf> {
fn final_path(path: &Path) -> io::Result<PathBuf> {
use std::{ffi::OsString, fs::File, os::windows::{ffi::OsStringExt, fs::OpenOptionsExt, io::AsRawHandle}};
use either::Either;
use windows_sys::Win32::{Foundation::HANDLE, Storage::FileSystem::{FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, GetFinalPathNameByHandleW, VOLUME_NAME_DOS}};
use yazi_shared::Either;
let file = std::fs::OpenOptions::new()
.access_mode(0)
@ -214,7 +214,8 @@ fn final_path(path: &Path) -> io::Result<PathBuf> {
match inner(&file, &mut [0u16; 512])? {
Either::Left(path) => Ok(path),
Either::Right(len) => inner(&file, &mut vec![0u16; len as usize])?
.left_or_err(|| io::Error::new(io::ErrorKind::InvalidData, "path too long")),
.left()
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "path too long")),
}
}

View file

@ -3,7 +3,7 @@ use std::{str::FromStr, time::Duration};
use anyhow::anyhow;
use mlua::{FromLua, IntoLua, Lua, LuaSerdeExt, Value};
use serde::{Deserialize, Serialize};
use serde_with::{DurationSeconds, serde_as};
use serde_with::{DurationSecondsWithFrac, serde_as};
use yazi_config::{Style, THEME};
use yazi_shared::event::CmdCow;
@ -13,7 +13,7 @@ pub struct PushOpt {
pub title: String,
pub content: String,
pub level: PushLevel,
#[serde_as(as = "DurationSeconds<f64>")] // FIXME
#[serde_as(as = "DurationSecondsWithFrac<f64>")]
pub timeout: Duration,
}

View file

@ -4,7 +4,7 @@ use anyhow::bail;
use mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
use yazi_shared::event::CmdCow;
#[derive(Debug)]
#[derive(Debug, Default)]
pub struct TickOpt {
pub interval: Duration,
}

View file

@ -26,6 +26,7 @@ impl Deref for Loader {
impl Default for Loader {
fn default() -> Self {
let cache = HashMap::from_iter([
// Plugins
("archive".to_owned(), preset!("plugins/archive").into()),
("code".to_owned(), preset!("plugins/code").into()),
("dds".to_owned(), preset!("plugins/dds").into()),
@ -51,6 +52,22 @@ impl Default for Loader {
("vfs".to_owned(), preset!("plugins/vfs").into()),
("video".to_owned(), preset!("plugins/video").into()),
("zoxide".to_owned(), preset!("plugins/zoxide").into()),
// Components
("current".to_owned(), [][..].into()),
("entity".to_owned(), [][..].into()),
("header".to_owned(), [][..].into()),
("linemode".to_owned(), [][..].into()),
("marker".to_owned(), [][..].into()),
("modal".to_owned(), [][..].into()),
("parent".to_owned(), [][..].into()),
("preview".to_owned(), [][..].into()),
("progress".to_owned(), [][..].into()),
("rail".to_owned(), [][..].into()),
("root".to_owned(), [][..].into()),
("status".to_owned(), [][..].into()),
("tab".to_owned(), [][..].into()),
("tabs".to_owned(), [][..].into()),
("tasks".to_owned(), [][..].into()),
]);
Self { cache: RwLock::new(cache) }
}

View file

@ -1,7 +1,7 @@
use anyhow::{Context, Result};
use futures::executor::block_on;
use mlua::Lua;
use yazi_binding::{Runtime, runtime_mut};
use yazi_binding::{Runtime, runtime_scope};
use yazi_boot::BOOT;
use yazi_macro::plugin_preset as preset;
use yazi_shared::RoCell;
@ -65,9 +65,7 @@ fn stage_2(lua: &'static Lua) -> mlua::Result<()> {
lua.load(preset!("compat")).set_name("compat.lua").exec()?;
if let Ok(b) = std::fs::read(BOOT.config_dir.join("init.lua")) {
let blocking = runtime_mut!(lua)?.critical_push("init", true);
block_on(lua.load(b).set_name("init.lua").exec_async())?;
runtime_mut!(lua)?.critical_pop(blocking)?;
runtime_scope!(lua, "init", block_on(lua.load(b).set_name("init.lua").exec_async()))?;
}
Ok(())

View file

@ -1,89 +0,0 @@
use serde::Serialize;
#[derive(Clone, Copy, Debug)]
pub enum Either<L, R> {
Left(L),
Right(R),
}
impl<L, R> Either<L, R> {
pub fn left(&self) -> Option<&L> {
match self {
Self::Left(l) => Some(l),
_ => None,
}
}
pub fn right(&self) -> Option<&R> {
match self {
Self::Right(r) => Some(r),
_ => None,
}
}
pub fn left_mut(&mut self) -> Option<&mut L> {
match self {
Self::Left(l) => Some(l),
_ => None,
}
}
pub fn right_mut(&mut self) -> Option<&mut R> {
match self {
Self::Right(r) => Some(r),
_ => None,
}
}
pub fn is_left_and<F: FnOnce(&L) -> bool>(&self, f: F) -> bool {
self.left().map(f).unwrap_or(false)
}
pub fn is_right_and<F: FnOnce(&R) -> bool>(&self, f: F) -> bool {
self.right().map(f).unwrap_or(false)
}
pub fn into_left(self) -> Option<L> {
match self {
Self::Left(l) => Some(l),
_ => None,
}
}
pub fn into_right(self) -> Option<R> {
match self {
Self::Right(r) => Some(r),
_ => None,
}
}
pub fn left_or_err<E, F: FnOnce() -> E>(self, f: F) -> Result<L, E> {
match self {
Self::Left(l) => Ok(l),
_ => Err(f()),
}
}
pub fn right_or_err<E, F: FnOnce() -> E>(self, f: F) -> Result<R, E> {
match self {
Self::Right(r) => Ok(r),
_ => Err(f()),
}
}
}
impl<L, R> Serialize for Either<L, R>
where
L: Serialize,
R: Serialize,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Self::Left(l) => l.serialize(serializer),
Self::Right(r) => r.serialize(serializer),
}
}
}

View file

@ -1,6 +1,6 @@
yazi_macro::mod_pub!(data errors event loc path pool scheme shell strand translit url wtf8);
yazi_macro::mod_flat!(alias bytes chars completion_token condition debounce either env id layer localset natsort os predictor ro_cell source sync_cell terminal tests throttle time utf8);
yazi_macro::mod_flat!(alias bytes chars completion_token condition debounce env id layer localset natsort os predictor ro_cell source sync_cell terminal tests throttle time utf8);
pub fn init() {
LOCAL_SET.with(tokio::task::LocalSet::new);

View file

@ -21,6 +21,7 @@ yazi-shared = { path = "../yazi-shared", version = "26.1.22" }
# External dependencies
anyhow = { workspace = true }
deadpool = { version = "0.12.3", default-features = false, features = [ "managed", "rt_tokio_1" ] }
either = { workspace = true }
futures = { workspace = true }
hashbrown = { workspace = true }
parking_lot = { workspace = true }

View file

@ -1,7 +1,8 @@
use std::{collections::VecDeque, io, time::{Duration, Instant}};
use either::Either;
use yazi_fs::provider::{DirReader, FileHolder};
use yazi_shared::{Either, url::{AsUrl, UrlBuf}};
use yazi_shared::url::{AsUrl, UrlBuf};
use super::ReadDir;
@ -66,7 +67,7 @@ impl SizeCalculator {
};
}
let Ok(Some(ent)) = front.right_mut()?.next().await else {
let Ok(Some(ent)) = front.as_mut().right()?.next().await else {
pop_and_continue!();
};