feat: subscribe to and intercept app-level events through DDS (#3632)

This commit is contained in:
三咲雅 misaki masa 2026-01-29 00:34:28 +08:00 committed by GitHub
parent 583345296f
commit 6d942ea011
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
82 changed files with 815 additions and 537 deletions

5
Cargo.lock generated
View file

@ -5542,6 +5542,7 @@ dependencies = [
"libc", "libc",
"mlua", "mlua",
"paste", "paste",
"ratatui",
"scopeguard", "scopeguard",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
@ -5551,6 +5552,7 @@ dependencies = [
"yazi-config", "yazi-config",
"yazi-core", "yazi-core",
"yazi-dds", "yazi-dds",
"yazi-emulator",
"yazi-fs", "yazi-fs",
"yazi-macro", "yazi-macro",
"yazi-parser", "yazi-parser",
@ -5558,6 +5560,7 @@ dependencies = [
"yazi-proxy", "yazi-proxy",
"yazi-scheduler", "yazi-scheduler",
"yazi-shared", "yazi-shared",
"yazi-term",
"yazi-tty", "yazi-tty",
"yazi-vfs", "yazi-vfs",
"yazi-watcher", "yazi-watcher",
@ -5704,6 +5707,7 @@ dependencies = [
"indexmap 2.13.0", "indexmap 2.13.0",
"parking_lot", "parking_lot",
"ratatui", "ratatui",
"serde",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"tokio-util", "tokio-util",
@ -5868,6 +5872,7 @@ dependencies = [
"hashbrown 0.16.1", "hashbrown 0.16.1",
"mlua", "mlua",
"ordered-float 5.1.0", "ordered-float 5.1.0",
"ratatui",
"serde", "serde",
"serde_with", "serde_with",
"tokio", "tokio",

View file

@ -8,7 +8,7 @@ function bugReportBody(creator, content, hash) {
return null return null
} }
return `Hey @${creator}, thank you for opening this issue to help us improve Yazi, appreciate it! return `Hey @${creator}, thank you for opening the issue to help improve Yazi, appreciate it!
I noticed that you did not correctly follow the issue template. Please ensure that: I noticed that you did not correctly follow the issue template. Please ensure that:
@ -18,7 +18,7 @@ I noticed that you did not correctly follow the issue template. Please ensure th
Issues with \`${LABEL_NAME}\` will be marked ready once edited with the proper content, or closed after 2 days of inactivity. Issues with \`${LABEL_NAME}\` will be marked ready once edited with the proper content, or closed after 2 days of inactivity.
Our maintainers work on Yazi in a personal capacity, and debug info helps them work efficiently, understand your setup quickly, and find a more appropriate solution. Thank you for the understanding! Our maintainers work on Yazi in their free time, this helps them work efficiently, understand your setup quickly, and find a more appropriate solution. Thanks for your understanding!
` `
} }
@ -27,7 +27,7 @@ function featureRequestBody(creator, content) {
return null return null
} }
return `Hey @${creator}, thank you for opening this issue to help us improve Yazi, appreciate it! return `Hey @${creator}, thank you for opening the issue to help improve Yazi, appreciate it!
I noticed that you did not correctly follow the issue template. Please ensure that: I noticed that you did not correctly follow the issue template. Please ensure that:
@ -37,7 +37,7 @@ I noticed that you did not correctly follow the issue template. Please ensure th
Issues with \`${LABEL_NAME}\` will be marked ready once edited with the proper content, or closed after 2 days of inactivity. Issues with \`${LABEL_NAME}\` will be marked ready once edited with the proper content, or closed after 2 days of inactivity.
Our maintainers work on Yazi in a personal capacity, and debug info helps them work efficiently, understand your setup quickly, and find a more appropriate solution. Thank you for the understanding! Our maintainers work on Yazi in their free time, this helps them work efficiently, understand your setup quickly, and find a more appropriate solution. Thanks for your understanding!
` `
} }

View file

@ -21,6 +21,7 @@ yazi-boot = { path = "../yazi-boot", version = "26.1.22" }
yazi-config = { path = "../yazi-config", version = "26.1.22" } yazi-config = { path = "../yazi-config", version = "26.1.22" }
yazi-core = { path = "../yazi-core", version = "26.1.22" } yazi-core = { path = "../yazi-core", version = "26.1.22" }
yazi-dds = { path = "../yazi-dds", version = "26.1.22" } yazi-dds = { path = "../yazi-dds", version = "26.1.22" }
yazi-emulator = { path = "../yazi-emulator", version = "26.1.22" }
yazi-fs = { path = "../yazi-fs", version = "26.1.22" } yazi-fs = { path = "../yazi-fs", version = "26.1.22" }
yazi-macro = { path = "../yazi-macro", version = "26.1.22" } yazi-macro = { path = "../yazi-macro", version = "26.1.22" }
yazi-parser = { path = "../yazi-parser", version = "26.1.22" } yazi-parser = { path = "../yazi-parser", version = "26.1.22" }
@ -28,6 +29,7 @@ yazi-plugin = { path = "../yazi-plugin", version = "26.1.22" }
yazi-proxy = { path = "../yazi-proxy", version = "26.1.22" } yazi-proxy = { path = "../yazi-proxy", version = "26.1.22" }
yazi-scheduler = { path = "../yazi-scheduler", version = "26.1.22" } yazi-scheduler = { path = "../yazi-scheduler", version = "26.1.22" }
yazi-shared = { path = "../yazi-shared", version = "26.1.22" } yazi-shared = { path = "../yazi-shared", version = "26.1.22" }
yazi-term = { path = "../yazi-term", version = "26.1.22" }
yazi-tty = { path = "../yazi-tty", version = "26.1.22" } yazi-tty = { path = "../yazi-tty", version = "26.1.22" }
yazi-vfs = { path = "../yazi-vfs", version = "26.1.22" } yazi-vfs = { path = "../yazi-vfs", version = "26.1.22" }
yazi-watcher = { path = "../yazi-watcher", version = "26.1.22" } yazi-watcher = { path = "../yazi-watcher", version = "26.1.22" }
@ -40,6 +42,7 @@ futures = { workspace = true }
hashbrown = { workspace = true } hashbrown = { workspace = true }
mlua = { workspace = true } mlua = { workspace = true }
paste = { workspace = true } paste = { workspace = true }
ratatui = { workspace = true }
scopeguard = { workspace = true } scopeguard = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
tokio-stream = { workspace = true } tokio-stream = { workspace = true }

View file

@ -1,4 +1,4 @@
use anyhow::{Result, bail}; use anyhow::Result;
use mlua::IntoLua; use mlua::IntoLua;
use tracing::error; use tracing::error;
use yazi_actor::lives::Lives; use yazi_actor::lives::Lives;
@ -6,16 +6,18 @@ use yazi_binding::runtime_mut;
use yazi_dds::{LOCAL, Payload, REMOTE}; use yazi_dds::{LOCAL, Payload, REMOTE};
use yazi_macro::succ; use yazi_macro::succ;
use yazi_plugin::LUA; use yazi_plugin::LUA;
use yazi_shared::{data::Data, event::CmdCow}; use yazi_shared::data::Data;
use crate::app::App; use crate::{Actor, Ctx};
impl App { pub struct AcceptPayload;
pub(crate) fn accept_payload(&self, mut c: CmdCow) -> Result<Data> {
let Some(payload) = c.take_any2::<Payload>("payload").transpose()? else {
bail!("'payload' is required for accept_payload");
};
impl Actor for AcceptPayload {
type Options = Payload<'static>;
const NAME: &str = "accept_payload";
fn act(cx: &mut Ctx, payload: Payload) -> Result<Data> {
let kind = payload.body.kind().to_owned(); let kind = payload.body.kind().to_owned();
let lock = if payload.receiver == 0 || payload.receiver != payload.sender { let lock = if payload.receiver == 0 || payload.receiver != payload.sender {
REMOTE.read() REMOTE.read()
@ -26,7 +28,7 @@ impl App {
let Some(handlers) = lock.get(&kind).filter(|&m| !m.is_empty()).cloned() else { succ!() }; let Some(handlers) = lock.get(&kind).filter(|&m| !m.is_empty()).cloned() else { succ!() };
drop(lock); drop(lock);
succ!(Lives::scope(&self.core, || { succ!(Lives::scope(&cx.core, || {
let body = payload.body.into_lua(&LUA)?; let body = payload.body.into_lua(&LUA)?;
for (id, cb) in handlers { for (id, cb) in handlers {
let blocking = runtime_mut!(LUA)?.critical_push(&id, true); let blocking = runtime_mut!(LUA)?.critical_push(&id, true);

View file

@ -1,30 +1,33 @@
use anyhow::Result; use anyhow::Result;
use yazi_actor::Ctx; use yazi_actor::Ctx;
use yazi_boot::BOOT; use yazi_boot::BOOT;
use yazi_macro::act; use yazi_macro::{act, succ};
use yazi_parser::{VoidOpt, mgr::CdSource}; use yazi_parser::{VoidOpt, mgr::CdSource};
use yazi_shared::{data::Data, strand::StrandLike, url::UrlLike}; use yazi_shared::{data::Data, strand::StrandLike, url::UrlLike};
use crate::app::App; use crate::Actor;
impl App { pub struct Bootstrap;
pub fn bootstrap(&mut self, _: VoidOpt) -> Result<Data> {
impl Actor for Bootstrap {
type Options = VoidOpt;
const NAME: &str = "bootstrap";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
for (i, file) in BOOT.files.iter().enumerate() { for (i, file) in BOOT.files.iter().enumerate() {
let tabs = &mut self.core.mgr.tabs; let tabs = &mut cx.core.mgr.tabs;
if tabs.len() <= i { if tabs.len() <= i {
tabs.push(Default::default()); tabs.push(Default::default());
} }
let cx = &mut Ctx::active(&mut self.core);
cx.tab = i; cx.tab = i;
if file.is_empty() { if file.is_empty() {
act!(mgr:cd, cx, (BOOT.cwds[i].clone(), CdSource::Tab))?; act!(mgr:cd, cx, (BOOT.cwds[i].clone(), CdSource::Tab))?;
} else if let Ok(u) = BOOT.cwds[i].try_join(file) { } else if let Ok(u) = BOOT.cwds[i].try_join(file) {
act!(mgr:reveal, cx, (u, CdSource::Tab))?; act!(mgr:reveal, cx, (u, CdSource::Tab))?;
} }
} }
succ!();
act!(render, self)
} }
} }

View file

@ -0,0 +1,23 @@
use anyhow::Result;
use yazi_macro::act;
use yazi_parser::{app::DeprecateOpt, notify::{PushLevel, PushOpt}};
use yazi_shared::data::Data;
use crate::{Actor, Ctx};
pub struct Deprecate;
impl Actor for Deprecate {
type Options = DeprecateOpt;
const NAME: &str = "deprecate";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
act!(notify:push, cx, PushOpt {
title: "Deprecated API".to_owned(),
content: opt.content.into_owned(),
level: PushLevel::Warn,
timeout: std::time::Duration::from_secs(20),
})
}
}

View file

@ -0,0 +1,17 @@
use anyhow::Result;
use yazi_actor::Ctx;
use yazi_macro::act;
use yazi_parser::VoidOpt;
use yazi_shared::data::Data;
use crate::Actor;
pub struct Focus;
impl Actor for Focus {
type Options = VoidOpt;
const NAME: &str = "focus";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> { act!(mgr:refresh, cx) }
}

View file

@ -4,14 +4,12 @@ yazi_macro::mod_flat!(
deprecate deprecate
focus focus
mouse mouse
notify
plugin plugin
plugin_do
quit quit
reflow reflow
render
resize resize
resume resume
stop stop
update_notify
update_progress update_progress
); );

View file

@ -9,14 +9,20 @@ use yazi_parser::app::MouseOpt;
use yazi_plugin::LUA; use yazi_plugin::LUA;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use crate::app::App; use crate::{Actor, Ctx};
impl App { pub struct Mouse;
pub fn mouse(&mut self, opt: MouseOpt) -> Result<Data> {
impl Actor for Mouse {
type Options = MouseOpt;
const NAME: &str = "mouse";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let event = yazi_plugin::bindings::MouseEvent::from(opt.event); let event = yazi_plugin::bindings::MouseEvent::from(opt.event);
let Some(size) = self.term.as_ref().and_then(|t| t.size().ok()) else { succ!() }; let Some(size) = cx.term.as_ref().and_then(|t| t.size().ok()) else { succ!() };
let result = Lives::scope(&self.core, move || { let result = Lives::scope(&cx.core, move || {
let area = yazi_binding::elements::Rect::from(size); let area = yazi_binding::elements::Rect::from(size);
let root = LUA.globals().raw_get::<Table>("Root")?.call_method::<Table>("new", area)?; let root = LUA.globals().raw_get::<Table>("Root")?.call_method::<Table>("new", area)?;

View file

@ -0,0 +1,38 @@
use anyhow::Result;
use yazi_macro::{act, succ};
use yazi_parser::app::{PluginMode, PluginOpt};
use yazi_plugin::loader::LOADER;
use yazi_proxy::{AppProxy, NotifyProxy};
use yazi_shared::data::Data;
use crate::{Actor, Ctx};
pub struct Plugin;
impl Actor for Plugin {
type Options = PluginOpt;
const NAME: &str = "plugin";
fn act(cx: &mut Ctx, mut opt: Self::Options) -> Result<Data> {
let mut hits = false;
if let Some(chunk) = LOADER.read().get(&*opt.id) {
hits = true;
opt.mode = opt.mode.auto_then(chunk.sync_entry);
}
if opt.mode == PluginMode::Async {
succ!(cx.core.tasks.scheduler.plugin_entry(opt));
} else if opt.mode == PluginMode::Sync && hits {
return act!(app:plugin_do, cx, opt);
}
tokio::spawn(async move {
match LOADER.ensure(&opt.id, |_| ()).await {
Ok(()) => AppProxy::plugin_do(opt),
Err(e) => NotifyProxy::push_error("Plugin load failed", e),
}
});
succ!();
}
}

View file

@ -2,52 +2,35 @@ use anyhow::Result;
use mlua::ObjectLike; use mlua::ObjectLike;
use scopeguard::defer; use scopeguard::defer;
use tracing::{error, warn}; use tracing::{error, warn};
use yazi_actor::lives::Lives;
use yazi_binding::runtime_mut; use yazi_binding::runtime_mut;
use yazi_dds::Sendable; use yazi_dds::Sendable;
use yazi_macro::succ; use yazi_macro::succ;
use yazi_parser::app::{PluginMode, PluginOpt}; use yazi_parser::app::{PluginMode, PluginOpt};
use yazi_plugin::{LUA, loader::{LOADER, Loader}}; use yazi_plugin::{LUA, loader::{LOADER, Loader}};
use yazi_proxy::AppProxy; use yazi_proxy::NotifyProxy;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use crate::app::App; use crate::{Actor, Ctx, lives::Lives};
impl App { pub struct PluginDo;
pub(crate) fn plugin(&mut self, mut opt: PluginOpt) -> Result<Data> {
let mut hits = false;
if let Some(chunk) = LOADER.read().get(&*opt.id) {
hits = true;
opt.mode = opt.mode.auto_then(chunk.sync_entry);
}
if opt.mode == PluginMode::Async { impl Actor for PluginDo {
succ!(self.core.tasks.plugin_entry(opt)); type Options = PluginOpt;
} else if opt.mode == PluginMode::Sync && hits {
return self.plugin_do(opt);
}
tokio::spawn(async move { const NAME: &str = "plugin_do";
match LOADER.ensure(&opt.id, |_| ()).await {
Ok(()) => AppProxy::plugin_do(opt),
Err(e) => AppProxy::notify_error("Plugin load failed", e),
}
});
succ!();
}
pub(crate) fn plugin_do(&mut self, opt: PluginOpt) -> Result<Data> { fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let loader = LOADER.read(); let loader = LOADER.read();
let Some(chunk) = loader.get(&*opt.id) else { let Some(chunk) = loader.get(&*opt.id) else {
succ!(warn!("plugin `{}` not found", opt.id)); succ!(warn!("plugin `{}` not found", opt.id));
}; };
if let Err(e) = Loader::compatible_or_error(&opt.id, chunk) { if let Err(e) = Loader::compatible_or_error(&opt.id, chunk) {
succ!(AppProxy::notify_error("Incompatible plugin", e)); succ!(NotifyProxy::push_error("Incompatible plugin", e));
} }
if opt.mode.auto_then(chunk.sync_entry) != PluginMode::Sync { if opt.mode.auto_then(chunk.sync_entry) != PluginMode::Sync {
succ!(self.core.tasks.plugin_entry(opt)); succ!(cx.core.tasks.scheduler.plugin_entry(opt));
} }
let blocking = runtime_mut!(LUA)?.critical_push(&opt.id, true); let blocking = runtime_mut!(LUA)?.critical_push(&opt.id, true);
@ -59,7 +42,7 @@ impl App {
}; };
drop(loader); drop(loader);
let result = Lives::scope(&self.core, || { let result = Lives::scope(&cx.core, || {
if let Some(cb) = opt.callback { if let Some(cb) = opt.callback {
cb(&LUA, plugin) cb(&LUA, plugin)
} else { } else {

View file

@ -0,0 +1,47 @@
use anyhow::Result;
use yazi_boot::ARGS;
use yazi_fs::provider::{Provider, local::Local};
use yazi_parser::app::QuitOpt;
use yazi_shared::{data::Data, strand::{StrandBuf, StrandLike, ToStrand}};
use yazi_term::Term;
use crate::{Actor, Ctx};
pub struct Quit;
impl Actor for Quit {
type Options = QuitOpt;
const NAME: &str = "quit";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
cx.core.tasks.shutdown();
cx.core.mgr.shutdown();
futures::executor::block_on(async {
_ = futures::join!(
yazi_dds::shutdown(),
yazi_dds::STATE.drain(),
Self::cwd_to_file(cx, opt.no_cwd_file),
Self::selected_to_file(opt.selected)
);
});
Term::goodbye(|| opt.code);
}
}
impl Quit {
async fn cwd_to_file(cx: &Ctx<'_>, no: bool) {
if let Some(p) = ARGS.cwd_file.as_ref().filter(|_| !no) {
let cwd = cx.core.mgr.cwd().to_strand();
Local::regular(p).write(cwd.encoded_bytes()).await.ok();
}
}
async fn selected_to_file(selected: Option<StrandBuf>) {
if let (Some(s), Some(p)) = (selected, &ARGS.chooser_file) {
Local::regular(p).write(s.encoded_bytes()).await.ok();
}
}
}

View file

@ -5,18 +5,24 @@ use tracing::error;
use yazi_actor::lives::Lives; use yazi_actor::lives::Lives;
use yazi_config::LAYOUT; use yazi_config::LAYOUT;
use yazi_macro::{render, succ}; use yazi_macro::{render, succ};
use yazi_parser::VoidOpt; use yazi_parser::app::ReflowOpt;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use crate::{Root, app::App}; use crate::{Actor, Ctx};
impl App { pub struct Reflow;
pub fn reflow(&mut self, _: VoidOpt) -> Result<Data> {
let Some(size) = self.term.as_ref().and_then(|t| t.size().ok()) else { succ!() }; impl Actor for Reflow {
type Options = ReflowOpt;
const NAME: &str = "reflow";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let Some(size) = cx.term.as_ref().and_then(|t| t.size().ok()) else { succ!() };
let mut layout = LAYOUT.get(); let mut layout = LAYOUT.get();
let result = Lives::scope(&self.core, || { let result = Lives::scope(&cx.core, || {
let comps = Root::reflow((Position::ORIGIN, size).into())?; let comps = (opt.reflow)((Position::ORIGIN, size).into())?;
for v in comps.sequence_values::<Value>() { for v in comps.sequence_values::<Value>() {
let Value::Table(t) = v? else { let Value::Table(t) = v? else {

View file

@ -0,0 +1,25 @@
use anyhow::Result;
use yazi_actor::Ctx;
use yazi_macro::act;
use yazi_parser::app::ReflowOpt;
use yazi_shared::data::Data;
use crate::Actor;
pub struct Resize;
impl Actor for Resize {
type Options = ReflowOpt;
const NAME: &str = "resize";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
act!(app:reflow, cx, opt)?;
cx.core.current_mut().arrow(0);
cx.core.parent_mut().map(|f| f.arrow(0));
cx.core.current_mut().sync_page(true);
act!(mgr:peek, cx)
}
}

View file

@ -0,0 +1,28 @@
use anyhow::Result;
use yazi_macro::{act, render, succ};
use yazi_parser::app::ResumeOpt;
use yazi_shared::data::Data;
use yazi_term::Term;
use crate::{Actor, Ctx};
pub struct Resume;
impl Actor for Resume {
type Options = ResumeOpt;
const NAME: &str = "resume";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
cx.core.active_mut().preview.reset();
*cx.term = Some(Term::start().unwrap());
// While the app resumes, it's possible that the terminal size has changed.
// We need to trigger a resize, and render the UI based on the resized area.
act!(app:resize, cx, opt.reflow)?;
opt.tx.send((true, opt.token))?;
succ!(render!());
}
}

View file

@ -3,18 +3,24 @@ use yazi_macro::succ;
use yazi_parser::app::StopOpt; use yazi_parser::app::StopOpt;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use crate::app::App; use crate::{Actor, Ctx};
impl App { pub struct Stop;
pub fn stop(&mut self, opt: StopOpt) -> Result<Data> {
self.core.active_mut().preview.reset_image(); impl Actor for Stop {
type Options = StopOpt;
const NAME: &str = "stop";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
cx.core.active_mut().preview.reset_image();
// We need to destroy the `term` first before stopping the `signals` // We need to destroy the `term` first before stopping the `signals`
// to prevent any signal from triggering the term to render again // to prevent any signal from triggering the term to render again
// while the app is being suspended. // while the app is being suspended.
self.term = None; *cx.term = None;
self.signals.stop(opt.token); opt.tx.send((false, opt.token))?;
succ!(); succ!();
} }

View file

@ -1,15 +1,21 @@
use anyhow::Result; use anyhow::Result;
use yazi_actor::Ctx; use yazi_actor::Ctx;
use yazi_macro::{act, render, succ}; use yazi_macro::{act, render, render_partial, succ};
use yazi_parser::app::UpdateProgressOpt; use yazi_parser::app::UpdateProgressOpt;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use crate::app::App; use crate::Actor;
impl App { pub struct UpdateProgress;
pub(crate) fn update_progress(&mut self, opt: UpdateProgressOpt) -> Result<Data> {
impl Actor for UpdateProgress {
type Options = UpdateProgressOpt;
const NAME: &str = "update_progress";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
// Update the progress of all tasks. // Update the progress of all tasks.
let tasks = &mut self.core.tasks; let tasks = &mut cx.core.tasks;
let progressed = tasks.summary != opt.summary; let progressed = tasks.summary != opt.summary;
tasks.summary = opt.summary; tasks.summary = opt.summary;
@ -18,7 +24,6 @@ impl App {
let new = tasks.paginate(); let new = tasks.paginate();
if tasks.snaps != new { if tasks.snaps != new {
tasks.snaps = new; tasks.snaps = new;
let cx = &mut Ctx::active(&mut self.core);
act!(tasks:arrow, cx)?; act!(tasks:arrow, cx)?;
succ!(render!()); succ!(render!());
} }
@ -29,7 +34,7 @@ impl App {
} else if tasks.summary.total == 0 { } else if tasks.summary.total == 0 {
succ!(render!()) succ!(render!())
} else { } else {
act!(render_partially, self) succ!(render_partial!())
} }
} }
} }

View file

@ -4,9 +4,11 @@ use anyhow::{Result, anyhow};
use yazi_core::{Core, mgr::Tabs, tab::{Folder, Tab}, tasks::Tasks}; use yazi_core::{Core, mgr::Tabs, tab::{Folder, Tab}, tasks::Tasks};
use yazi_fs::File; use yazi_fs::File;
use yazi_shared::{Id, Source, event::Cmd, url::UrlBuf}; use yazi_shared::{Id, Source, event::Cmd, url::UrlBuf};
use yazi_term::Term;
pub struct Ctx<'a> { pub struct Ctx<'a> {
pub core: &'a mut Core, pub core: &'a mut Core,
pub term: &'a mut Option<Term>,
pub tab: usize, pub tab: usize,
pub level: usize, pub level: usize,
pub source: Source, pub source: Source,
@ -26,7 +28,7 @@ impl DerefMut for Ctx<'_> {
impl<'a> Ctx<'a> { impl<'a> Ctx<'a> {
#[inline] #[inline]
pub fn new(core: &'a mut Core, cmd: &Cmd) -> Result<Self> { pub fn new(cmd: &Cmd, core: &'a mut Core, term: &'a mut Option<Term>) -> Result<Self> {
let tab = if let Ok(id) = cmd.get::<Id>("tab") { let tab = if let Ok(id) = cmd.get::<Id>("tab") {
core core
.mgr .mgr
@ -40,6 +42,7 @@ impl<'a> Ctx<'a> {
Ok(Self { Ok(Self {
core, core,
term,
tab, tab,
level: 0, level: 0,
source: cmd.source, source: cmd.source,
@ -53,6 +56,7 @@ impl<'a> Ctx<'a> {
let tab = cx.core.mgr.tabs.cursor; let tab = cx.core.mgr.tabs.cursor;
Self { Self {
core: cx.core, core: cx.core,
term: cx.term,
tab, tab,
level: cx.level, level: cx.level,
source: cx.source, source: cx.source,
@ -62,10 +66,11 @@ impl<'a> Ctx<'a> {
} }
#[inline] #[inline]
pub fn active(core: &'a mut Core) -> Self { pub fn active(core: &'a mut Core, term: &'a mut Option<Term>) -> Self {
let tab = core.mgr.tabs.cursor; let tab = core.mgr.tabs.cursor;
Self { Self {
core, core,
term,
tab, tab,
level: 0, level: 0,
source: Source::Unknown, source: Source::Unknown,

View file

@ -1,5 +1,5 @@
extern crate self as yazi_actor; extern crate self as yazi_actor;
yazi_macro::mod_pub!(cmp confirm core help input lives mgr pick spot tasks which); yazi_macro::mod_pub!(app cmp confirm core help input lives mgr notify pick spot tasks which);
yazi_macro::mod_flat!(actor context); yazi_macro::mod_flat!(actor context);

View file

@ -11,7 +11,7 @@ use yazi_dds::Pubsub;
use yazi_fs::{File, FilesOp, Splatter, max_common_root, path::skip_url, provider::{FileBuilder, Provider, local::{Gate, Local}}}; use yazi_fs::{File, FilesOp, Splatter, max_common_root, path::skip_url, provider::{FileBuilder, Provider, local::{Gate, Local}}};
use yazi_macro::{err, succ}; use yazi_macro::{err, succ};
use yazi_parser::VoidOpt; use yazi_parser::VoidOpt;
use yazi_proxy::{AppProxy, HIDER, TasksProxy}; use yazi_proxy::{AppProxy, HIDER, NotifyProxy, TasksProxy};
use yazi_shared::{data::Data, path::PathDyn, strand::{AsStrand, AsStrandJoin, Strand, StrandBuf, StrandLike}, terminal_clear, url::{AsUrl, UrlBuf, UrlCow, UrlLike}}; use yazi_shared::{data::Data, path::PathDyn, strand::{AsStrand, AsStrandJoin, Strand, StrandBuf, StrandLike}, terminal_clear, url::{AsUrl, UrlBuf, UrlCow, UrlLike}};
use yazi_tty::TTY; use yazi_tty::TTY;
use yazi_vfs::{VfsFile, maybe_exists, provider}; use yazi_vfs::{VfsFile, maybe_exists, provider};
@ -28,12 +28,12 @@ impl Actor for BulkRename {
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> { fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
let Some(opener) = Self::opener() else { let Some(opener) = Self::opener() else {
succ!(AppProxy::notify_warn("Bulk rename", "No text opener found")); succ!(NotifyProxy::push_warn("Bulk rename", "No text opener found"));
}; };
let selected: Vec<_> = cx.tab().selected_or_hovered().cloned().collect(); let selected: Vec<_> = cx.tab().selected_or_hovered().cloned().collect();
if selected.is_empty() { if selected.is_empty() {
succ!(AppProxy::notify_warn("Bulk rename", "No files selected")); succ!(NotifyProxy::push_warn("Bulk rename", "No files selected"));
} }
let root = max_common_root(&selected); let root = max_common_root(&selected);

View file

@ -1,7 +1,7 @@
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use yazi_macro::{act, render, render_and, succ}; use yazi_macro::{act, render, render_and, succ};
use yazi_parser::{VoidOpt, mgr::EscapeOpt}; use yazi_parser::{VoidOpt, mgr::EscapeOpt};
use yazi_proxy::AppProxy; use yazi_proxy::NotifyProxy;
use yazi_shared::{data::Data, url::UrlLike}; use yazi_shared::{data::Data, url::UrlLike};
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -76,7 +76,7 @@ impl Actor for EscapeVisual {
if !select { if !select {
tab.selected.remove_many(urls); tab.selected.remove_many(urls);
} else if urls.len() != tab.selected.add_many(urls) { } else if urls.len() != tab.selected.add_many(urls) {
AppProxy::notify_warn( NotifyProxy::push_warn(
"Escape visual mode", "Escape visual mode",
"Some files cannot be selected, due to path nesting conflict.", "Some files cannot be selected, due to path nesting conflict.",
); );

View file

@ -4,10 +4,10 @@ use anyhow::Result;
use tokio::{select, time}; use tokio::{select, time};
use yazi_config::popup::ConfirmCfg; use yazi_config::popup::ConfirmCfg;
use yazi_dds::spark::SparkKind; use yazi_dds::spark::SparkKind;
use yazi_macro::{emit, succ}; use yazi_macro::{act, succ};
use yazi_parser::mgr::QuitOpt; use yazi_parser::app::QuitOpt;
use yazi_proxy::ConfirmProxy; use yazi_proxy::{AppProxy, ConfirmProxy};
use yazi_shared::{data::Data, event::EventQuit, strand::{Strand, StrandLike, ToStrandJoin}, url::AsUrl}; use yazi_shared::{data::Data, strand::{Strand, StrandLike, ToStrandJoin}, url::AsUrl};
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -19,8 +19,6 @@ impl Actor for Quit {
const NAME: &str = "quit"; const NAME: &str = "quit";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> { fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let event = opt.into();
let ongoing = cx.tasks().ongoing().clone(); let ongoing = cx.tasks().ongoing().clone();
let (left, left_names) = { let (left, left_names) = {
let ongoing = ongoing.lock(); let ongoing = ongoing.lock();
@ -28,7 +26,7 @@ impl Actor for Quit {
}; };
if left == 0 { if left == 0 {
succ!(emit!(Quit(event))); return act!(app:quit, cx, opt);
} }
tokio::spawn(async move { tokio::spawn(async move {
@ -40,13 +38,13 @@ impl Actor for Quit {
i += 1; i += 1;
if i > 40 { break } if i > 40 { break }
else if ongoing.lock().is_empty() { else if ongoing.lock().is_empty() {
emit!(Quit(event)); AppProxy::quit(opt);
return; return;
} }
} }
b = token.future() => { b = token.future() => {
if b { if b {
emit!(Quit(event)); AppProxy::quit(opt);
} }
return; return;
} }
@ -54,7 +52,7 @@ impl Actor for Quit {
} }
if token.future().await { if token.future().await {
emit!(Quit(event)); AppProxy::quit(opt);
} }
}); });
succ!(); succ!();
@ -73,7 +71,7 @@ impl Quit {
{ {
let paths = selected.into_iter().join(Strand::Utf8("\n")); let paths = selected.into_iter().join(Strand::Utf8("\n"));
if !paths.is_empty() { if !paths.is_empty() {
emit!(Quit(EventQuit { selected: Some(paths), ..Default::default() })); AppProxy::quit(QuitOpt { selected: Some(paths), ..Default::default() });
} }
} }
} }

View file

@ -8,7 +8,7 @@ use yazi_fs::{FilesOp, cha::Cha};
use yazi_macro::{act, succ}; use yazi_macro::{act, succ};
use yazi_parser::{VoidOpt, mgr::{CdSource, SearchOpt, SearchOptVia}}; use yazi_parser::{VoidOpt, mgr::{CdSource, SearchOpt, SearchOptVia}};
use yazi_plugin::external; use yazi_plugin::external;
use yazi_proxy::{AppProxy, InputProxy, MgrProxy}; use yazi_proxy::{InputProxy, MgrProxy, NotifyProxy};
use yazi_shared::{data::Data, url::UrlLike}; use yazi_shared::{data::Data, url::UrlLike};
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -54,7 +54,7 @@ impl Actor for SearchDo {
let hidden = tab.pref.show_hidden; let hidden = tab.pref.show_hidden;
let Ok(cwd) = tab.cwd().to_search(&opt.subject) else { let Ok(cwd) = tab.cwd().to_search(&opt.subject) else {
succ!(AppProxy::notify_warn("Search", "Only local filesystem searches are supported")); succ!(NotifyProxy::push_warn("Search", "Only local filesystem searches are supported"));
}; };
tab.search = Some(tokio::spawn(async move { tab.search = Some(tokio::spawn(async move {

View file

@ -2,7 +2,7 @@ use anyhow::Result;
use yazi_core::tab::Tab; use yazi_core::tab::Tab;
use yazi_macro::{act, render, succ}; use yazi_macro::{act, render, succ};
use yazi_parser::mgr::{CdSource, TabCreateOpt}; use yazi_parser::mgr::{CdSource, TabCreateOpt};
use yazi_proxy::AppProxy; use yazi_proxy::NotifyProxy;
use yazi_shared::{data::Data, url::UrlLike}; use yazi_shared::{data::Data, url::UrlLike};
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -18,7 +18,7 @@ impl Actor for TabCreate {
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> { fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
if cx.tabs().len() >= MAX_TABS { if cx.tabs().len() >= MAX_TABS {
succ!(AppProxy::notify_warn( succ!(NotifyProxy::push_warn(
"Too many tabs", "Too many tabs",
"You can only open up to 9 tabs at the same time." "You can only open up to 9 tabs at the same time."
)); ));

View file

@ -1,7 +1,7 @@
use anyhow::Result; use anyhow::Result;
use yazi_macro::{render_and, succ}; use yazi_macro::{render_and, succ};
use yazi_parser::mgr::ToggleOpt; use yazi_parser::mgr::ToggleOpt;
use yazi_proxy::AppProxy; use yazi_proxy::NotifyProxy;
use yazi_shared::{data::Data, url::UrlCow}; use yazi_shared::{data::Data, url::UrlCow};
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -26,7 +26,7 @@ impl Actor for Toggle {
}; };
if !b { if !b {
AppProxy::notify_warn( NotifyProxy::push_warn(
"Toggle", "Toggle",
"This file cannot be selected, due to path nesting conflict.", "This file cannot be selected, due to path nesting conflict.",
); );

View file

@ -1,7 +1,7 @@
use anyhow::Result; use anyhow::Result;
use yazi_macro::{render, succ}; use yazi_macro::{render, succ};
use yazi_parser::mgr::ToggleAllOpt; use yazi_parser::mgr::ToggleAllOpt;
use yazi_proxy::AppProxy; use yazi_proxy::NotifyProxy;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -39,7 +39,7 @@ impl Actor for ToggleAll {
}; };
if warn { if warn {
AppProxy::notify_warn( NotifyProxy::push_warn(
"Toggle all", "Toggle all",
"Some files cannot be selected, due to path nesting conflict.", "Some files cannot be selected, due to path nesting conflict.",
); );

View file

@ -3,7 +3,7 @@ use yazi_macro::{act, render, succ};
use yazi_parser::VoidOpt; use yazi_parser::VoidOpt;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use crate::Actor; use crate::{Actor, Ctx};
pub struct Unyank; pub struct Unyank;
@ -12,7 +12,7 @@ impl Actor for Unyank {
const NAME: &str = "unyank"; const NAME: &str = "unyank";
fn act(cx: &mut crate::Ctx, _: Self::Options) -> Result<Data> { fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
let repeek = cx.hovered().is_some_and(|f| f.is_dir() && cx.mgr.yanked.contains_in(&f.url)); let repeek = cx.hovered().is_some_and(|f| f.is_dir() && cx.mgr.yanked.contains_in(&f.url));
cx.mgr.yanked.clear(); cx.mgr.yanked.clear();

View file

@ -3,7 +3,7 @@ use yazi_macro::{render, succ};
use yazi_parser::mgr::UpdatePeekedOpt; use yazi_parser::mgr::UpdatePeekedOpt;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use crate::Actor; use crate::{Actor, Ctx};
pub struct UpdatePeeked; pub struct UpdatePeeked;
@ -12,7 +12,7 @@ impl Actor for UpdatePeeked {
const NAME: &str = "update_peeked"; const NAME: &str = "update_peeked";
fn act(cx: &mut crate::Ctx, opt: Self::Options) -> Result<Data> { fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let Some(hovered) = cx.hovered().map(|h| &h.url) else { let Some(hovered) = cx.hovered().map(|h| &h.url) else {
succ!(cx.tab_mut().preview.reset()); succ!(cx.tab_mut().preview.reset());
}; };

View file

@ -0,0 +1,31 @@
use std::time::{Duration, Instant};
use anyhow::Result;
use yazi_core::notify::Message;
use yazi_macro::succ;
use yazi_parser::notify::PushOpt;
use yazi_proxy::NotifyProxy;
use yazi_shared::data::Data;
use crate::{Actor, Ctx};
pub struct Push;
impl Actor for Push {
type Options = PushOpt;
const NAME: &str = "push";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let instant = Instant::now();
let mut msg = Message::from(opt);
msg.timeout += instant - cx.notify.messages.first().map_or(instant, |m| m.instant);
if cx.notify.messages.iter().all(|m| m != &msg) {
cx.notify.messages.push(msg);
NotifyProxy::tick(Duration::ZERO);
}
succ!();
}
}

View file

@ -0,0 +1,64 @@
use std::time::Duration;
use anyhow::Result;
use ratatui::layout::Rect;
use yazi_core::notify::Notify;
use yazi_emulator::Dimension;
use yazi_macro::succ;
use yazi_parser::notify::TickOpt;
use yazi_proxy::NotifyProxy;
use yazi_shared::data::Data;
use crate::{Actor, Ctx};
pub struct Tick;
impl Actor for Tick {
type Options = TickOpt;
const NAME: &str = "tick";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
cx.notify.tick_handle.take().map(|h| h.abort());
let Dimension { rows, columns, .. } = Dimension::available();
let area = Notify::available(Rect { x: 0, y: 0, width: columns, height: rows });
let limit = cx.notify.limit(area);
if limit == 0 {
succ!();
}
for m in &mut cx.notify.messages[..limit] {
if m.timeout.is_zero() {
m.percent = m.percent.saturating_sub(20);
} else if m.percent < 100 {
m.percent += 20;
} else {
m.timeout = m.timeout.saturating_sub(opt.interval);
}
}
cx.notify.messages.retain(|m| m.percent > 0 || !m.timeout.is_zero());
let limit = cx.notify.limit(area);
let timeouts: Vec<_> = cx.notify.messages[..limit]
.iter()
.filter(|&m| m.percent == 100 && !m.timeout.is_zero())
.map(|m| m.timeout)
.collect();
let interval = if timeouts.len() != limit {
Duration::from_millis(50)
} else if let Some(min) = timeouts.iter().min() {
*min
} else {
succ!();
};
cx.notify.tick_handle = Some(tokio::spawn(async move {
tokio::time::sleep(interval).await;
NotifyProxy::tick(interval);
}));
succ!();
}
}

View file

@ -31,6 +31,7 @@ impl Keymap {
Layer::Help => &self.help, Layer::Help => &self.help,
Layer::Cmp => &self.cmp, Layer::Cmp => &self.cmp,
Layer::Which => &[], Layer::Which => &[],
Layer::Notify => &[],
} }
} }
} }

View file

@ -34,6 +34,7 @@ hashbrown = { workspace = true }
indexmap = { workspace = true } indexmap = { workspace = true }
parking_lot = { workspace = true } parking_lot = { workspace = true }
ratatui = { workspace = true } ratatui = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
tokio-stream = { workspace = true } tokio-stream = { workspace = true }
tokio-util = { workspace = true } tokio-util = { workspace = true }

View file

@ -1,20 +0,0 @@
use std::time::{Duration, Instant};
use yazi_parser::app::NotifyOpt;
use yazi_proxy::AppProxy;
use crate::notify::{Message, Notify};
impl Notify {
pub fn push(&mut self, opt: NotifyOpt) {
let instant = Instant::now();
let mut msg = Message::from(opt);
msg.timeout += instant - self.messages.first().map_or(instant, |m| m.instant);
if self.messages.iter().all(|m| m != &msg) {
self.messages.push(msg);
AppProxy::update_notify(Duration::ZERO);
}
}
}

View file

@ -1,49 +0,0 @@
use std::time::Duration;
use ratatui::layout::Rect;
use yazi_parser::notify::TickOpt;
use yazi_proxy::AppProxy;
use crate::notify::Notify;
impl Notify {
pub fn tick(&mut self, opt: TickOpt, area: Rect) {
self.tick_handle.take().map(|h| h.abort());
let limit = self.limit(area);
if limit == 0 {
return;
}
for m in &mut self.messages[..limit] {
if m.timeout.is_zero() {
m.percent = m.percent.saturating_sub(20);
} else if m.percent < 100 {
m.percent += 20;
} else {
m.timeout = m.timeout.saturating_sub(opt.interval);
}
}
self.messages.retain(|m| m.percent > 0 || !m.timeout.is_zero());
let limit = self.limit(area);
let timeouts: Vec<_> = self.messages[..limit]
.iter()
.filter(|&m| m.percent == 100 && !m.timeout.is_zero())
.map(|m| m.timeout)
.collect();
let interval = if timeouts.len() != limit {
Duration::from_millis(50)
} else if let Some(min) = timeouts.iter().min() {
*min
} else {
return;
};
self.tick_handle = Some(tokio::spawn(async move {
tokio::time::sleep(interval).await;
AppProxy::update_notify(interval);
}));
}
}

View file

@ -1,14 +1,14 @@
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use yazi_parser::app::{NotifyLevel, NotifyOpt}; use yazi_parser::notify::{PushLevel, PushOpt};
use super::NOTIFY_BORDER; use super::NOTIFY_BORDER;
pub struct Message { pub struct Message {
pub title: String, pub title: String,
pub content: String, pub content: String,
pub level: NotifyLevel, pub level: PushLevel,
pub timeout: Duration, pub timeout: Duration,
pub instant: Instant, pub instant: Instant,
@ -16,8 +16,8 @@ pub struct Message {
pub max_width: usize, pub max_width: usize,
} }
impl From<NotifyOpt> for Message { impl From<PushOpt> for Message {
fn from(opt: NotifyOpt) -> Self { fn from(opt: PushOpt) -> Self {
let title = opt.title.lines().next().unwrap_or_default(); let title = opt.title.lines().next().unwrap_or_default();
let title_width = title.width() + (opt.level.icon().width() + /* Space */ 1); let title_width = title.width() + (opt.level.icon().width() + /* Space */ 1);

View file

@ -1,5 +1,3 @@
yazi_macro::mod_pub!(commands);
yazi_macro::mod_flat!(message notify); yazi_macro::mod_flat!(message notify);
pub const NOTIFY_BORDER: u16 = 2; pub const NOTIFY_BORDER: u16 = 2;

View file

@ -1,17 +1,23 @@
use std::ops::ControlFlow; use std::ops::ControlFlow;
use ratatui::layout::Rect; use ratatui::layout::{Constraint, Layout, Rect};
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use super::{Message, NOTIFY_SPACING}; use super::{Message, NOTIFY_SPACING};
#[derive(Default)] #[derive(Default)]
pub struct Notify { pub struct Notify {
pub(super) tick_handle: Option<JoinHandle<()>>, pub tick_handle: Option<JoinHandle<()>>,
pub messages: Vec<Message>, pub messages: Vec<Message>,
} }
impl Notify { impl Notify {
pub fn available(area: Rect) -> Rect {
let chunks = Layout::horizontal([Constraint::Fill(1), Constraint::Min(80)]).split(area);
let chunks = Layout::vertical([Constraint::Fill(1), Constraint::Max(1)]).split(chunks[1]);
chunks[0]
}
pub fn limit(&self, area: Rect) -> usize { pub fn limit(&self, area: Rect) -> usize {
if self.messages.is_empty() { if self.messages.is_empty() {
return 0; return 0;

View file

@ -1,4 +1,4 @@
yazi_macro::mod_flat!(file plugin prework process tasks); yazi_macro::mod_flat!(file prework process tasks);
pub const TASKS_BORDER: u16 = 2; pub const TASKS_BORDER: u16 = 2;
pub const TASKS_PADDING: u16 = 2; pub const TASKS_PADDING: u16 = 2;

View file

@ -1,7 +0,0 @@
use yazi_parser::app::PluginOpt;
use super::Tasks;
impl Tasks {
pub fn plugin_entry(&self, opt: PluginOpt) { self.scheduler.plugin_entry(opt); }
}

View file

@ -1,11 +1,12 @@
use std::{fmt::Display, io::Write, str::FromStr}; use std::{fmt::Display, io::Write, str::FromStr};
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use mlua::{IntoLua, Lua, Value};
use yazi_boot::BOOT; use yazi_boot::BOOT;
use yazi_macro::{emit, relay}; use yazi_macro::{emit, relay};
use yazi_shared::Id; use yazi_shared::{Id, event::CmdCow};
use crate::{ID, ember::Ember}; use crate::{ID, ember::Ember, spark::Spark};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Payload<'a> { pub struct Payload<'a> {
@ -76,6 +77,25 @@ impl<'a> From<Ember<'a>> for Payload<'a> {
fn from(value: Ember<'a>) -> Self { Self::new(value) } fn from(value: Ember<'a>) -> Self { Self::new(value) }
} }
impl<'a> TryFrom<Spark<'a>> for Payload<'a> {
type Error = ();
fn try_from(value: Spark<'a>) -> Result<Self, Self::Error> {
match value {
Spark::AppAcceptPayload(payload) => Ok(payload),
_ => Err(()),
}
}
}
impl TryFrom<CmdCow> for Payload<'_> {
type Error = anyhow::Error;
fn try_from(mut c: CmdCow) -> Result<Self, Self::Error> {
c.take_any2("payload").ok_or_else(|| anyhow!("Missing 'payload' in Payload"))?
}
}
impl Display for Payload<'_> { impl Display for Payload<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let result = match &self.body { let result = match &self.body {
@ -104,3 +124,15 @@ impl Display for Payload<'_> {
} }
} }
} }
impl<'a> IntoLua for Payload<'a> {
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
lua
.create_table_from([
("receiver", yazi_binding::Id(self.receiver).into_lua(lua)?),
("sender", yazi_binding::Id(self.sender).into_lua(lua)?),
("body", self.body.into_lua(lua)?),
])?
.into_lua(lua)
}
}

View file

@ -7,6 +7,21 @@ pub enum Spark<'a> {
// Void // Void
Void(yazi_parser::VoidOpt), Void(yazi_parser::VoidOpt),
// App
AppAcceptPayload(crate::Payload<'a>),
AppBootstrap(yazi_parser::VoidOpt),
AppDeprecate(yazi_parser::app::DeprecateOpt),
AppFocus(yazi_parser::VoidOpt),
AppMouse(yazi_parser::app::MouseOpt),
AppPlugin(yazi_parser::app::PluginOpt),
AppPluginDo(yazi_parser::app::PluginOpt),
AppQuit(yazi_parser::app::QuitOpt),
AppReflow(yazi_parser::app::ReflowOpt),
AppResize(yazi_parser::app::ReflowOpt),
AppResume(yazi_parser::app::ResumeOpt),
AppStop(yazi_parser::app::StopOpt),
AppUpdateProgress(yazi_parser::app::UpdateProgressOpt),
// Mgr // Mgr
Arrow(yazi_parser::ArrowOpt), Arrow(yazi_parser::ArrowOpt),
Back(yazi_parser::VoidOpt), Back(yazi_parser::VoidOpt),
@ -42,7 +57,7 @@ pub enum Spark<'a> {
OpenDo(yazi_parser::mgr::OpenDoOpt), OpenDo(yazi_parser::mgr::OpenDoOpt),
Paste(yazi_parser::mgr::PasteOpt), Paste(yazi_parser::mgr::PasteOpt),
Peek(yazi_parser::mgr::PeekOpt), Peek(yazi_parser::mgr::PeekOpt),
Quit(yazi_parser::mgr::QuitOpt), Quit(yazi_parser::app::QuitOpt),
Refresh(yazi_parser::VoidOpt), Refresh(yazi_parser::VoidOpt),
Remove(yazi_parser::mgr::RemoveOpt), Remove(yazi_parser::mgr::RemoveOpt),
RemoveDo(yazi_parser::mgr::RemoveOpt), RemoveDo(yazi_parser::mgr::RemoveOpt),
@ -107,6 +122,7 @@ pub enum Spark<'a> {
InputShow(yazi_parser::input::ShowOpt), InputShow(yazi_parser::input::ShowOpt),
// Notify // Notify
NotifyPush(yazi_parser::notify::PushOpt),
NotifyTick(yazi_parser::notify::TickOpt), NotifyTick(yazi_parser::notify::TickOpt),
// Pick // Pick
@ -160,6 +176,21 @@ impl<'a> IntoLua for Spark<'a> {
// Void // Void
Self::Void(b) => b.into_lua(lua), Self::Void(b) => b.into_lua(lua),
// App
Self::AppAcceptPayload(b) => b.into_lua(lua),
Self::AppBootstrap(b) => b.into_lua(lua),
Self::AppDeprecate(b) => b.into_lua(lua),
Self::AppFocus(b) => b.into_lua(lua),
Self::AppMouse(b) => b.into_lua(lua),
Self::AppPlugin(b) => b.into_lua(lua),
Self::AppPluginDo(b) => b.into_lua(lua),
Self::AppQuit(b) => b.into_lua(lua),
Self::AppReflow(b) => b.into_lua(lua),
Self::AppResize(b) => b.into_lua(lua),
Self::AppResume(b) => b.into_lua(lua),
Self::AppStop(b) => b.into_lua(lua),
Self::AppUpdateProgress(b) => b.into_lua(lua),
// Mgr // Mgr
Self::Arrow(b) => b.into_lua(lua), Self::Arrow(b) => b.into_lua(lua),
Self::Back(b) => b.into_lua(lua), Self::Back(b) => b.into_lua(lua),
@ -260,6 +291,7 @@ impl<'a> IntoLua for Spark<'a> {
Self::InputShow(b) => b.into_lua(lua), Self::InputShow(b) => b.into_lua(lua),
// Notify // Notify
Self::NotifyPush(b) => b.into_lua(lua),
Self::NotifyTick(b) => b.into_lua(lua), Self::NotifyTick(b) => b.into_lua(lua),
// Pick // Pick
@ -292,6 +324,8 @@ impl<'a> IntoLua for Spark<'a> {
try_from_spark!( try_from_spark!(
VoidOpt, VoidOpt,
app:bootstrap,
app:focus,
mgr:back, mgr:back,
mgr:bulk_rename, mgr:bulk_rename,
mgr:enter, mgr:enter,
@ -309,7 +343,17 @@ try_from_spark!(
mgr:unyank, mgr:unyank,
mgr:watch mgr:watch
); );
// App
try_from_spark!(ArrowOpt, mgr:arrow, mgr:tab_swap); try_from_spark!(ArrowOpt, mgr:arrow, mgr:tab_swap);
try_from_spark!(app::DeprecateOpt, app:deprecate);
try_from_spark!(app::MouseOpt, app:mouse);
try_from_spark!(app::PluginOpt, app:plugin, app:plugin_do);
try_from_spark!(app::QuitOpt, app:quit, mgr:quit);
try_from_spark!(app::ReflowOpt, app:reflow, app:resize);
try_from_spark!(app::ResumeOpt, app:resume);
try_from_spark!(app::StopOpt, app:stop);
try_from_spark!(app::UpdateProgressOpt, app:update_progress);
try_from_spark!(cmp::CloseOpt, cmp:close); try_from_spark!(cmp::CloseOpt, cmp:close);
try_from_spark!(cmp::ShowOpt, cmp:show); try_from_spark!(cmp::ShowOpt, cmp:show);
try_from_spark!(cmp::TriggerOpt, cmp:trigger); try_from_spark!(cmp::TriggerOpt, cmp:trigger);
@ -347,7 +391,6 @@ try_from_spark!(mgr::OpenDoOpt, mgr:open_do);
try_from_spark!(mgr::OpenOpt, mgr:open); try_from_spark!(mgr::OpenOpt, mgr:open);
try_from_spark!(mgr::PasteOpt, mgr:paste); try_from_spark!(mgr::PasteOpt, mgr:paste);
try_from_spark!(mgr::PeekOpt, mgr:peek); try_from_spark!(mgr::PeekOpt, mgr:peek);
try_from_spark!(mgr::QuitOpt, mgr:quit);
try_from_spark!(mgr::RemoveOpt, mgr:remove, mgr:remove_do); try_from_spark!(mgr::RemoveOpt, mgr:remove, mgr:remove_do);
try_from_spark!(mgr::RenameOpt, mgr:rename); try_from_spark!(mgr::RenameOpt, mgr:rename);
try_from_spark!(mgr::RevealOpt, mgr:reveal); try_from_spark!(mgr::RevealOpt, mgr:reveal);
@ -371,11 +414,12 @@ try_from_spark!(mgr::UpdateYankedOpt<'a>, mgr:update_yanked);
try_from_spark!(mgr::UploadOpt, mgr:upload); try_from_spark!(mgr::UploadOpt, mgr:upload);
try_from_spark!(mgr::VisualModeOpt, mgr:visual_mode); try_from_spark!(mgr::VisualModeOpt, mgr:visual_mode);
try_from_spark!(mgr::YankOpt, mgr:yank); try_from_spark!(mgr::YankOpt, mgr:yank);
try_from_spark!(notify::PushOpt, notify:push);
try_from_spark!(notify::TickOpt, notify:tick); try_from_spark!(notify::TickOpt, notify:tick);
try_from_spark!(pick::CloseOpt, pick:close); try_from_spark!(pick::CloseOpt, pick:close);
try_from_spark!(pick::ShowOpt, pick:show); try_from_spark!(pick::ShowOpt, pick:show);
try_from_spark!(spot::CopyOpt, spot:copy); try_from_spark!(spot::CopyOpt, spot:copy);
try_from_spark!(tasks::ProcessOpenOpt, tasks:process_open); try_from_spark!(tasks::ProcessOpenOpt, tasks:process_open);
try_from_spark!(tasks::UpdateSucceedOpt, tasks:update_succeed); try_from_spark!(tasks::UpdateSucceedOpt, tasks:update_succeed);
try_from_spark!(which::CallbackOpt, which:callback);
try_from_spark!(which::ActivateOpt, which:activate); try_from_spark!(which::ActivateOpt, which:activate);
try_from_spark!(which::CallbackOpt, which:callback);

View file

@ -2,9 +2,10 @@ use std::{sync::atomic::Ordering, time::{Duration, Instant}};
use anyhow::Result; use anyhow::Result;
use tokio::{select, time::sleep}; use tokio::{select, time::sleep};
use yazi_actor::Ctx;
use yazi_core::Core; use yazi_core::Core;
use yazi_macro::act; use yazi_macro::act;
use yazi_shared::event::{Event, NEED_RENDER}; use yazi_shared::{data::Data, event::{Event, NEED_RENDER}};
use yazi_term::Term; use yazi_term::Term;
use crate::{Dispatcher, Signals}; use crate::{Dispatcher, Signals};
@ -21,21 +22,23 @@ impl App {
let (mut rx, signals) = (Event::take(), Signals::start()?); let (mut rx, signals) = (Event::take(), Signals::start()?);
let mut app = Self { core: Core::make(), term: Some(term), signals }; let mut app = Self { core: Core::make(), term: Some(term), signals };
act!(bootstrap, app)?; app.bootstrap()?;
let mut events = Vec::with_capacity(50); let mut events = Vec::with_capacity(50);
let (mut timeout, mut last_render) = (None, Instant::now()); let (mut timeout, mut need_render, mut last_render) = (None, 0, Instant::now());
macro_rules! drain_events { macro_rules! drain_events {
() => { () => {
for event in events.drain(..) { for event in events.drain(..) {
Dispatcher::new(&mut app).dispatch(event)?; Dispatcher::new(&mut app).dispatch(event)?;
if !NEED_RENDER.load(Ordering::Relaxed) {
need_render = NEED_RENDER.load(Ordering::Relaxed);
if need_render == 0 {
continue; continue;
} }
timeout = Duration::from_millis(10).checked_sub(last_render.elapsed()); timeout = Duration::from_millis(10).checked_sub(last_render.elapsed());
if timeout.is_none() { if timeout.is_none() {
act!(render, app)?; app.render(need_render == 2)?;
last_render = Instant::now(); last_render = Instant::now();
} }
} }
@ -46,7 +49,7 @@ impl App {
if let Some(t) = timeout.take() { if let Some(t) = timeout.take() {
select! { select! {
_ = sleep(t) => { _ = sleep(t) => {
act!(render, app)?; app.render(need_render == 2)?;
last_render = Instant::now(); last_render = Instant::now();
} }
n = rx.recv_many(&mut events, 50) => { n = rx.recv_many(&mut events, 50) => {
@ -62,4 +65,11 @@ impl App {
} }
Ok(()) Ok(())
} }
fn bootstrap(&mut self) -> anyhow::Result<Data> {
let cx = &mut Ctx::active(&mut self.core, &mut self.term);
act!(app:bootstrap, cx)?;
self.render(false)
}
} }

View file

@ -1,17 +0,0 @@
use anyhow::Result;
use yazi_macro::succ;
use yazi_parser::app::{DeprecateOpt, NotifyLevel, NotifyOpt};
use yazi_shared::data::Data;
use crate::app::App;
impl App {
pub(crate) fn deprecate(&mut self, opt: DeprecateOpt) -> Result<Data> {
succ!(self.core.notify.push(NotifyOpt {
title: "Deprecated API".to_owned(),
content: opt.content.into_owned(),
level: NotifyLevel::Warn,
timeout: std::time::Duration::from_secs(20),
}));
}
}

View file

@ -1,15 +0,0 @@
use anyhow::Result;
use yazi_actor::Ctx;
use yazi_macro::act;
use yazi_parser::VoidOpt;
use yazi_shared::data::Data;
use crate::app::App;
impl App {
pub fn focus(&mut self, _: VoidOpt) -> Result<Data> {
let cx = &mut Ctx::active(&mut self.core);
act!(mgr:refresh, cx)
}
}

View file

@ -1,12 +0,0 @@
use anyhow::Result;
use yazi_macro::succ;
use yazi_parser::app::NotifyOpt;
use yazi_shared::data::Data;
use crate::app::App;
impl App {
pub(crate) fn notify(&mut self, opt: NotifyOpt) -> Result<Data> {
succ!(self.core.notify.push(opt));
}
}

View file

@ -1,37 +0,0 @@
use yazi_boot::ARGS;
use yazi_fs::provider::{Provider, local::Local};
use yazi_shared::{event::EventQuit, strand::{StrandBuf, StrandLike, ToStrand}};
use yazi_term::Term;
use crate::app::App;
impl App {
pub(crate) fn quit(&mut self, opt: EventQuit) -> ! {
self.core.tasks.shutdown();
self.core.mgr.shutdown();
futures::executor::block_on(async {
_ = futures::join!(
yazi_dds::shutdown(),
yazi_dds::STATE.drain(),
self.cwd_to_file(opt.no_cwd_file),
self.selected_to_file(opt.selected)
);
});
Term::goodbye(|| opt.code);
}
async fn cwd_to_file(&self, no: bool) {
if let Some(p) = ARGS.cwd_file.as_ref().filter(|_| !no) {
let cwd = self.core.mgr.cwd().to_strand();
Local::regular(p).write(cwd.encoded_bytes()).await.ok();
}
}
async fn selected_to_file(&self, selected: Option<StrandBuf>) {
if let (Some(s), Some(p)) = (selected, &ARGS.chooser_file) {
Local::regular(p).write(s.encoded_bytes()).await.ok();
}
}
}

View file

@ -1,20 +0,0 @@
use anyhow::Result;
use yazi_actor::Ctx;
use yazi_macro::act;
use yazi_parser::VoidOpt;
use yazi_shared::data::Data;
use crate::app::App;
impl App {
pub fn resize(&mut self, _: VoidOpt) -> Result<Data> {
act!(reflow, self)?;
self.core.current_mut().arrow(0);
self.core.parent_mut().map(|f| f.arrow(0));
self.core.current_mut().sync_page(true);
let cx = &mut Ctx::active(&mut self.core);
act!(mgr:peek, cx)
}
}

View file

@ -1,22 +0,0 @@
use anyhow::Result;
use yazi_macro::{act, render, succ};
use yazi_parser::app::ResumeOpt;
use yazi_shared::data::Data;
use yazi_term::Term;
use crate::app::App;
impl App {
pub(crate) fn resume(&mut self, opt: ResumeOpt) -> Result<Data> {
self.core.active_mut().preview.reset();
self.term = Some(Term::start().unwrap());
// While the app resumes, it's possible that the terminal size has changed.
// We need to trigger a resize, and render the UI based on the resized area.
act!(resize, self)?;
self.signals.resume(opt.token);
succ!(render!());
}
}

View file

@ -1,24 +0,0 @@
use anyhow::Result;
use ratatui::layout::Rect;
use yazi_emulator::Dimension;
use yazi_macro::act;
use yazi_parser::notify::TickOpt;
use yazi_shared::data::Data;
use crate::{app::App, notify};
impl App {
pub(crate) fn update_notify(&mut self, opt: TickOpt) -> Result<Data> {
let Dimension { rows, columns, .. } = Dimension::available();
let area =
notify::Notify::available(Rect { x: 0, y: 0, width: columns, height: rows });
self.core.notify.tick(opt, area);
if self.core.notify.messages.is_empty() {
act!(render, self)
} else {
act!(render_partially, self)
}
}
}

View file

@ -1,3 +1 @@
yazi_macro::mod_pub!(commands); yazi_macro::mod_flat!(app render);
yazi_macro::mod_flat!(app);

View file

@ -6,17 +6,20 @@ use ratatui::{CompletedFrame, backend::{Backend, CrosstermBackend}, buffer::Buff
use yazi_actor::{Ctx, lives::Lives}; use yazi_actor::{Ctx, lives::Lives};
use yazi_binding::elements::COLLISION; use yazi_binding::elements::COLLISION;
use yazi_macro::{act, succ}; use yazi_macro::{act, succ};
use yazi_parser::VoidOpt;
use yazi_shared::{data::Data, event::NEED_RENDER}; use yazi_shared::{data::Data, event::NEED_RENDER};
use yazi_tty::TTY; use yazi_tty::TTY;
use crate::{app::App, root::Root}; use crate::{app::App, root::Root};
impl App { impl App {
pub(crate) fn render(&mut self, _: VoidOpt) -> Result<Data> { pub(crate) fn render(&mut self, partial: bool) -> Result<Data> {
NEED_RENDER.store(false, Ordering::Relaxed); NEED_RENDER.store(0, Ordering::Relaxed);
let Some(term) = &mut self.term else { succ!() }; let Some(term) = &mut self.term else { succ!() };
if partial {
return self.render_partially();
}
Self::routine(true, None); Self::routine(true, None);
let _guard = scopeguard::guard(self.core.cursor(), |c| Self::routine(false, c)); let _guard = scopeguard::guard(self.core.cursor(), |c| Self::routine(false, c));
@ -31,21 +34,21 @@ impl App {
Self::patch(frame); Self::patch(frame);
} }
if !self.core.notify.messages.is_empty() { if !self.core.notify.messages.is_empty() {
act!(render_partially, self)?; self.render_partially()?;
} }
// Reload preview if collision is resolved // Reload preview if collision is resolved
if collision && !COLLISION.load(Ordering::Relaxed) { if collision && !COLLISION.load(Ordering::Relaxed) {
let cx = &mut Ctx::active(&mut self.core); let cx = &mut Ctx::active(&mut self.core, &mut self.term);
act!(mgr:peek, cx, true)?; act!(mgr:peek, cx, true)?;
} }
succ!(); succ!();
} }
pub(crate) fn render_partially(&mut self, _: VoidOpt) -> Result<Data> { pub(crate) fn render_partially(&mut self) -> Result<Data> {
let Some(term) = &mut self.term else { succ!() }; let Some(term) = &mut self.term else { succ!() };
if !term.can_partial() { if !term.can_partial() {
return act!(render, self); return self.render(false);
} }
Self::routine(true, None); Self::routine(true, None);

View file

@ -1,8 +1,9 @@
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use anyhow::Result; use anyhow::Result;
use crossterm::event::KeyEvent; use crossterm::event::{KeyEvent, MouseEvent};
use tracing::warn; use tracing::warn;
use yazi_actor::Ctx;
use yazi_config::keymap::Key; use yazi_config::keymap::Key;
use yazi_macro::{act, emit, succ}; use yazi_macro::{act, emit, succ};
use yazi_shared::{data::Data, event::{CmdCow, Event, NEED_RENDER}}; use yazi_shared::{data::Data, event::{CmdCow, Event, NEED_RENDER}};
@ -20,17 +21,15 @@ impl<'a> Dispatcher<'a> {
#[inline] #[inline]
pub(super) fn dispatch(&mut self, event: Event) -> Result<()> { pub(super) fn dispatch(&mut self, event: Event) -> Result<()> {
// FIXME: handle errors
let result = match event { let result = match event {
Event::Call(cmd) => self.dispatch_call(cmd), Event::Call(cmd) => self.dispatch_call(cmd),
Event::Seq(cmds) => self.dispatch_seq(cmds), Event::Seq(cmds) => self.dispatch_seq(cmds),
Event::Render => self.dispatch_render(), Event::Render(partial) => self.dispatch_render(partial),
Event::Key(key) => self.dispatch_key(key), Event::Key(key) => self.dispatch_key(key),
Event::Mouse(mouse) => act!(mouse, self.app, mouse), Event::Mouse(mouse) => self.dispatch_mouse(mouse),
Event::Resize => act!(resize, self.app), Event::Resize => self.dispatch_resize(),
Event::Focus => act!(focus, self.app), Event::Focus => self.dispatch_focus(),
Event::Paste(str) => self.dispatch_paste(str), Event::Paste(str) => self.dispatch_paste(str),
Event::Quit(opt) => act!(quit, self.app, opt),
}; };
if let Err(err) = result { if let Err(err) = result {
@ -54,8 +53,13 @@ impl<'a> Dispatcher<'a> {
} }
#[inline] #[inline]
fn dispatch_render(&mut self) -> Result<Data> { fn dispatch_render(&mut self, partial: bool) -> Result<Data> {
succ!(NEED_RENDER.store(true, Ordering::Relaxed)) if partial {
_ = NEED_RENDER.compare_exchange(0, 2, Ordering::Relaxed, Ordering::Relaxed);
} else {
NEED_RENDER.store(1, Ordering::Relaxed);
}
succ!()
} }
#[inline] #[inline]
@ -64,6 +68,24 @@ impl<'a> Dispatcher<'a> {
succ!(); succ!();
} }
#[inline]
fn dispatch_mouse(&mut self, mouse: MouseEvent) -> Result<Data> {
let cx = &mut Ctx::active(&mut self.app.core, &mut self.app.term);
act!(app:mouse, cx, mouse)
}
#[inline]
fn dispatch_resize(&mut self) -> Result<Data> {
let cx = &mut Ctx::active(&mut self.app.core, &mut self.app.term);
act!(app:resize, cx, crate::Root::reflow as fn(_) -> _)
}
#[inline]
fn dispatch_focus(&mut self) -> Result<Data> {
let cx = &mut Ctx::active(&mut self.app.core, &mut self.app.term);
act!(app:focus, cx)
}
#[inline] #[inline]
fn dispatch_paste(&mut self, str: String) -> Result<Data> { fn dispatch_paste(&mut self, str: String) -> Result<Data> {
if self.app.core.input.visible { if self.app.core.input.visible {

View file

@ -1,4 +1,4 @@
use anyhow::Result; use anyhow::{Context, Result};
use yazi_actor::Ctx; use yazi_actor::Ctx;
use yazi_macro::{act, succ}; use yazi_macro::{act, succ};
use yazi_shared::{Layer, data::Data, event::CmdCow}; use yazi_shared::{Layer, data::Data, event::CmdCow};
@ -27,34 +27,45 @@ impl<'a> Executor<'a> {
Layer::Help => self.help(cmd), Layer::Help => self.help(cmd),
Layer::Cmp => self.cmp(cmd), Layer::Cmp => self.cmp(cmd),
Layer::Which => self.which(cmd), Layer::Which => self.which(cmd),
Layer::Notify => self.notify(cmd),
} }
} }
fn app(&mut self, cmd: CmdCow) -> Result<Data> { fn app(&mut self, mut cmd: CmdCow) -> Result<Data> {
let cx = &mut Ctx::new(&cmd, &mut self.app.core, &mut self.app.term)?;
macro_rules! on { macro_rules! on {
($name:ident) => { ($name:ident) => {
if cmd.name == stringify!($name) { if cmd.name == stringify!($name) {
return act!($name, self.app, cmd); return act!(app:$name, cx, cmd);
} }
}; };
} }
on!(accept_payload); on!(accept_payload);
on!(notify);
on!(plugin); on!(plugin);
on!(plugin_do); on!(plugin_do);
on!(update_notify);
on!(update_progress); on!(update_progress);
on!(resize);
on!(stop);
on!(resume);
on!(deprecate); on!(deprecate);
on!(quit);
succ!(); match &*cmd.name {
"resize" => act!(app:resize, cx, crate::Root::reflow as fn(_) -> _),
"resume" => act!(app:resume, cx, yazi_parser::app::ResumeOpt {
tx: self.app.signals.tx.clone(),
token: cmd.take_any("token").context("Invalid 'token' in ResumeOpt")?,
reflow: crate::Root::reflow,
}),
"stop" => act!(app:stop, cx, yazi_parser::app::StopOpt {
tx: self.app.signals.tx.clone(),
token: cmd.take_any("token").context("Invalid 'token' in StopOpt")?,
}),
_ => succ!(),
}
} }
fn mgr(&mut self, cmd: CmdCow) -> Result<Data> { fn mgr(&mut self, cmd: CmdCow) -> Result<Data> {
let cx = &mut Ctx::new(&mut self.app.core, &cmd)?; let cx = &mut Ctx::new(&cmd, &mut self.app.core, &mut self.app.term)?;
macro_rules! on { macro_rules! on {
($name:ident) => { ($name:ident) => {
@ -143,13 +154,13 @@ impl<'a> Executor<'a> {
// Help // Help
"help" => act!(help:toggle, cx, Layer::Mgr), "help" => act!(help:toggle, cx, Layer::Mgr),
// Plugin // Plugin
"plugin" => act!(plugin, self.app, cmd), "plugin" => act!(app:plugin, cx, cmd),
_ => succ!(), _ => succ!(),
} }
} }
fn tasks(&mut self, cmd: CmdCow) -> Result<Data> { fn tasks(&mut self, cmd: CmdCow) -> Result<Data> {
let cx = &mut Ctx::new(&mut self.app.core, &cmd)?; let cx = &mut Ctx::new(&cmd, &mut self.app.core, &mut self.app.term)?;
macro_rules! on { macro_rules! on {
($name:ident) => { ($name:ident) => {
@ -173,13 +184,13 @@ impl<'a> Executor<'a> {
// Help // Help
"help" => act!(help:toggle, cx, Layer::Tasks), "help" => act!(help:toggle, cx, Layer::Tasks),
// Plugin // Plugin
"plugin" => act!(plugin, self.app, cmd), "plugin" => act!(app:plugin, cx, cmd),
_ => succ!(), _ => succ!(),
} }
} }
fn spot(&mut self, cmd: CmdCow) -> Result<Data> { fn spot(&mut self, cmd: CmdCow) -> Result<Data> {
let cx = &mut Ctx::new(&mut self.app.core, &cmd)?; let cx = &mut Ctx::new(&cmd, &mut self.app.core, &mut self.app.term)?;
macro_rules! on { macro_rules! on {
($name:ident) => { ($name:ident) => {
@ -198,13 +209,13 @@ impl<'a> Executor<'a> {
// Help // Help
"help" => act!(help:toggle, cx, Layer::Spot), "help" => act!(help:toggle, cx, Layer::Spot),
// Plugin // Plugin
"plugin" => act!(plugin, self.app, cmd), "plugin" => act!(app:plugin, cx, cmd),
_ => succ!(), _ => succ!(),
} }
} }
fn pick(&mut self, cmd: CmdCow) -> Result<Data> { fn pick(&mut self, cmd: CmdCow) -> Result<Data> {
let cx = &mut Ctx::new(&mut self.app.core, &cmd)?; let cx = &mut Ctx::new(&cmd, &mut self.app.core, &mut self.app.term)?;
macro_rules! on { macro_rules! on {
($name:ident) => { ($name:ident) => {
@ -222,14 +233,14 @@ impl<'a> Executor<'a> {
// Help // Help
"help" => act!(help:toggle, cx, Layer::Pick), "help" => act!(help:toggle, cx, Layer::Pick),
// Plugin // Plugin
"plugin" => act!(plugin, self.app, cmd), "plugin" => act!(app:plugin, cx, cmd),
_ => succ!(), _ => succ!(),
} }
} }
fn input(&mut self, cmd: CmdCow) -> Result<Data> { fn input(&mut self, cmd: CmdCow) -> Result<Data> {
let mode = self.app.core.input.mode(); let mode = self.app.core.input.mode();
let cx = &mut Ctx::new(&mut self.app.core, &cmd)?; let cx = &mut Ctx::new(&cmd, &mut self.app.core, &mut self.app.term)?;
macro_rules! on { macro_rules! on {
($name:ident) => { ($name:ident) => {
@ -249,7 +260,7 @@ impl<'a> Executor<'a> {
// Help // Help
"help" => return act!(help:toggle, cx, Layer::Input), "help" => return act!(help:toggle, cx, Layer::Input),
// Plugin // Plugin
"plugin" => return act!(plugin, self.app, cmd), "plugin" => return act!(app:plugin, cx, cmd),
_ => {} _ => {}
} }
} }
@ -263,7 +274,7 @@ impl<'a> Executor<'a> {
} }
fn confirm(&mut self, cmd: CmdCow) -> Result<Data> { fn confirm(&mut self, cmd: CmdCow) -> Result<Data> {
let cx = &mut Ctx::new(&mut self.app.core, &cmd)?; let cx = &mut Ctx::new(&cmd, &mut self.app.core, &mut self.app.term)?;
macro_rules! on { macro_rules! on {
($name:ident) => { ($name:ident) => {
@ -281,7 +292,7 @@ impl<'a> Executor<'a> {
} }
fn help(&mut self, cmd: CmdCow) -> Result<Data> { fn help(&mut self, cmd: CmdCow) -> Result<Data> {
let cx = &mut Ctx::new(&mut self.app.core, &cmd)?; let cx = &mut Ctx::new(&cmd, &mut self.app.core, &mut self.app.term)?;
macro_rules! on { macro_rules! on {
($name:ident) => { ($name:ident) => {
@ -299,13 +310,13 @@ impl<'a> Executor<'a> {
// Help // Help
"close" => act!(help:toggle, cx, Layer::Help), "close" => act!(help:toggle, cx, Layer::Help),
// Plugin // Plugin
"plugin" => act!(plugin, self.app, cmd), "plugin" => act!(app:plugin, cx, cmd),
_ => succ!(), _ => succ!(),
} }
} }
fn cmp(&mut self, cmd: CmdCow) -> Result<Data> { fn cmp(&mut self, cmd: CmdCow) -> Result<Data> {
let cx = &mut Ctx::new(&mut self.app.core, &cmd)?; let cx = &mut Ctx::new(&cmd, &mut self.app.core, &mut self.app.term)?;
macro_rules! on { macro_rules! on {
($name:ident) => { ($name:ident) => {
@ -324,13 +335,13 @@ impl<'a> Executor<'a> {
// Help // Help
"help" => act!(help:toggle, cx, Layer::Cmp), "help" => act!(help:toggle, cx, Layer::Cmp),
// Plugin // Plugin
"plugin" => act!(plugin, self.app, cmd), "plugin" => act!(app:plugin, cx, cmd),
_ => succ!(), _ => succ!(),
} }
} }
fn which(&mut self, cmd: CmdCow) -> Result<Data> { fn which(&mut self, cmd: CmdCow) -> Result<Data> {
let cx = &mut Ctx::new(&mut self.app.core, &cmd)?; let cx = &mut Ctx::new(&cmd, &mut self.app.core, &mut self.app.term)?;
macro_rules! on { macro_rules! on {
($name:ident) => { ($name:ident) => {
@ -345,4 +356,21 @@ impl<'a> Executor<'a> {
succ!(); succ!();
} }
fn notify(&mut self, cmd: CmdCow) -> Result<Data> {
let cx = &mut Ctx::new(&cmd, &mut self.app.core, &mut self.app.term)?;
macro_rules! on {
($name:ident) => {
if cmd.name == stringify!($name) {
return act!(notify:$name, cx, cmd);
}
};
}
on!(push);
on!(tick);
succ!();
}
} }

View file

@ -8,15 +8,6 @@ pub(crate) struct Notify<'a> {
impl<'a> Notify<'a> { impl<'a> Notify<'a> {
pub(crate) fn new(core: &'a Core) -> Self { Self { core } } pub(crate) fn new(core: &'a Core) -> Self { Self { core } }
pub(crate) fn available(area: Rect) -> Rect {
let chunks = layout::Layout::horizontal([Constraint::Fill(1), Constraint::Min(80)]).split(area);
let chunks =
layout::Layout::vertical([Constraint::Fill(1), Constraint::Max(1)]).split(chunks[1]);
chunks[0]
}
fn tiles<'m>(area: Rect, messages: impl Iterator<Item = &'m Message> + Clone) -> Vec<Rect> { fn tiles<'m>(area: Rect, messages: impl Iterator<Item = &'m Message> + Clone) -> Vec<Rect> {
layout::Layout::vertical( layout::Layout::vertical(
[Constraint::Fill(1)] [Constraint::Fill(1)]
@ -42,7 +33,7 @@ impl<'a> Notify<'a> {
impl Widget for Notify<'_> { impl Widget for Notify<'_> {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
let notify = &self.core.notify; let notify = &self.core.notify;
let available = Self::available(area); let available = yazi_core::notify::Notify::available(area);
let messages = notify.messages.iter().take(notify.limit(available)).rev(); let messages = notify.messages.iter().take(notify.limit(available)).rev();
let tiles = Self::tiles(available, messages.clone()); let tiles = Self::tiles(available, messages.clone());

View file

@ -26,7 +26,7 @@ impl<'a> Router<'a> {
use Layer as L; use Layer as L;
Ok(match layer { Ok(match layer {
L::App => unreachable!(), L::App | L::Notify => unreachable!(),
L::Mgr | L::Tasks | L::Spot | L::Pick | L::Input | L::Confirm | L::Help => { L::Mgr | L::Tasks | L::Spot | L::Pick | L::Input | L::Confirm | L::Help => {
self.matches(layer, key) self.matches(layer, key)
} }
@ -42,7 +42,7 @@ impl<'a> Router<'a> {
} }
if on.len() > 1 { if on.len() > 1 {
let cx = &mut Ctx::active(&mut self.app.core); let cx = &mut Ctx::active(&mut self.app.core, &mut self.app.term);
act!(which:activate, cx, (layer, key)).ok(); act!(which:activate, cx, (layer, key)).ok();
} else { } else {
emit!(Seq(ChordCow::from(chord).into_seq())); emit!(Seq(ChordCow::from(chord).into_seq()));

View file

@ -6,7 +6,7 @@ use yazi_config::YAZI;
use yazi_shared::{CompletionToken, event::Event}; use yazi_shared::{CompletionToken, event::Event};
pub(super) struct Signals { pub(super) struct Signals {
tx: mpsc::UnboundedSender<(bool, CompletionToken)>, pub(super) tx: mpsc::UnboundedSender<(bool, CompletionToken)>,
} }
impl Signals { impl Signals {
@ -17,10 +17,6 @@ impl Signals {
Ok(Self { tx }) Ok(Self { tx })
} }
pub(super) fn stop(&mut self, token: CompletionToken) { self.tx.send((false, token)).ok(); }
pub(super) fn resume(&mut self, token: CompletionToken) { self.tx.send((true, token)).ok(); }
#[cfg(unix)] #[cfg(unix)]
fn handle_sys(n: libc::c_int) -> bool { fn handle_sys(n: libc::c_int) -> bool {
use libc::{SIGCONT, SIGHUP, SIGINT, SIGQUIT, SIGSTOP, SIGTERM, SIGTSTP}; use libc::{SIGCONT, SIGHUP, SIGINT, SIGQUIT, SIGSTOP, SIGTERM, SIGTSTP};
@ -30,7 +26,7 @@ impl Signals {
match n { match n {
SIGINT => { /* ignored */ } SIGINT => { /* ignored */ }
SIGQUIT | SIGHUP | SIGTERM => { SIGQUIT | SIGHUP | SIGTERM => {
Event::Quit(Default::default()).emit(); AppProxy::quit(Default::default());
return false; return false;
} }
SIGTSTP => { SIGTSTP => {
@ -38,7 +34,7 @@ impl Signals {
AppProxy::stop().await; AppProxy::stop().await;
if unsafe { libc::kill(0, SIGSTOP) } != 0 { if unsafe { libc::kill(0, SIGSTOP) } != 0 {
error!("Failed to stop the process:\n{}", std::io::Error::last_os_error()); error!("Failed to stop the process:\n{}", std::io::Error::last_os_error());
Event::Quit(Default::default()).emit(); AppProxy::quit(Default::default());
} }
}); });
} }

View file

@ -1,8 +1,5 @@
#[macro_export] #[macro_export]
macro_rules! emit { macro_rules! emit {
(Quit($opt:expr)) => {
yazi_shared::event::Event::Quit($opt).emit();
};
(Call($cmd:expr)) => { (Call($cmd:expr)) => {
yazi_shared::event::Event::Call(yazi_shared::event::CmdCow::from($cmd)).emit(); yazi_shared::event::Event::Call(yazi_shared::event::CmdCow::from($cmd)).emit();
}; };

View file

@ -1,7 +1,7 @@
#[macro_export] #[macro_export]
macro_rules! render { macro_rules! render {
() => { () => {
yazi_shared::event::NEED_RENDER.store(true, std::sync::atomic::Ordering::Relaxed); yazi_shared::event::NEED_RENDER.store(1, std::sync::atomic::Ordering::Relaxed);
}; };
($cond:expr) => { ($cond:expr) => {
if $cond { if $cond {
@ -28,3 +28,15 @@ macro_rules! render_and {
} }
}; };
} }
#[macro_export]
macro_rules! render_partial {
() => {{
_ = yazi_shared::event::NEED_RENDER.compare_exchange(
0,
2,
std::sync::atomic::Ordering::Relaxed,
std::sync::atomic::Ordering::Relaxed,
);
}};
}

View file

@ -32,6 +32,7 @@ dyn-clone = { workspace = true }
hashbrown = { workspace = true } hashbrown = { workspace = true }
mlua = { workspace = true } mlua = { workspace = true }
ordered-float = { workspace = true } ordered-float = { workspace = true }
ratatui = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_with = { workspace = true } serde_with = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }

View file

@ -1,6 +1,8 @@
use anyhow::bail; use anyhow::bail;
use mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
use yazi_shared::{SStr, event::CmdCow}; use yazi_shared::{SStr, event::CmdCow};
#[derive(Debug)]
pub struct DeprecateOpt { pub struct DeprecateOpt {
pub content: SStr, pub content: SStr,
} }
@ -16,3 +18,11 @@ impl TryFrom<CmdCow> for DeprecateOpt {
Ok(Self { content }) Ok(Self { content })
} }
} }
impl FromLua for DeprecateOpt {
fn from_lua(_: Value, _: &Lua) -> mlua::Result<Self> { Err("unsupported".into_lua_err()) }
}
impl IntoLua for DeprecateOpt {
fn into_lua(self, _: &Lua) -> mlua::Result<Value> { Err("unsupported".into_lua_err()) }
}

View file

@ -1 +1 @@
yazi_macro::mod_flat!(deprecate mouse notify plugin resume stop update_progress); yazi_macro::mod_flat!(deprecate mouse plugin quit reflow resume stop update_progress);

View file

@ -1,5 +1,7 @@
use crossterm::event::MouseEvent; use crossterm::event::MouseEvent;
use mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
#[derive(Debug)]
pub struct MouseOpt { pub struct MouseOpt {
pub event: MouseEvent, pub event: MouseEvent,
} }
@ -7,3 +9,11 @@ pub struct MouseOpt {
impl From<MouseEvent> for MouseOpt { impl From<MouseEvent> for MouseOpt {
fn from(event: MouseEvent) -> Self { Self { event } } fn from(event: MouseEvent) -> Self { Self { event } }
} }
impl FromLua for MouseOpt {
fn from_lua(_: Value, _: &Lua) -> mlua::Result<Self> { Err("unsupported".into_lua_err()) }
}
impl IntoLua for MouseOpt {
fn into_lua(self, _: &Lua) -> mlua::Result<Value> { Err("unsupported".into_lua_err()) }
}

View file

@ -3,7 +3,7 @@ use std::{borrow::Cow, fmt::{self, Debug}, str::FromStr};
use anyhow::bail; use anyhow::bail;
use dyn_clone::DynClone; use dyn_clone::DynClone;
use hashbrown::HashMap; use hashbrown::HashMap;
use mlua::{Lua, Table}; use mlua::{ExternalError, FromLua, IntoLua, Lua, Table, Value};
use serde::Deserialize; use serde::Deserialize;
use yazi_shared::{SStr, data::{Data, DataKey}, event::{Cmd, CmdCow}}; use yazi_shared::{SStr, data::{Data, DataKey}, event::{Cmd, CmdCow}};
@ -39,6 +39,14 @@ impl TryFrom<CmdCow> for PluginOpt {
} }
} }
impl FromLua for PluginOpt {
fn from_lua(_: Value, _: &Lua) -> mlua::Result<Self> { Err("unsupported".into_lua_err()) }
}
impl IntoLua for PluginOpt {
fn into_lua(self, _: &Lua) -> mlua::Result<Value> { Err("unsupported".into_lua_err()) }
}
impl PluginOpt { impl PluginOpt {
pub fn new_callback(id: impl Into<SStr>, f: impl PluginCallback) -> Self { pub fn new_callback(id: impl Into<SStr>, f: impl PluginCallback) -> Self {
Self { Self {

View file

@ -0,0 +1,35 @@
use mlua::{FromLua, IntoLua, Lua, LuaSerdeExt, Value};
use serde::{Deserialize, Serialize};
use yazi_shared::{event::CmdCow, strand::StrandBuf};
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct QuitOpt {
pub code: i32,
#[serde(skip)] // FIXME
pub selected: Option<StrandBuf>,
pub no_cwd_file: bool,
}
impl TryFrom<CmdCow> for QuitOpt {
type Error = anyhow::Error;
fn try_from(mut c: CmdCow) -> Result<Self, Self::Error> {
if let Some(opt) = c.take_any2("opt") {
return opt;
}
Ok(Self {
code: c.get("code").unwrap_or_default(),
selected: None, // FIXME
no_cwd_file: c.bool("no-cwd-file"),
})
}
}
impl FromLua for QuitOpt {
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> { lua.from_value(value) }
}
impl IntoLua for QuitOpt {
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> { lua.to_value(&self) }
}

View file

@ -0,0 +1,19 @@
use mlua::{ExternalError, FromLua, IntoLua, Lua, Table, Value};
use ratatui::layout::Rect;
#[derive(Debug)]
pub struct ReflowOpt {
pub reflow: fn(Rect) -> mlua::Result<Table>,
}
impl From<fn(Rect) -> mlua::Result<Table>> for ReflowOpt {
fn from(f: fn(Rect) -> mlua::Result<Table>) -> Self { Self { reflow: f } }
}
impl FromLua for ReflowOpt {
fn from_lua(_: Value, _: &Lua) -> mlua::Result<Self> { Err("unsupported".into_lua_err()) }
}
impl IntoLua for ReflowOpt {
fn into_lua(self, _: &Lua) -> mlua::Result<Value> { Err("unsupported".into_lua_err()) }
}

View file

@ -1,18 +1,19 @@
use anyhow::bail; use mlua::{ExternalError, FromLua, IntoLua, Lua, Table, Value};
use yazi_shared::{CompletionToken, event::CmdCow}; use ratatui::layout::Rect;
use tokio::sync::mpsc;
use yazi_shared::CompletionToken;
#[derive(Debug)]
pub struct ResumeOpt { pub struct ResumeOpt {
pub tx: mpsc::UnboundedSender<(bool, CompletionToken)>,
pub token: CompletionToken, pub token: CompletionToken,
pub reflow: fn(Rect) -> mlua::Result<Table>,
} }
impl TryFrom<CmdCow> for ResumeOpt { impl FromLua for ResumeOpt {
type Error = anyhow::Error; fn from_lua(_: Value, _: &Lua) -> mlua::Result<Self> { Err("unsupported".into_lua_err()) }
fn try_from(mut c: CmdCow) -> Result<Self, Self::Error> {
let Some(token) = c.take_any("token") else {
bail!("Invalid 'token' in ResumeOpt");
};
Ok(Self { token })
} }
impl IntoLua for ResumeOpt {
fn into_lua(self, _: &Lua) -> mlua::Result<Value> { Err("unsupported".into_lua_err()) }
} }

View file

@ -1,18 +1,17 @@
use anyhow::bail; use mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
use yazi_shared::{CompletionToken, event::CmdCow}; use tokio::sync::mpsc;
use yazi_shared::CompletionToken;
#[derive(Debug)]
pub struct StopOpt { pub struct StopOpt {
pub tx: mpsc::UnboundedSender<(bool, CompletionToken)>,
pub token: CompletionToken, pub token: CompletionToken,
} }
impl TryFrom<CmdCow> for StopOpt { impl FromLua for StopOpt {
type Error = anyhow::Error; fn from_lua(_: Value, _: &Lua) -> mlua::Result<Self> { Err("unsupported".into_lua_err()) }
fn try_from(mut c: CmdCow) -> Result<Self, Self::Error> {
let Some(token) = c.take_any("token") else {
bail!("Invalid 'token' in StopOpt");
};
Ok(Self { token })
} }
impl IntoLua for StopOpt {
fn into_lua(self, _: &Lua) -> mlua::Result<Value> { Err("unsupported".into_lua_err()) }
} }

View file

@ -1,8 +1,10 @@
use anyhow::bail; use anyhow::bail;
use mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
use ordered_float::OrderedFloat; use ordered_float::OrderedFloat;
use serde::Serialize; use serde::Serialize;
use yazi_shared::event::CmdCow; use yazi_shared::event::CmdCow;
#[derive(Debug)]
pub struct UpdateProgressOpt { pub struct UpdateProgressOpt {
pub summary: TaskSummary, pub summary: TaskSummary,
} }
@ -19,6 +21,14 @@ impl TryFrom<CmdCow> for UpdateProgressOpt {
} }
} }
impl FromLua for UpdateProgressOpt {
fn from_lua(_: Value, _: &Lua) -> mlua::Result<Self> { Err("unsupported".into_lua_err()) }
}
impl IntoLua for UpdateProgressOpt {
fn into_lua(self, _: &Lua) -> mlua::Result<Value> { Err("unsupported".into_lua_err()) }
}
// --- Progress // --- Progress
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
pub struct TaskSummary { pub struct TaskSummary {

View file

@ -1,13 +1,15 @@
use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; use mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
use yazi_shared::event::CmdCow; use yazi_shared::event::CmdCow;
use crate::mgr::QuitOpt; use crate::app::QuitOpt;
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct CloseOpt(pub QuitOpt); pub struct CloseOpt(pub QuitOpt);
impl From<CmdCow> for CloseOpt { impl TryFrom<CmdCow> for CloseOpt {
fn from(c: CmdCow) -> Self { Self(c.into()) } type Error = anyhow::Error;
fn try_from(c: CmdCow) -> Result<Self, Self::Error> { c.try_into().map(Self) }
} }
impl FromLua for CloseOpt { impl FromLua for CloseOpt {

View file

@ -19,7 +19,6 @@ yazi_macro::mod_flat!(
open_do open_do
paste paste
peek peek
quit
remove remove
rename rename
reveal reveal

View file

@ -1,29 +0,0 @@
use mlua::{FromLua, IntoLua, Lua, LuaSerdeExt, Value};
use serde::{Deserialize, Serialize};
use yazi_shared::event::{CmdCow, EventQuit};
#[derive(Debug, Default, Deserialize, Serialize)]
pub struct QuitOpt {
pub code: i32,
pub no_cwd_file: bool,
}
impl From<CmdCow> for QuitOpt {
fn from(c: CmdCow) -> Self {
Self { code: c.get("code").unwrap_or_default(), no_cwd_file: c.bool("no-cwd-file") }
}
}
impl From<QuitOpt> for EventQuit {
fn from(value: QuitOpt) -> Self {
Self { code: value.code, no_cwd_file: value.no_cwd_file, ..Default::default() }
}
}
impl FromLua for QuitOpt {
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> { lua.from_value(value) }
}
impl IntoLua for QuitOpt {
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> { lua.to_value(&self) }
}

View file

@ -1 +1 @@
yazi_macro::mod_flat!(tick); yazi_macro::mod_flat!(push tick);

View file

@ -8,16 +8,16 @@ use yazi_config::{Style, THEME};
use yazi_shared::event::CmdCow; use yazi_shared::event::CmdCow;
#[serde_as] #[serde_as]
#[derive(Clone, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub struct NotifyOpt { pub struct PushOpt {
pub title: String, pub title: String,
pub content: String, pub content: String,
pub level: NotifyLevel, pub level: PushLevel,
#[serde_as(as = "DurationSeconds<f64>")] // FIXME #[serde_as(as = "DurationSeconds<f64>")] // FIXME
pub timeout: Duration, pub timeout: Duration,
} }
impl TryFrom<CmdCow> for NotifyOpt { impl TryFrom<CmdCow> for PushOpt {
type Error = anyhow::Error; type Error = anyhow::Error;
fn try_from(mut c: CmdCow) -> Result<Self, Self::Error> { fn try_from(mut c: CmdCow) -> Result<Self, Self::Error> {
@ -25,25 +25,25 @@ impl TryFrom<CmdCow> for NotifyOpt {
} }
} }
impl FromLua for NotifyOpt { impl FromLua for PushOpt {
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> { lua.from_value(value) } fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> { lua.from_value(value) }
} }
impl IntoLua for NotifyOpt { impl IntoLua for PushOpt {
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> { lua.to_value(&self) } fn into_lua(self, lua: &Lua) -> mlua::Result<Value> { lua.to_value(&self) }
} }
// --- Level // --- Level
#[derive(Clone, Copy, Default, Deserialize, Eq, PartialEq, Serialize)] #[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub enum NotifyLevel { pub enum PushLevel {
#[default] #[default]
Info, Info,
Warn, Warn,
Error, Error,
} }
impl NotifyLevel { impl PushLevel {
pub fn icon(self) -> &'static str { pub fn icon(self) -> &'static str {
match self { match self {
Self::Info => &THEME.notify.icon_info, Self::Info => &THEME.notify.icon_info,
@ -61,7 +61,7 @@ impl NotifyLevel {
} }
} }
impl FromStr for NotifyLevel { impl FromStr for PushLevel {
type Err = serde::de::value::Error; type Err = serde::de::value::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {

View file

@ -6,8 +6,8 @@ use tokio_stream::wrappers::UnboundedReceiverStream;
use yazi_binding::{elements::{Line, Pos, Text}, runtime}; use yazi_binding::{elements::{Line, Pos, Text}, runtime};
use yazi_config::{keymap::{Chord, ChordCow, Key}, popup::{ConfirmCfg, InputCfg}}; use yazi_config::{keymap::{Chord, ChordCow, Key}, popup::{ConfirmCfg, InputCfg}};
use yazi_macro::relay; use yazi_macro::relay;
use yazi_parser::{app::NotifyOpt, which::ActivateOpt}; use yazi_parser::{notify::PushOpt, which::ActivateOpt};
use yazi_proxy::{AppProxy, ConfirmProxy, InputProxy, WhichProxy}; use yazi_proxy::{ConfirmProxy, InputProxy, NotifyProxy, WhichProxy};
use yazi_shared::Debounce; use yazi_shared::Debounce;
use super::Utils; use super::Utils;
@ -100,7 +100,7 @@ impl Utils {
} }
pub(super) fn notify(lua: &Lua) -> mlua::Result<Function> { pub(super) fn notify(lua: &Lua) -> mlua::Result<Function> {
lua.create_function(|_, opt: NotifyOpt| Ok(AppProxy::notify(opt))) lua.create_function(|_, opt: PushOpt| Ok(NotifyProxy::push(opt)))
} }
fn parse_keys(value: Value) -> mlua::Result<Vec<Key>> { fn parse_keys(value: Value) -> mlua::Result<Vec<Key>> {

View file

@ -1,12 +1,14 @@
use std::time::Duration;
use yazi_macro::{emit, relay}; use yazi_macro::{emit, relay};
use yazi_parser::app::{NotifyLevel, NotifyOpt, PluginOpt, TaskSummary}; use yazi_parser::app::{PluginOpt, QuitOpt, TaskSummary};
use yazi_shared::CompletionToken; use yazi_shared::CompletionToken;
pub struct AppProxy; pub struct AppProxy;
impl AppProxy { impl AppProxy {
pub fn quit(opt: QuitOpt) {
emit!(Call(relay!(app:quit).with_any("opt", opt)));
}
pub async fn stop() { pub async fn stop() {
let token = CompletionToken::default(); let token = CompletionToken::default();
emit!(Call(relay!(app:stop).with_any("token", token.clone()))); emit!(Call(relay!(app:stop).with_any("token", token.clone())));
@ -19,32 +21,6 @@ impl AppProxy {
token.future().await; token.future().await;
} }
pub fn notify(opt: NotifyOpt) {
emit!(Call(relay!(app:notify).with_any("opt", opt)));
}
pub fn update_notify(dur: Duration) {
emit!(Call(relay!(app:update_notify, [dur.as_secs_f64()])));
}
pub fn notify_warn(title: impl Into<String>, content: impl Into<String>) {
Self::notify(NotifyOpt {
title: title.into(),
content: content.into(),
level: NotifyLevel::Warn,
timeout: Duration::from_secs(5),
});
}
pub fn notify_error(title: &str, content: impl ToString) {
Self::notify(NotifyOpt {
title: title.to_owned(),
content: content.to_string(),
level: NotifyLevel::Error,
timeout: Duration::from_secs(10),
});
}
pub fn plugin(opt: PluginOpt) { pub fn plugin(opt: PluginOpt) {
emit!(Call(relay!(app:plugin).with_any("opt", opt))); emit!(Call(relay!(app:plugin).with_any("opt", opt)));
} }

View file

@ -1,5 +1,5 @@
mod macros; mod macros;
yazi_macro::mod_flat!(app cmp confirm input mgr pick semaphore tasks which); yazi_macro::mod_flat!(app cmp confirm input mgr notify pick semaphore tasks which);
pub fn init() { crate::init_semaphore(); } pub fn init() { crate::init_semaphore(); }

34
yazi-proxy/src/notify.rs Normal file
View file

@ -0,0 +1,34 @@
use std::time::Duration;
use yazi_macro::{emit, relay};
use yazi_parser::notify::{PushLevel, PushOpt};
pub struct NotifyProxy;
impl NotifyProxy {
pub fn push(opt: PushOpt) {
emit!(Call(relay!(notify:push).with_any("opt", opt)));
}
pub fn push_warn(title: impl Into<String>, content: impl Into<String>) {
Self::push(PushOpt {
title: title.into(),
content: content.into(),
level: PushLevel::Warn,
timeout: Duration::from_secs(5),
});
}
pub fn push_error(title: &str, content: impl ToString) {
Self::push(PushOpt {
title: title.to_owned(),
content: content.to_string(),
level: PushLevel::Error,
timeout: Duration::from_secs(10),
});
}
pub fn tick(dur: Duration) {
emit!(Call(relay!(notify:tick, [dur.as_secs_f64()])));
}
}

View file

@ -1,7 +1,7 @@
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use tokio::{io::{AsyncBufReadExt, BufReader}, select, sync::mpsc}; use tokio::{io::{AsyncBufReadExt, BufReader}, select, sync::mpsc};
use yazi_binding::Permit; use yazi_binding::Permit;
use yazi_proxy::{AppProxy, HIDER}; use yazi_proxy::{AppProxy, HIDER, NotifyProxy};
use super::{ProcessInBg, ProcessInBlock, ProcessInOrphan, ShellOpt}; use super::{ProcessInBg, ProcessInBlock, ProcessInOrphan, ShellOpt};
use crate::{TaskOp, TaskOps, process::{ProcessOutBg, ProcessOutBlock, ProcessOutOrphan}}; use crate::{TaskOp, TaskOps, process::{ProcessOutBg, ProcessOutBlock, ProcessOutOrphan}};
@ -20,7 +20,7 @@ impl Process {
let (id, cmd) = (task.id, task.cmd.clone()); let (id, cmd) = (task.id, task.cmd.clone());
let result = super::shell(task.into()).await; let result = super::shell(task.into()).await;
if let Err(e) = result { if let Err(e) = result {
AppProxy::notify_warn(cmd.to_string_lossy(), format!("Failed to start process: {e}")); NotifyProxy::push_warn(cmd.to_string_lossy(), format!("Failed to start process: {e}"));
return Ok(self.ops.out(id, ProcessOutBlock::Succ)); return Ok(self.ops.out(id, ProcessOutBlock::Succ));
} }
@ -31,7 +31,7 @@ impl Process {
Some(code) => format!("Process exited with status code: {code}"), Some(code) => format!("Process exited with status code: {code}"),
None => "Process terminated by signal".to_string(), None => "Process terminated by signal".to_string(),
}; };
AppProxy::notify_warn(cmd.to_string_lossy(), content); NotifyProxy::push_warn(cmd.to_string_lossy(), content);
} }
Ok(self.ops.out(id, ProcessOutBlock::Succ)) Ok(self.ops.out(id, ProcessOutBlock::Succ))

View file

@ -2,7 +2,7 @@ use crossterm::event::{KeyEvent, MouseEvent};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use super::CmdCow; use super::CmdCow;
use crate::{RoCell, strand::StrandBuf}; use crate::RoCell;
static TX: RoCell<mpsc::UnboundedSender<Event>> = RoCell::new(); static TX: RoCell<mpsc::UnboundedSender<Event>> = RoCell::new();
static RX: RoCell<mpsc::UnboundedReceiver<Event>> = RoCell::new(); static RX: RoCell<mpsc::UnboundedReceiver<Event>> = RoCell::new();
@ -11,20 +11,12 @@ static RX: RoCell<mpsc::UnboundedReceiver<Event>> = RoCell::new();
pub enum Event { pub enum Event {
Call(CmdCow), Call(CmdCow),
Seq(Vec<CmdCow>), Seq(Vec<CmdCow>),
Render, Render(bool),
Key(KeyEvent), Key(KeyEvent),
Mouse(MouseEvent), Mouse(MouseEvent),
Resize, Resize,
Focus, Focus,
Paste(String), Paste(String),
Quit(EventQuit),
}
#[derive(Debug, Default)]
pub struct EventQuit {
pub code: i32,
pub no_cwd_file: bool,
pub selected: Option<StrandBuf>,
} }
impl Event { impl Event {

View file

@ -1,3 +1,3 @@
yazi_macro::mod_flat!(cmd cow event); yazi_macro::mod_flat!(cmd cow event);
pub static NEED_RENDER: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); pub static NEED_RENDER: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0);

View file

@ -16,6 +16,7 @@ pub enum Layer {
Help, Help,
Cmp, Cmp,
Which, Which,
Notify,
} }
impl Display for Layer { impl Display for Layer {
@ -31,6 +32,7 @@ impl Display for Layer {
Self::Help => "help", Self::Help => "help",
Self::Cmp => "cmp", Self::Cmp => "cmp",
Self::Which => "which", Self::Which => "which",
Self::Notify => "notify",
}) })
} }
} }