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

View file

@ -8,7 +8,7 @@ function bugReportBody(creator, content, hash) {
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:
@ -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.
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 `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:
@ -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.
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-core = { path = "../yazi-core", 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-macro = { path = "../yazi-macro", 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-scheduler = { path = "../yazi-scheduler", 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-vfs = { path = "../yazi-vfs", version = "26.1.22" }
yazi-watcher = { path = "../yazi-watcher", version = "26.1.22" }
@ -40,6 +42,7 @@ futures = { workspace = true }
hashbrown = { workspace = true }
mlua = { workspace = true }
paste = { workspace = true }
ratatui = { workspace = true }
scopeguard = { workspace = true }
tokio = { workspace = true }
tokio-stream = { workspace = true }

View file

@ -1,4 +1,4 @@
use anyhow::{Result, bail};
use anyhow::Result;
use mlua::IntoLua;
use tracing::error;
use yazi_actor::lives::Lives;
@ -6,16 +6,18 @@ use yazi_binding::runtime_mut;
use yazi_dds::{LOCAL, Payload, REMOTE};
use yazi_macro::succ;
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(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");
};
pub struct AcceptPayload;
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 lock = if payload.receiver == 0 || payload.receiver != payload.sender {
REMOTE.read()
@ -26,7 +28,7 @@ impl App {
let Some(handlers) = lock.get(&kind).filter(|&m| !m.is_empty()).cloned() else { succ!() };
drop(lock);
succ!(Lives::scope(&self.core, || {
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);

View file

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

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
focus
mouse
notify
plugin
plugin_do
quit
reflow
render
resize
resume
stop
update_notify
update_progress
);

View file

@ -9,14 +9,20 @@ use yazi_parser::app::MouseOpt;
use yazi_plugin::LUA;
use yazi_shared::data::Data;
use crate::app::App;
use crate::{Actor, Ctx};
impl App {
pub fn mouse(&mut self, opt: MouseOpt) -> Result<Data> {
pub struct Mouse;
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 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 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 scopeguard::defer;
use tracing::{error, warn};
use yazi_actor::lives::Lives;
use yazi_binding::runtime_mut;
use yazi_dds::Sendable;
use yazi_macro::succ;
use yazi_parser::app::{PluginMode, PluginOpt};
use yazi_plugin::{LUA, loader::{LOADER, Loader}};
use yazi_proxy::AppProxy;
use yazi_proxy::NotifyProxy;
use yazi_shared::data::Data;
use crate::app::App;
use crate::{Actor, Ctx, lives::Lives};
impl App {
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);
}
pub struct PluginDo;
if opt.mode == PluginMode::Async {
succ!(self.core.tasks.plugin_entry(opt));
} else if opt.mode == PluginMode::Sync && hits {
return self.plugin_do(opt);
}
impl Actor for PluginDo {
type Options = PluginOpt;
tokio::spawn(async move {
match LOADER.ensure(&opt.id, |_| ()).await {
Ok(()) => AppProxy::plugin_do(opt),
Err(e) => AppProxy::notify_error("Plugin load failed", e),
}
});
succ!();
}
const NAME: &str = "plugin_do";
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 Some(chunk) = loader.get(&*opt.id) else {
succ!(warn!("plugin `{}` not found", opt.id));
};
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 {
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);
@ -59,7 +42,7 @@ impl App {
};
drop(loader);
let result = Lives::scope(&self.core, || {
let result = Lives::scope(&cx.core, || {
if let Some(cb) = opt.callback {
cb(&LUA, plugin)
} 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_config::LAYOUT;
use yazi_macro::{render, succ};
use yazi_parser::VoidOpt;
use yazi_parser::app::ReflowOpt;
use yazi_shared::data::Data;
use crate::{Root, app::App};
use crate::{Actor, Ctx};
impl App {
pub fn reflow(&mut self, _: VoidOpt) -> Result<Data> {
let Some(size) = self.term.as_ref().and_then(|t| t.size().ok()) else { succ!() };
pub struct Reflow;
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 result = Lives::scope(&self.core, || {
let comps = Root::reflow((Position::ORIGIN, size).into())?;
let result = Lives::scope(&cx.core, || {
let comps = (opt.reflow)((Position::ORIGIN, size).into())?;
for v in comps.sequence_values::<Value>() {
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_shared::data::Data;
use crate::app::App;
use crate::{Actor, Ctx};
impl App {
pub fn stop(&mut self, opt: StopOpt) -> Result<Data> {
self.core.active_mut().preview.reset_image();
pub struct Stop;
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`
// to prevent any signal from triggering the term to render again
// while the app is being suspended.
self.term = None;
*cx.term = None;
self.signals.stop(opt.token);
opt.tx.send((false, opt.token))?;
succ!();
}

View file

@ -1,15 +1,21 @@
use anyhow::Result;
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_shared::data::Data;
use crate::app::App;
use crate::Actor;
impl App {
pub(crate) fn update_progress(&mut self, opt: UpdateProgressOpt) -> Result<Data> {
pub struct UpdateProgress;
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.
let tasks = &mut self.core.tasks;
let tasks = &mut cx.core.tasks;
let progressed = tasks.summary != opt.summary;
tasks.summary = opt.summary;
@ -18,7 +24,6 @@ impl App {
let new = tasks.paginate();
if tasks.snaps != new {
tasks.snaps = new;
let cx = &mut Ctx::active(&mut self.core);
act!(tasks:arrow, cx)?;
succ!(render!());
}
@ -29,7 +34,7 @@ impl App {
} else if tasks.summary.total == 0 {
succ!(render!())
} 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_fs::File;
use yazi_shared::{Id, Source, event::Cmd, url::UrlBuf};
use yazi_term::Term;
pub struct Ctx<'a> {
pub core: &'a mut Core,
pub term: &'a mut Option<Term>,
pub tab: usize,
pub level: usize,
pub source: Source,
@ -26,7 +28,7 @@ impl DerefMut for Ctx<'_> {
impl<'a> Ctx<'a> {
#[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") {
core
.mgr
@ -40,6 +42,7 @@ impl<'a> Ctx<'a> {
Ok(Self {
core,
term,
tab,
level: 0,
source: cmd.source,
@ -53,6 +56,7 @@ impl<'a> Ctx<'a> {
let tab = cx.core.mgr.tabs.cursor;
Self {
core: cx.core,
term: cx.term,
tab,
level: cx.level,
source: cx.source,
@ -62,10 +66,11 @@ impl<'a> Ctx<'a> {
}
#[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;
Self {
core,
term,
tab,
level: 0,
source: Source::Unknown,

View file

@ -1,5 +1,5 @@
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);

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_macro::{err, succ};
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_tty::TTY;
use yazi_vfs::{VfsFile, maybe_exists, provider};
@ -28,12 +28,12 @@ impl Actor for BulkRename {
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
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();
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);

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
use anyhow::Result;
use yazi_macro::{render, succ};
use yazi_parser::mgr::ToggleAllOpt;
use yazi_proxy::AppProxy;
use yazi_proxy::NotifyProxy;
use yazi_shared::data::Data;
use crate::{Actor, Ctx};
@ -39,7 +39,7 @@ impl Actor for ToggleAll {
};
if warn {
AppProxy::notify_warn(
NotifyProxy::push_warn(
"Toggle all",
"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_shared::data::Data;
use crate::Actor;
use crate::{Actor, Ctx};
pub struct Unyank;
@ -12,7 +12,7 @@ impl Actor for 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));
cx.mgr.yanked.clear();

View file

@ -3,7 +3,7 @@ use yazi_macro::{render, succ};
use yazi_parser::mgr::UpdatePeekedOpt;
use yazi_shared::data::Data;
use crate::Actor;
use crate::{Actor, Ctx};
pub struct UpdatePeeked;
@ -12,7 +12,7 @@ impl Actor for UpdatePeeked {
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 {
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::Cmp => &self.cmp,
Layer::Which => &[],
Layer::Notify => &[],
}
}
}

View file

@ -34,6 +34,7 @@ hashbrown = { workspace = true }
indexmap = { workspace = true }
parking_lot = { workspace = true }
ratatui = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true }
tokio-stream = { 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 unicode_width::UnicodeWidthStr;
use yazi_parser::app::{NotifyLevel, NotifyOpt};
use yazi_parser::notify::{PushLevel, PushOpt};
use super::NOTIFY_BORDER;
pub struct Message {
pub title: String,
pub content: String,
pub level: NotifyLevel,
pub level: PushLevel,
pub timeout: Duration,
pub instant: Instant,
@ -16,8 +16,8 @@ pub struct Message {
pub max_width: usize,
}
impl From<NotifyOpt> for Message {
fn from(opt: NotifyOpt) -> Self {
impl From<PushOpt> for Message {
fn from(opt: PushOpt) -> Self {
let title = opt.title.lines().next().unwrap_or_default();
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);
pub const NOTIFY_BORDER: u16 = 2;

View file

@ -1,17 +1,23 @@
use std::ops::ControlFlow;
use ratatui::layout::Rect;
use ratatui::layout::{Constraint, Layout, Rect};
use tokio::task::JoinHandle;
use super::{Message, NOTIFY_SPACING};
#[derive(Default)]
pub struct Notify {
pub(super) tick_handle: Option<JoinHandle<()>>,
pub tick_handle: Option<JoinHandle<()>>,
pub messages: Vec<Message>,
}
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 {
if self.messages.is_empty() {
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_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 anyhow::{Result, anyhow};
use mlua::{IntoLua, Lua, Value};
use yazi_boot::BOOT;
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)]
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) }
}
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<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
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(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
Arrow(yazi_parser::ArrowOpt),
Back(yazi_parser::VoidOpt),
@ -42,7 +57,7 @@ pub enum Spark<'a> {
OpenDo(yazi_parser::mgr::OpenDoOpt),
Paste(yazi_parser::mgr::PasteOpt),
Peek(yazi_parser::mgr::PeekOpt),
Quit(yazi_parser::mgr::QuitOpt),
Quit(yazi_parser::app::QuitOpt),
Refresh(yazi_parser::VoidOpt),
Remove(yazi_parser::mgr::RemoveOpt),
RemoveDo(yazi_parser::mgr::RemoveOpt),
@ -107,6 +122,7 @@ pub enum Spark<'a> {
InputShow(yazi_parser::input::ShowOpt),
// Notify
NotifyPush(yazi_parser::notify::PushOpt),
NotifyTick(yazi_parser::notify::TickOpt),
// Pick
@ -160,6 +176,21 @@ impl<'a> IntoLua for Spark<'a> {
// Void
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
Self::Arrow(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),
// Notify
Self::NotifyPush(b) => b.into_lua(lua),
Self::NotifyTick(b) => b.into_lua(lua),
// Pick
@ -292,6 +324,8 @@ impl<'a> IntoLua for Spark<'a> {
try_from_spark!(
VoidOpt,
app:bootstrap,
app:focus,
mgr:back,
mgr:bulk_rename,
mgr:enter,
@ -309,7 +343,17 @@ try_from_spark!(
mgr:unyank,
mgr:watch
);
// App
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::ShowOpt, cmp:show);
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::PasteOpt, mgr:paste);
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::RenameOpt, mgr:rename);
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::VisualModeOpt, mgr:visual_mode);
try_from_spark!(mgr::YankOpt, mgr:yank);
try_from_spark!(notify::PushOpt, notify:push);
try_from_spark!(notify::TickOpt, notify:tick);
try_from_spark!(pick::CloseOpt, pick:close);
try_from_spark!(pick::ShowOpt, pick:show);
try_from_spark!(spot::CopyOpt, spot:copy);
try_from_spark!(tasks::ProcessOpenOpt, tasks:process_open);
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::CallbackOpt, which:callback);

View file

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

View file

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

View file

@ -1,8 +1,9 @@
use std::sync::atomic::Ordering;
use anyhow::Result;
use crossterm::event::KeyEvent;
use crossterm::event::{KeyEvent, MouseEvent};
use tracing::warn;
use yazi_actor::Ctx;
use yazi_config::keymap::Key;
use yazi_macro::{act, emit, succ};
use yazi_shared::{data::Data, event::{CmdCow, Event, NEED_RENDER}};
@ -20,17 +21,15 @@ impl<'a> Dispatcher<'a> {
#[inline]
pub(super) fn dispatch(&mut self, event: Event) -> Result<()> {
// FIXME: handle errors
let result = match event {
Event::Call(cmd) => self.dispatch_call(cmd),
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::Mouse(mouse) => act!(mouse, self.app, mouse),
Event::Resize => act!(resize, self.app),
Event::Focus => act!(focus, self.app),
Event::Mouse(mouse) => self.dispatch_mouse(mouse),
Event::Resize => self.dispatch_resize(),
Event::Focus => self.dispatch_focus(),
Event::Paste(str) => self.dispatch_paste(str),
Event::Quit(opt) => act!(quit, self.app, opt),
};
if let Err(err) = result {
@ -54,8 +53,13 @@ impl<'a> Dispatcher<'a> {
}
#[inline]
fn dispatch_render(&mut self) -> Result<Data> {
succ!(NEED_RENDER.store(true, Ordering::Relaxed))
fn dispatch_render(&mut self, partial: bool) -> Result<Data> {
if partial {
_ = NEED_RENDER.compare_exchange(0, 2, Ordering::Relaxed, Ordering::Relaxed);
} else {
NEED_RENDER.store(1, Ordering::Relaxed);
}
succ!()
}
#[inline]
@ -64,6 +68,24 @@ impl<'a> Dispatcher<'a> {
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]
fn dispatch_paste(&mut self, str: String) -> Result<Data> {
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_macro::{act, succ};
use yazi_shared::{Layer, data::Data, event::CmdCow};
@ -27,34 +27,45 @@ impl<'a> Executor<'a> {
Layer::Help => self.help(cmd),
Layer::Cmp => self.cmp(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 {
($name:ident) => {
if cmd.name == stringify!($name) {
return act!($name, self.app, cmd);
return act!(app:$name, cx, cmd);
}
};
}
on!(accept_payload);
on!(notify);
on!(plugin);
on!(plugin_do);
on!(update_notify);
on!(update_progress);
on!(resize);
on!(stop);
on!(resume);
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> {
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 {
($name:ident) => {
@ -143,13 +154,13 @@ impl<'a> Executor<'a> {
// Help
"help" => act!(help:toggle, cx, Layer::Mgr),
// Plugin
"plugin" => act!(plugin, self.app, cmd),
"plugin" => act!(app:plugin, cx, cmd),
_ => succ!(),
}
}
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 {
($name:ident) => {
@ -173,13 +184,13 @@ impl<'a> Executor<'a> {
// Help
"help" => act!(help:toggle, cx, Layer::Tasks),
// Plugin
"plugin" => act!(plugin, self.app, cmd),
"plugin" => act!(app:plugin, cx, cmd),
_ => succ!(),
}
}
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 {
($name:ident) => {
@ -198,13 +209,13 @@ impl<'a> Executor<'a> {
// Help
"help" => act!(help:toggle, cx, Layer::Spot),
// Plugin
"plugin" => act!(plugin, self.app, cmd),
"plugin" => act!(app:plugin, cx, cmd),
_ => succ!(),
}
}
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 {
($name:ident) => {
@ -222,14 +233,14 @@ impl<'a> Executor<'a> {
// Help
"help" => act!(help:toggle, cx, Layer::Pick),
// Plugin
"plugin" => act!(plugin, self.app, cmd),
"plugin" => act!(app:plugin, cx, cmd),
_ => succ!(),
}
}
fn input(&mut self, cmd: CmdCow) -> Result<Data> {
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 {
($name:ident) => {
@ -249,7 +260,7 @@ impl<'a> Executor<'a> {
// Help
"help" => return act!(help:toggle, cx, Layer::Input),
// 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> {
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 {
($name:ident) => {
@ -281,7 +292,7 @@ impl<'a> Executor<'a> {
}
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 {
($name:ident) => {
@ -299,13 +310,13 @@ impl<'a> Executor<'a> {
// Help
"close" => act!(help:toggle, cx, Layer::Help),
// Plugin
"plugin" => act!(plugin, self.app, cmd),
"plugin" => act!(app:plugin, cx, cmd),
_ => succ!(),
}
}
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 {
($name:ident) => {
@ -324,13 +335,13 @@ impl<'a> Executor<'a> {
// Help
"help" => act!(help:toggle, cx, Layer::Cmp),
// Plugin
"plugin" => act!(plugin, self.app, cmd),
"plugin" => act!(app:plugin, cx, cmd),
_ => succ!(),
}
}
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 {
($name:ident) => {
@ -345,4 +356,21 @@ impl<'a> Executor<'a> {
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> {
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> {
layout::Layout::vertical(
[Constraint::Fill(1)]
@ -42,7 +33,7 @@ impl<'a> Notify<'a> {
impl Widget for Notify<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
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 tiles = Self::tiles(available, messages.clone());

View file

@ -26,7 +26,7 @@ impl<'a> Router<'a> {
use Layer as L;
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 => {
self.matches(layer, key)
}
@ -42,7 +42,7 @@ impl<'a> Router<'a> {
}
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();
} else {
emit!(Seq(ChordCow::from(chord).into_seq()));

View file

@ -6,7 +6,7 @@ use yazi_config::YAZI;
use yazi_shared::{CompletionToken, event::Event};
pub(super) struct Signals {
tx: mpsc::UnboundedSender<(bool, CompletionToken)>,
pub(super) tx: mpsc::UnboundedSender<(bool, CompletionToken)>,
}
impl Signals {
@ -17,10 +17,6 @@ impl Signals {
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)]
fn handle_sys(n: libc::c_int) -> bool {
use libc::{SIGCONT, SIGHUP, SIGINT, SIGQUIT, SIGSTOP, SIGTERM, SIGTSTP};
@ -30,7 +26,7 @@ impl Signals {
match n {
SIGINT => { /* ignored */ }
SIGQUIT | SIGHUP | SIGTERM => {
Event::Quit(Default::default()).emit();
AppProxy::quit(Default::default());
return false;
}
SIGTSTP => {
@ -38,7 +34,7 @@ impl Signals {
AppProxy::stop().await;
if unsafe { libc::kill(0, SIGSTOP) } != 0 {
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_rules! emit {
(Quit($opt:expr)) => {
yazi_shared::event::Event::Quit($opt).emit();
};
(Call($cmd:expr)) => {
yazi_shared::event::Event::Call(yazi_shared::event::CmdCow::from($cmd)).emit();
};

View file

@ -1,7 +1,7 @@
#[macro_export]
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) => {
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 }
mlua = { workspace = true }
ordered-float = { workspace = true }
ratatui = { workspace = true }
serde = { workspace = true }
serde_with = { workspace = true }
tokio = { workspace = true }

View file

@ -1,6 +1,8 @@
use anyhow::bail;
use mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
use yazi_shared::{SStr, event::CmdCow};
#[derive(Debug)]
pub struct DeprecateOpt {
pub content: SStr,
}
@ -16,3 +18,11 @@ impl TryFrom<CmdCow> for DeprecateOpt {
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 mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
#[derive(Debug)]
pub struct MouseOpt {
pub event: MouseEvent,
}
@ -7,3 +9,11 @@ pub struct MouseOpt {
impl From<MouseEvent> for MouseOpt {
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 dyn_clone::DynClone;
use hashbrown::HashMap;
use mlua::{Lua, Table};
use mlua::{ExternalError, FromLua, IntoLua, Lua, Table, Value};
use serde::Deserialize;
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 {
pub fn new_callback(id: impl Into<SStr>, f: impl PluginCallback) -> 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 yazi_shared::{CompletionToken, event::CmdCow};
use mlua::{ExternalError, FromLua, IntoLua, Lua, Table, Value};
use ratatui::layout::Rect;
use tokio::sync::mpsc;
use yazi_shared::CompletionToken;
#[derive(Debug)]
pub struct ResumeOpt {
pub tx: mpsc::UnboundedSender<(bool, CompletionToken)>,
pub token: CompletionToken,
pub reflow: fn(Rect) -> mlua::Result<Table>,
}
impl TryFrom<CmdCow> for ResumeOpt {
type Error = anyhow::Error;
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 FromLua for ResumeOpt {
fn from_lua(_: Value, _: &Lua) -> mlua::Result<Self> { Err("unsupported".into_lua_err()) }
}
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 yazi_shared::{CompletionToken, event::CmdCow};
use mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
use tokio::sync::mpsc;
use yazi_shared::CompletionToken;
#[derive(Debug)]
pub struct StopOpt {
pub tx: mpsc::UnboundedSender<(bool, CompletionToken)>,
pub token: CompletionToken,
}
impl TryFrom<CmdCow> for StopOpt {
type Error = anyhow::Error;
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 FromLua for StopOpt {
fn from_lua(_: Value, _: &Lua) -> mlua::Result<Self> { Err("unsupported".into_lua_err()) }
}
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 mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
use ordered_float::OrderedFloat;
use serde::Serialize;
use yazi_shared::event::CmdCow;
#[derive(Debug)]
pub struct UpdateProgressOpt {
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
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
pub struct TaskSummary {

View file

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

View file

@ -19,7 +19,6 @@ yazi_macro::mod_flat!(
open_do
paste
peek
quit
remove
rename
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;
#[serde_as]
#[derive(Clone, Deserialize, Serialize)]
pub struct NotifyOpt {
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct PushOpt {
pub title: String,
pub content: String,
pub level: NotifyLevel,
pub level: PushLevel,
#[serde_as(as = "DurationSeconds<f64>")] // FIXME
pub timeout: Duration,
}
impl TryFrom<CmdCow> for NotifyOpt {
impl TryFrom<CmdCow> for PushOpt {
type Error = anyhow::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) }
}
impl IntoLua for NotifyOpt {
impl IntoLua for PushOpt {
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> { lua.to_value(&self) }
}
// --- Level
#[derive(Clone, Copy, Default, Deserialize, Eq, PartialEq, Serialize)]
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum NotifyLevel {
pub enum PushLevel {
#[default]
Info,
Warn,
Error,
}
impl NotifyLevel {
impl PushLevel {
pub fn icon(self) -> &'static str {
match self {
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;
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_config::{keymap::{Chord, ChordCow, Key}, popup::{ConfirmCfg, InputCfg}};
use yazi_macro::relay;
use yazi_parser::{app::NotifyOpt, which::ActivateOpt};
use yazi_proxy::{AppProxy, ConfirmProxy, InputProxy, WhichProxy};
use yazi_parser::{notify::PushOpt, which::ActivateOpt};
use yazi_proxy::{ConfirmProxy, InputProxy, NotifyProxy, WhichProxy};
use yazi_shared::Debounce;
use super::Utils;
@ -100,7 +100,7 @@ impl Utils {
}
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>> {

View file

@ -1,12 +1,14 @@
use std::time::Duration;
use yazi_macro::{emit, relay};
use yazi_parser::app::{NotifyLevel, NotifyOpt, PluginOpt, TaskSummary};
use yazi_parser::app::{PluginOpt, QuitOpt, TaskSummary};
use yazi_shared::CompletionToken;
pub struct AppProxy;
impl AppProxy {
pub fn quit(opt: QuitOpt) {
emit!(Call(relay!(app:quit).with_any("opt", opt)));
}
pub async fn stop() {
let token = CompletionToken::default();
emit!(Call(relay!(app:stop).with_any("token", token.clone())));
@ -19,32 +21,6 @@ impl AppProxy {
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) {
emit!(Call(relay!(app:plugin).with_any("opt", opt)));
}

View file

@ -1,5 +1,5 @@
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(); }

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 tokio::{io::{AsyncBufReadExt, BufReader}, select, sync::mpsc};
use yazi_binding::Permit;
use yazi_proxy::{AppProxy, HIDER};
use yazi_proxy::{AppProxy, HIDER, NotifyProxy};
use super::{ProcessInBg, ProcessInBlock, ProcessInOrphan, ShellOpt};
use crate::{TaskOp, TaskOps, process::{ProcessOutBg, ProcessOutBlock, ProcessOutOrphan}};
@ -20,7 +20,7 @@ impl Process {
let (id, cmd) = (task.id, task.cmd.clone());
let result = super::shell(task.into()).await;
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));
}
@ -31,7 +31,7 @@ impl Process {
Some(code) => format!("Process exited with status code: {code}"),
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))

View file

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

View file

@ -1,3 +1,3 @@
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,
Cmp,
Which,
Notify,
}
impl Display for Layer {
@ -31,6 +32,7 @@ impl Display for Layer {
Self::Help => "help",
Self::Cmp => "cmp",
Self::Which => "which",
Self::Notify => "notify",
})
}
}