From cc2414728caa2fb848be27e3b96e977fa6913211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Wed, 18 Mar 2026 19:18:36 +0800 Subject: [PATCH] feat: new `ya.exec()` API and `ya exec` subcommand to execute an action and await its result (#3780) --- CHANGELOG.md | 2 + Cargo.lock | 23 ++++--- Cargo.toml | 2 +- yazi-cli/Cargo.toml | 1 + yazi-cli/src/args.rs | 48 +++++++++++++-- yazi-cli/src/dds/draw.rs | 42 +++++++++++++ yazi-cli/src/dds/exec.rs | 70 +++++++++++++++++++++ yazi-cli/src/dds/mod.rs | 3 + yazi-cli/src/dds/shot.rs | 76 +++++++++++++++++++++++ yazi-cli/src/main.rs | 28 ++++++--- yazi-dds/src/client.rs | 98 ++---------------------------- yazi-dds/src/ember/hi.rs | 8 ++- yazi-dds/src/payload.rs | 2 +- yazi-dds/src/stream.rs | 10 +-- yazi-fm/src/app/app.rs | 2 +- yazi-fm/src/dispatcher.rs | 54 +++++++++------- yazi-fm/src/executor.rs | 1 - yazi-plugin/Cargo.toml | 3 - yazi-plugin/preset/plugins/dds.lua | 43 ++++++++----- yazi-plugin/src/utils/call.rs | 26 ++++---- yazi-plugin/src/utils/utils.rs | 2 +- yazi-shared/src/data/data.rs | 10 ++- yazi-shared/src/event/action.rs | 4 ++ yazi-term/Cargo.toml | 3 - 24 files changed, 373 insertions(+), 188 deletions(-) create mode 100644 yazi-cli/src/dds/draw.rs create mode 100644 yazi-cli/src/dds/exec.rs create mode 100644 yazi-cli/src/dds/mod.rs create mode 100644 yazi-cli/src/dds/shot.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 08843e64..8758cc44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): - Certificate authentication for SFTP VFS provider ([#3716]) - New `hovered` condition specifying different icons for hovered files ([#3728]) - Allow using `ps.sub()` in `init.lua` directly without a plugin ([#3638]) +- New `ya.exec()` API and `ya exec` subcommand to execute an action and await its result ([#3780]) - New `sort_fallback` option to control fallback sorting behavior ([#3077]) - New `fs.access()` API to access the filesystem ([#3668]) - New `relay-notify-push` DDS event to customize the notification handler ([#3642]) @@ -1684,3 +1685,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): [#3748]: https://github.com/sxyazi/yazi/pull/3748 [#3757]: https://github.com/sxyazi/yazi/pull/3757 [#3765]: https://github.com/sxyazi/yazi/pull/3765 +[#3780]: https://github.com/sxyazi/yazi/pull/3780 diff --git a/Cargo.lock b/Cargo.lock index bca5df10..b89e4e89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4626,9 +4626,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.0.6+spec-1.1.0" +version = "1.0.7+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "399b1124a3c9e16766831c6bba21e50192572cdd98706ea114f9502509686ffc" +checksum = "dd28d57d8a6f6e458bc0b8784f8fdcc4b99a437936056fa122cb234f18656a96" dependencies = [ "indexmap 2.13.0", "serde_core", @@ -4641,27 +4641,27 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.0.0+spec-1.1.0" +version = "1.0.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9" dependencies = [ "serde_core", ] [[package]] name = "toml_parser" -version = "1.0.9+spec-1.1.0" +version = "1.0.10+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.0.6+spec-1.1.0" +version = "1.0.7+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d" [[package]] name = "tracing" @@ -5541,9 +5541,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.15" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" [[package]] name = "wit-bindgen" @@ -5773,6 +5773,7 @@ dependencies = [ "clap_complete_fig", "clap_complete_nushell", "crossterm 0.29.0", + "hashbrown 0.16.1", "serde", "serde_json", "tokio", @@ -6015,7 +6016,6 @@ version = "26.2.2" dependencies = [ "ansi-to-tui", "anyhow", - "crossterm 0.29.0", "futures", "hashbrown 0.16.1", "libc", @@ -6151,7 +6151,6 @@ version = "26.2.2" dependencies = [ "anyhow", "crossterm 0.29.0", - "libc", "ratatui", "tokio", "yazi-config", diff --git a/Cargo.toml b/Cargo.toml index 966f3719..a7206601 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,7 +72,7 @@ thiserror = "2.0.18" tokio = { version = "1.50.0", features = [ "full" ] } tokio-stream = "0.1.18" tokio-util = "0.7.18" -toml = { version = "1.0.6" } +toml = { version = "1.0.7" } tracing = { version = "0.1.44", features = [ "max_level_debug", "release_max_level_debug" ] } twox-hash = { version = "2.1.2", default-features = false, features = [ "std", "random", "xxhash3_128" ] } typed-path = "0.12.3" diff --git a/yazi-cli/Cargo.toml b/yazi-cli/Cargo.toml index ca446679..900ac43b 100644 --- a/yazi-cli/Cargo.toml +++ b/yazi-cli/Cargo.toml @@ -34,6 +34,7 @@ yazi-shim = { path = "../yazi-shim", version = "26.2.2" } anyhow = { workspace = true } clap = { workspace = true } crossterm = { workspace = true } +hashbrown = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } diff --git a/yazi-cli/src/args.rs b/yazi-cli/src/args.rs index a2728658..591acb65 100644 --- a/yazi-cli/src/args.rs +++ b/yazi-cli/src/args.rs @@ -17,10 +17,12 @@ pub(super) struct Args { #[derive(Subcommand)] pub(super) enum Command { - /// Emit a command to be executed by the current instance. + /// Emit an action to be executed by the current instance. Emit(CommandEmit), - /// Emit a command to be executed by the specified instance. + /// Emit an action to be executed by the specified instance. EmitTo(CommandEmitTo), + /// Execute an action on the current instance and print its result. + Exec(CommandExec), /// Manage packages. #[command(subcommand)] Pkg(CommandPkg), @@ -34,9 +36,9 @@ pub(super) enum Command { #[derive(clap::Args)] pub(super) struct CommandEmit { - /// Name of the command. + /// Name of the action. pub(super) name: String, - /// Arguments of the command. + /// Arguments of the action. #[arg(allow_hyphen_values = true, trailing_var_arg = true)] pub(super) args: Vec, } @@ -45,13 +47,22 @@ pub(super) struct CommandEmit { pub(super) struct CommandEmitTo { /// Receiver ID. pub(super) receiver: Id, - /// Name of the command. + /// Name of the action. pub(super) name: String, - /// Arguments of the command. + /// Arguments of the action. #[arg(allow_hyphen_values = true, trailing_var_arg = true)] pub(super) args: Vec, } +#[derive(clap::Args)] +pub(super) struct CommandExec { + /// Name of the action. + pub(super) name: String, + /// Arguments of the action. + #[arg(allow_hyphen_values = true, trailing_var_arg = true)] + pub(super) args: Vec, +} + #[derive(Subcommand)] pub(super) enum CommandPkg { /// Add packages. @@ -156,6 +167,29 @@ macro_rules! impl_emit_body { }; } +macro_rules! impl_exec_body { + ($name:ident) => { + impl $name { + #[allow(dead_code)] + pub(super) fn body(self, reply_to: Id) -> Result { + #[derive(serde::Serialize)] + #[serde(untagged)] + enum Elem { + Id(Id), + Name(String), + Arg(Vec), + } + + let action: Vec<_> = [Elem::Id(reply_to), Elem::Name(self.name)] + .into_iter() + .chain(self.args.into_iter().map(|s| Elem::Arg(s.into_encoded_bytes()))) + .collect(); + Ok(serde_json::to_string(&action)?) + } + } + }; +} + macro_rules! impl_pub_body { ($name:ident) => { impl $name { @@ -178,5 +212,7 @@ macro_rules! impl_pub_body { impl_emit_body!(CommandEmit); impl_emit_body!(CommandEmitTo); +impl_exec_body!(CommandExec); + impl_pub_body!(CommandPub); impl_pub_body!(CommandPubTo); diff --git a/yazi-cli/src/dds/draw.rs b/yazi-cli/src/dds/draw.rs new file mode 100644 index 00000000..80625f30 --- /dev/null +++ b/yazi-cli/src/dds/draw.rs @@ -0,0 +1,42 @@ +use anyhow::{Context, Result}; +use hashbrown::HashSet; +use tokio::{io::AsyncWriteExt, time}; +use yazi_dds::{ClientReader, Payload, Stream, ember::EmberHi}; +use yazi_macro::try_format; + +use crate::dds::Dds; + +impl Dds { + /// Connect to an existing server and listen in on the messages that are being + /// sent by other yazi instances: + /// - If no server is running, fail right away; + /// - If a server is closed, attempt to reconnect forever. + pub(crate) async fn draw(kinds: HashSet<&str>) -> Result<()> { + async fn make(kinds: &HashSet<&str>) -> Result { + let (lines, mut writer) = Stream::connect().await?; + let hi = Payload::new(EmberHi::borrowed(kinds.iter().copied())); + writer.write_all(try_format!("{hi}\n")?.as_bytes()).await?; + writer.flush().await?; + Ok(lines) + } + + let mut lines = make(&kinds).await.context("No running Yazi instance found")?; + loop { + match lines.next_line().await? { + Some(s) => { + let kind = s.split(',').next(); + if matches!(kind, Some(kind) if kinds.contains(kind)) { + println!("{s}"); + } + } + None => loop { + time::sleep(time::Duration::from_secs(1)).await; + if let Ok(new) = make(&kinds).await { + lines = new; + break; + } + }, + } + } + } +} diff --git a/yazi-cli/src/dds/exec.rs b/yazi-cli/src/dds/exec.rs new file mode 100644 index 00000000..a5d2a660 --- /dev/null +++ b/yazi-cli/src/dds/exec.rs @@ -0,0 +1,70 @@ +use std::str::FromStr; + +use anyhow::{Result, bail}; +use serde::Deserialize; +use tokio::io::AsyncWriteExt; +use yazi_dds::{ID, Payload, Stream, ember::{Ember, EmberHi}}; +use yazi_macro::try_format; +use yazi_shared::{Id, data::Data}; + +use crate::{CommandExec, CommandPub, dds::Dds}; + +impl Dds { + pub(crate) async fn exec(cmd: CommandExec) -> anyhow::Result { + let receiver = CommandPub::receiver()?; + let req = cmd.body(*yazi_dds::ID)?; + let resp = Self::ask("dds-exec", receiver, &req, "dds-exec-result").await?; + + #[derive(Deserialize)] + struct Body { + ok: bool, + #[serde(default)] + value: Data, + #[serde(default)] + error: String, + } + + let body = Body::deserialize(&resp)?; + if body.ok { + Ok(body.value) + } else if !body.error.is_empty() { + bail!("{}", body.error) + } else { + bail!("Unknown error") + } + } + + /// Send one custom message and wait for a matching custom reply. + async fn ask(kind: &str, receiver: Id, body: &str, reply_kind: &str) -> Result { + Ember::validate(kind)?; + Ember::validate(reply_kind)?; + + let payload = try_format!( + "{}\n{kind},{receiver},{ID},{body}\n", + Payload::new(EmberHi::borrowed([reply_kind])), + )?; + + let (mut lines, mut writer) = Stream::connect().await?; + writer.write_all(payload.as_bytes()).await?; + writer.flush().await?; + drop(writer); + + while let Ok(Some(line)) = lines.next_line().await { + match line.split(',').next() { + Some("hey") => { + if let Ok(Ember::Hey(hey)) = Payload::from_str(&line).map(|p| p.body) { + Self::ensure_version(Some(&hey.version))?; + Self::ensure_ability(&hey.peers, kind, receiver)?; + } + } + Some(kind) if kind == reply_kind => match Payload::from_str(&line)?.body { + Ember::Custom(body) => return Ok(body.data), + _ => bail!("Expected custom payload of kind `{reply_kind}`"), + }, + _ => {} + } + } + + bail!("Connection closed before receiving reply") + } +} diff --git a/yazi-cli/src/dds/mod.rs b/yazi-cli/src/dds/mod.rs new file mode 100644 index 00000000..af442055 --- /dev/null +++ b/yazi-cli/src/dds/mod.rs @@ -0,0 +1,3 @@ +yazi_macro::mod_flat!(draw exec shot); + +pub(crate) struct Dds; diff --git a/yazi-cli/src/dds/shot.rs b/yazi-cli/src/dds/shot.rs new file mode 100644 index 00000000..77db3957 --- /dev/null +++ b/yazi-cli/src/dds/shot.rs @@ -0,0 +1,76 @@ +use std::str::FromStr; + +use anyhow::{Result, bail}; +use hashbrown::HashMap; +use tokio::io::AsyncWriteExt; +use yazi_dds::{ID, Payload, Peer, Stream, ember::{Ember, EmberBye, EmberHi}}; +use yazi_macro::try_format; +use yazi_shared::Id; + +use crate::dds::Dds; + +impl Dds { + /// Connect to an existing server to send a single message. + pub(crate) async fn shot(kind: &str, receiver: Id, body: &str) -> Result<()> { + Ember::validate(kind)?; + + let payload = try_format!( + "{}\n{kind},{receiver},{ID},{body}\n{}\n", + Payload::new(EmberHi::borrowed([])), + Payload::new(EmberBye::borrowed()) + )?; + + let (mut lines, mut writer) = Stream::connect().await?; + writer.write_all(payload.as_bytes()).await?; + writer.flush().await?; + drop(writer); + + let (mut peers, mut version) = Default::default(); + while let Ok(Some(line)) = lines.next_line().await { + match line.split(',').next() { + Some("hey") => { + if let Ok(Ember::Hey(hey)) = Payload::from_str(&line).map(|p| p.body) { + (peers, version) = (hey.peers, Some(hey.version)); + } + } + Some("bye") => break, + _ => {} + } + } + + Self::ensure_version(version.as_deref())?; + Self::ensure_ability(&peers, kind, receiver)?; + Ok(()) + } + + pub(super) fn ensure_version(version: Option<&str>) -> Result<()> { + if version.as_deref() != Some(EmberHi::version()) { + bail!( + "Incompatible version (Ya {}, Yazi {}). Restart all `ya` and `yazi` processes if you upgrade either one.", + EmberHi::version(), + version.as_deref().unwrap_or("Unknown") + ); + } + Ok(()) + } + + pub(super) fn ensure_ability(peers: &HashMap, kind: &str, receiver: Id) -> Result<()> { + match (receiver, peers.get(&receiver).map(|p| p.able(kind))) { + // Send to all receivers + (Id(0), _) if peers.is_empty() => { + bail!("No receiver found. Check if any receivers are running.") + } + (Id(0), _) if peers.values().all(|p| !p.able(kind)) => { + bail!("No receiver has the ability to receive `{kind}` messages.") + } + (Id(0), _) => Ok(()), + + // Send to a specific receiver + (_, Some(true)) => Ok(()), + (_, Some(false)) => { + bail!("Receiver `{receiver}` does not have the ability to receive `{kind}` messages.") + } + (_, None) => bail!("Receiver `{receiver}` not found. Check if the receiver is running."), + } + } +} diff --git a/yazi-cli/src/main.rs b/yazi-cli/src/main.rs index a2a15be2..f43e9228 100644 --- a/yazi-cli/src/main.rs +++ b/yazi-cli/src/main.rs @@ -1,4 +1,4 @@ -yazi_macro::mod_pub!(package shared); +yazi_macro::mod_pub!(dds package shared); yazi_macro::mod_flat!(args); @@ -44,9 +44,7 @@ async fn run() -> anyhow::Result<()> { Command::Emit(cmd) => { yazi_boot::init_default(); yazi_dds::init(); - if let Err(e) = - yazi_dds::Client::shot("dds-emit", CommandPub::receiver()?, &cmd.body()?).await - { + if let Err(e) = dds::Dds::shot("dds-emit", CommandPub::receiver()?, &cmd.body()?).await { errln!("Cannot emit command: {e}")?; std::process::exit(1); } @@ -55,12 +53,25 @@ async fn run() -> anyhow::Result<()> { Command::EmitTo(cmd) => { yazi_boot::init_default(); yazi_dds::init(); - if let Err(e) = yazi_dds::Client::shot("dds-emit", cmd.receiver, &cmd.body()?).await { + if let Err(e) = dds::Dds::shot("dds-emit", cmd.receiver, &cmd.body()?).await { errln!("Cannot emit command: {e}")?; std::process::exit(1); } } + Command::Exec(cmd) => { + yazi_boot::init_default(); + yazi_dds::init(); + + match dds::Dds::exec(cmd).await { + Ok(data) => outln!("{}", serde_json::to_string(&data)?)?, + Err(e) => { + errln!("Cannot execute command: {e}")?; + std::process::exit(1); + } + } + } + Command::Pkg(cmd) => { package::init()?; @@ -77,8 +88,7 @@ async fn run() -> anyhow::Result<()> { Command::Pub(cmd) => { yazi_boot::init_default(); yazi_dds::init(); - if let Err(e) = yazi_dds::Client::shot(&cmd.kind, CommandPub::receiver()?, &cmd.body()?).await - { + if let Err(e) = dds::Dds::shot(&cmd.kind, CommandPub::receiver()?, &cmd.body()?).await { errln!("Cannot send message: {e}")?; std::process::exit(1); } @@ -87,7 +97,7 @@ async fn run() -> anyhow::Result<()> { Command::PubTo(cmd) => { yazi_boot::init_default(); yazi_dds::init(); - if let Err(e) = yazi_dds::Client::shot(&cmd.kind, cmd.receiver, &cmd.body()?).await { + if let Err(e) = dds::Dds::shot(&cmd.kind, cmd.receiver, &cmd.body()?).await { errln!("Cannot send message: {e}")?; std::process::exit(1); } @@ -96,7 +106,7 @@ async fn run() -> anyhow::Result<()> { Command::Sub(cmd) => { yazi_boot::init_default(); yazi_dds::init(); - yazi_dds::Client::draw(cmd.kinds.split(',').collect()).await?; + dds::Dds::draw(cmd.kinds.split(',').collect()).await?; tokio::signal::ctrl_c().await?; } diff --git a/yazi-dds/src/client.rs b/yazi-dds/src/client.rs index 8fdffec3..cf4dbce2 100644 --- a/yazi-dds/src/client.rs +++ b/yazi-dds/src/client.rs @@ -1,6 +1,6 @@ -use std::{iter, mem, str::FromStr}; +use std::{mem, str::FromStr}; -use anyhow::{Context, Result, bail}; +use anyhow::Result; use hashbrown::{HashMap, HashSet}; use parking_lot::RwLock; use serde::{Deserialize, Serialize}; @@ -9,7 +9,7 @@ use tracing::error; use yazi_macro::try_format; use yazi_shared::{Id, RoCell}; -use crate::{ClientReader, ClientWriter, Payload, Pubsub, Server, Stream, ember::{Ember, EmberBye, EmberHey, EmberHi}}; +use crate::{ClientReader, ClientWriter, Payload, Pubsub, Server, Stream, ember::{Ember, EmberHey}}; pub static ID: RoCell = RoCell::new(); pub(super) static PEERS: RoCell>> = RoCell::new(); @@ -77,96 +77,6 @@ impl Client { }); } - /// Connect to an existing server to send a single message. - pub async fn shot(kind: &str, receiver: Id, body: &str) -> Result<()> { - Ember::validate(kind)?; - - let payload = try_format!( - "{}\n{kind},{receiver},{ID},{body}\n{}\n", - Payload::new(EmberHi::borrowed(iter::empty())), - Payload::new(EmberBye::borrowed()) - )?; - - let (mut lines, mut writer) = Stream::connect().await?; - writer.write_all(payload.as_bytes()).await?; - writer.flush().await?; - drop(writer); - - let (mut peers, mut version) = Default::default(); - while let Ok(Some(line)) = lines.next_line().await { - match line.split(',').next() { - Some("hey") => { - if let Ok(Ember::Hey(hey)) = Payload::from_str(&line).map(|p| p.body) { - (peers, version) = (hey.peers, Some(hey.version)); - } - } - Some("bye") => break, - _ => {} - } - } - - if version.as_deref() != Some(EmberHi::version()) { - bail!( - "Incompatible version (Ya {}, Yazi {}). Restart all `ya` and `yazi` processes if you upgrade either one.", - EmberHi::version(), - version.as_deref().unwrap_or("Unknown") - ); - } - - match (receiver, peers.get(&receiver).map(|p| p.able(kind))) { - // Send to all receivers - (Id(0), _) if peers.is_empty() => { - bail!("No receiver found. Check if any receivers are running.") - } - (Id(0), _) if peers.values().all(|p| !p.able(kind)) => { - bail!("No receiver has the ability to receive `{kind}` messages.") - } - (Id(0), _) => {} - - // Send to a specific receiver - (_, Some(true)) => {} - (_, Some(false)) => { - bail!("Receiver `{receiver}` does not have the ability to receive `{kind}` messages.") - } - (_, None) => bail!("Receiver `{receiver}` not found. Check if the receiver is running."), - } - - Ok(()) - } - - /// Connect to an existing server and listen in on the messages that are being - /// sent by other yazi instances: - /// - If no server is running, fail right away; - /// - If a server is closed, attempt to reconnect forever. - pub async fn draw(kinds: HashSet<&str>) -> Result<()> { - async fn make(kinds: &HashSet<&str>) -> Result { - let (lines, mut writer) = Stream::connect().await?; - let hi = Payload::new(EmberHi::borrowed(kinds.iter().copied())); - writer.write_all(try_format!("{hi}\n")?.as_bytes()).await?; - writer.flush().await?; - Ok(lines) - } - - let mut lines = make(&kinds).await.context("No running Yazi instance found")?; - loop { - match lines.next_line().await? { - Some(s) => { - let kind = s.split(',').next(); - if matches!(kind, Some(kind) if kinds.contains(kind)) { - println!("{s}"); - } - } - None => loop { - time::sleep(time::Duration::from_secs(1)).await; - if let Ok(new) = make(&kinds).await { - lines = new; - break; - } - }, - } - } - } - pub(super) fn push<'a>(payload: impl Into>) -> Result<()> { Ok(QUEUE_TX.send(try_format!("{}\n", payload.into())?)?) } @@ -221,5 +131,5 @@ impl Client { impl Peer { pub(super) fn new(abilities: &HashSet) -> Self { Self { abilities: abilities.clone() } } - pub(super) fn able(&self, ability: &str) -> bool { self.abilities.contains(ability) } + pub fn able(&self, ability: &str) -> bool { self.abilities.contains(ability) } } diff --git a/yazi-dds/src/ember/hi.rs b/yazi-dds/src/ember/hi.rs index d94d804b..f8b1149b 100644 --- a/yazi-dds/src/ember/hi.rs +++ b/yazi-dds/src/ember/hi.rs @@ -18,9 +18,13 @@ pub struct EmberHi<'a> { impl<'a> EmberHi<'a> { pub fn borrowed(abilities: I) -> Ember<'a> where - I: Iterator, + I: IntoIterator, { - Self { abilities: abilities.map(Into::into).collect(), version: Self::version().into() }.into() + Self { + abilities: abilities.into_iter().map(Into::into).collect(), + version: Self::version().into(), + } + .into() } pub fn version() -> &'static str { diff --git a/yazi-dds/src/payload.rs b/yazi-dds/src/payload.rs index f1dd3d59..c1281358 100644 --- a/yazi-dds/src/payload.rs +++ b/yazi-dds/src/payload.rs @@ -16,7 +16,7 @@ pub struct Payload<'a> { } impl<'a> Payload<'a> { - pub(super) fn new(body: Ember<'a>) -> Self { Self { receiver: Id(0), sender: *ID, body } } + pub fn new(body: Ember<'a>) -> Self { Self { receiver: Id(0), sender: *ID, body } } pub(super) fn flush(&self) -> Result<()> { writeln!(std::io::stdout(), "{self}")?; diff --git a/yazi-dds/src/stream.rs b/yazi-dds/src/stream.rs index e4b5b6e9..dcd4a650 100644 --- a/yazi-dds/src/stream.rs +++ b/yazi-dds/src/stream.rs @@ -1,13 +1,13 @@ use tokio::io::{BufReader, Lines, ReadHalf, WriteHalf}; -pub(super) struct Stream; +pub struct Stream; use tokio::io::AsyncBufReadExt; #[cfg(unix)] -pub(super) type ClientReader = Lines>>; +pub type ClientReader = Lines>>; #[cfg(not(unix))] -pub(super) type ClientReader = Lines>>; +pub type ClientReader = Lines>>; #[cfg(unix)] pub(super) type ClientWriter = WriteHalf; @@ -21,14 +21,14 @@ pub(super) type ServerListener = tokio::net::TcpListener; impl Stream { #[cfg(unix)] - pub(super) async fn connect() -> std::io::Result<(ClientReader, ClientWriter)> { + pub async fn connect() -> std::io::Result<(ClientReader, ClientWriter)> { let stream = tokio::net::UnixStream::connect(Self::socket_file()).await?; let (reader, writer) = tokio::io::split(stream); Ok((BufReader::new(reader).lines(), writer)) } #[cfg(not(unix))] - pub(super) async fn connect() -> std::io::Result<(ClientReader, ClientWriter)> { + pub async fn connect() -> std::io::Result<(ClientReader, ClientWriter)> { let stream = tokio::net::TcpStream::connect("127.0.0.1:33581").await?; let (reader, writer) = tokio::io::split(stream); Ok((BufReader::new(reader).lines(), writer)) diff --git a/yazi-fm/src/app/app.rs b/yazi-fm/src/app/app.rs index 9e14b296..1ea6622e 100644 --- a/yazi-fm/src/app/app.rs +++ b/yazi-fm/src/app/app.rs @@ -29,7 +29,7 @@ impl App { macro_rules! drain_events { () => { for event in events.drain(..) { - Dispatcher::new(&mut app).dispatch(event)?; + Dispatcher::new(&mut app).dispatch(event); need_render = NEED_RENDER.load(Ordering::Relaxed); if need_render == 0 { diff --git a/yazi-fm/src/dispatcher.rs b/yazi-fm/src/dispatcher.rs index fac9e74e..2b5af3d6 100644 --- a/yazi-fm/src/dispatcher.rs +++ b/yazi-fm/src/dispatcher.rs @@ -2,10 +2,11 @@ use std::sync::atomic::Ordering; use anyhow::Result; use crossterm::event::{KeyEvent, MouseEvent}; +use tokio::sync::mpsc; use tracing::warn; use yazi_actor::Ctx; use yazi_config::keymap::Key; -use yazi_macro::{act, emit, succ}; +use yazi_macro::{act, emit}; use yazi_shared::{data::Data, event::{ActionCow, Event, NEED_RENDER}}; use yazi_widgets::input::InputMode; @@ -20,7 +21,7 @@ impl<'a> Dispatcher<'a> { pub(super) fn new(app: &'a mut App) -> Self { Self { app } } #[inline] - pub(super) fn dispatch(&mut self, event: Event) -> Result<()> { + pub(super) fn dispatch(&mut self, event: Event) { let result = match event { Event::Call(action) => self.dispatch_call(action), Event::Seq(actions) => self.dispatch_seq(actions), @@ -32,64 +33,71 @@ impl<'a> Dispatcher<'a> { Event::Paste(str) => self.dispatch_paste(str), }; - if let Err(err) = result { - warn!("Event dispatch error: {err:?}"); + if let Err(e) = &result { + warn!("Event dispatch error: {e:?}"); + } + } + + fn dispatch_call(&mut self, action: ActionCow) -> Result<()> { + let tx = action.any::>>("__reply").cloned(); + let result = Executor::new(self.app).execute(action); + + if let Err(e) = &result { + warn!("Call dispatch error: {e:?}"); + } + if let Some(tx) = tx { + tx.send(result).ok(); } Ok(()) } #[inline] - fn dispatch_call(&mut self, action: ActionCow) -> Result { - Executor::new(self.app).execute(action) - } - - #[inline] - fn dispatch_seq(&mut self, mut actions: Vec) -> Result { + fn dispatch_seq(&mut self, mut actions: Vec) -> Result<()> { if let Some(last) = actions.pop() { self.dispatch_call(last)?; } if !actions.is_empty() { emit!(Seq(actions)); } - succ!(); + Ok(()) } #[inline] - fn dispatch_render(&mut self, partial: bool) -> Result { + fn dispatch_render(&mut self, partial: bool) -> Result<()> { if partial { _ = NEED_RENDER.compare_exchange(0, 2, Ordering::Relaxed, Ordering::Relaxed); } else { NEED_RENDER.store(1, Ordering::Relaxed); } - succ!() + Ok(()) } #[inline] - fn dispatch_key(&mut self, key: KeyEvent) -> Result { + fn dispatch_key(&mut self, key: KeyEvent) -> Result<()> { Router::new(self.app).route(Key::from(key))?; - succ!(); + Ok(()) } #[inline] - fn dispatch_mouse(&mut self, mouse: MouseEvent) -> Result { + fn dispatch_mouse(&mut self, mouse: MouseEvent) -> Result<()> { let cx = &mut Ctx::active(&mut self.app.core, &mut self.app.term); - act!(app:mouse, cx, mouse) + act!(app:mouse, cx, mouse).map(|_| ()) } #[inline] - fn dispatch_resize(&mut self) -> Result { + fn dispatch_resize(&mut self) -> Result<()> { let cx = &mut Ctx::active(&mut self.app.core, &mut self.app.term); - act!(app:resize, cx, crate::Root::reflow as fn(_) -> _) + act!(app:resize, cx, crate::Root::reflow as fn(_) -> _).map(|_| ()) } #[inline] - fn dispatch_focus(&mut self) -> Result { + fn dispatch_focus(&mut self) -> Result<()> { let cx = &mut Ctx::active(&mut self.app.core, &mut self.app.term); - act!(app:focus, cx) + act!(app:focus, cx).map(|_| ()) } #[inline] - fn dispatch_paste(&mut self, str: String) -> Result { + fn dispatch_paste(&mut self, str: String) -> Result<()> { if self.app.core.input.visible { let input = &mut self.app.core.input; if input.mode() == InputMode::Insert { @@ -98,6 +106,6 @@ impl<'a> Dispatcher<'a> { input.replace_str(&str)?; } } - succ!(); + Ok(()) } } diff --git a/yazi-fm/src/executor.rs b/yazi-fm/src/executor.rs index bf35ce13..2f8396f2 100644 --- a/yazi-fm/src/executor.rs +++ b/yazi-fm/src/executor.rs @@ -14,7 +14,6 @@ impl<'a> Executor<'a> { #[inline] pub(super) fn new(app: &'a mut App) -> Self { Self { app } } - #[inline] pub(super) fn execute(&mut self, action: ActionCow) -> Result { match action.layer { Layer::App => self.app(action), diff --git a/yazi-plugin/Cargo.toml b/yazi-plugin/Cargo.toml index b79c4d59..16493a9b 100644 --- a/yazi-plugin/Cargo.toml +++ b/yazi-plugin/Cargo.toml @@ -58,6 +58,3 @@ uzers = { workspace = true } [target."cfg(windows)".dependencies] windows-sys = { version = "0.61.2", features = [ "Win32_System_JobObjects" ] } - -[target.'cfg(target_os = "macos")'.dependencies] -crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } diff --git a/yazi-plugin/preset/plugins/dds.lua b/yazi-plugin/preset/plugins/dds.lua index 4b28fda0..c81857ab 100644 --- a/yazi-plugin/preset/plugins/dds.lua +++ b/yazi-plugin/preset/plugins/dds.lua @@ -1,20 +1,35 @@ local M = {} -function M:setup() - ps.sub_remote("dds-emit", function(action) - local i, args = 1, {} - for j = 2, #action do - local word = string.char(table.unpack(action[j])) - local key = word:match("^%-%-([^=]+)") - if not key then - i, args[i] = i + 1, word - elseif #key + 2 == #word then - args[key] = true - else - args[key] = word:sub(#key + 4) - end +function M.parse_args(t, i) + local j, args = 1, {} + for i = i, #t do + local word = string.char(table.unpack(t[i])) + local key = word:match("^%-%-([^=]+)") + if not key then + j, args[j] = j + 1, word + elseif #key + 2 == #word then + args[key] = true + else + args[key] = word:sub(#key + 4) end - ya.emit(action[1], args) + end + return args +end + +function M:setup() + ps.sub_remote("dds-emit", function(t) ya.emit(t[1], M.parse_args(t, 2)) end) + + ps.sub_remote("dds-exec", function(t) + ya.async(function() + local ok, value = pcall(ya.exec, t[2], M.parse_args(t, 3)) + ps.pub_to(t[1], "dds-exec-result", ok and { + ok = true, + value = value, + } or { + ok = false, + error = tostring(value), + }) + end) end) end diff --git a/yazi-plugin/src/utils/call.rs b/yazi-plugin/src/utils/call.rs index d88d1b51..ac3fb1d3 100644 --- a/yazi-plugin/src/utils/call.rs +++ b/yazi-plugin/src/utils/call.rs @@ -1,7 +1,8 @@ -use mlua::{Function, Lua, Table}; +use mlua::{ExternalError, Function, Lua, Table}; +use tokio::sync::mpsc; use yazi_dds::Sendable; use yazi_macro::emit; -use yazi_shared::{Layer, Source, event::Action}; +use yazi_shared::{Layer, Source, data::Data, event::Action}; use super::Utils; @@ -14,15 +15,18 @@ impl Utils { }) } - pub(super) fn mgr_emit(lua: &Lua) -> mlua::Result { - lua.create_function(|lua, (name, args): (String, Table)| { - emit!(Call(Action { - name: name.into(), - args: Sendable::table_to_args(lua, args)?, - layer: Layer::Mgr, - source: Source::Emit, - })); - Ok(()) + pub(super) fn exec(lua: &Lua) -> mlua::Result { + lua.create_async_function(|lua, (name, args): (String, Table)| async move { + let mut action = Action::new(name, Source::Emit, Some(Layer::Mgr))?; + action.args = Sendable::table_to_args(&lua, args)?; + + let (tx, mut rx) = mpsc::unbounded_channel::>(); + emit!(Call(action.with_any("__reply", tx))); + + Sendable::data_to_value( + &lua, + rx.recv().await.ok_or_else(|| "channel closed before action response".into_lua_err())??, + ) }) } } diff --git a/yazi-plugin/src/utils/utils.rs b/yazi-plugin/src/utils/utils.rs index 3d5db587..971661d5 100644 --- a/yazi-plugin/src/utils/utils.rs +++ b/yazi-plugin/src/utils/utils.rs @@ -17,7 +17,7 @@ pub fn compose( // Call b"emit" => Utils::emit(lua)?, - b"mgr_emit" => Utils::mgr_emit(lua)?, + b"exec" => Utils::exec(lua)?, // Image b"image_info" => Utils::image_info(lua)?, diff --git a/yazi-shared/src/data/data.rs b/yazi-shared/src/data/data.rs index b14346b9..a0146b27 100644 --- a/yazi-shared/src/data/data.rs +++ b/yazi-shared/src/data/data.rs @@ -7,9 +7,10 @@ use serde::{Deserialize, Serialize}; use crate::{Id, SStr, data::{DataAny, DataKey}, path::PathBufDyn, strand::{IntoStrand, StrandBuf}, url::{UrlBuf, UrlCow}}; // --- Data -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(untagged)] pub enum Data { + #[default] Nil, Boolean(bool), Integer(i64), @@ -216,6 +217,13 @@ impl Data { } } + pub fn as_any(&self) -> Option<&T> { + match self { + Self::Any(b) => (&**b).as_any().downcast_ref::(), + _ => None, + } + } + pub fn into_string(self) -> Option { match self { Self::String(s) => Some(s), diff --git a/yazi-shared/src/event/action.rs b/yazi-shared/src/event/action.rs index 82d5cb11..78df5796 100644 --- a/yazi-shared/src/event/action.rs +++ b/yazi-shared/src/event/action.rs @@ -98,6 +98,10 @@ impl Action { pub fn bool(&self, name: impl Into) -> bool { self.get(name).unwrap_or(false) } + pub fn any(&self, name: impl Into) -> Option<&T> { + self.args.get(&name.into())?.as_any() + } + pub fn first<'a, T>(&'a self) -> Result where T: TryFrom<&'a Data>, diff --git a/yazi-term/Cargo.toml b/yazi-term/Cargo.toml index e97b8382..68ae8a8f 100644 --- a/yazi-term/Cargo.toml +++ b/yazi-term/Cargo.toml @@ -26,8 +26,5 @@ crossterm = { workspace = true } ratatui = { workspace = true } tokio = { workspace = true } -[target."cfg(unix)".dependencies] -libc = { workspace = true } - [target.'cfg(target_os = "macos")'.dependencies] crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] }