diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c97d706..a3ca6912 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): - New `ind-hidden` and `key-hidden` DDS events to change hidden status in Lua ([#3748]) - New `marker_symbol` option to specify the symbol used for marking files ([#3689]) - New `--discard` for `ya pkg` that discard local changes made to packages ([#3781]) +- New `bulk_exit` action that customizes the prompt for bulk operations ([#3792]) - New `fs.unique()` creates a unique file or directory ([#3677]) - New `download` DDS event fires when remote files are downloaded ([#3687]) - New `ind-which-activate` DDS event to change the which component behavior ([#3608]) @@ -1688,3 +1689,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): [#3765]: https://github.com/sxyazi/yazi/pull/3765 [#3780]: https://github.com/sxyazi/yazi/pull/3780 [#3781]: https://github.com/sxyazi/yazi/pull/3781 +[#3792]: https://github.com/sxyazi/yazi/pull/3792 diff --git a/yazi-actor/src/mgr/bulk_exit.rs b/yazi-actor/src/mgr/bulk_exit.rs new file mode 100644 index 00000000..05271e2a --- /dev/null +++ b/yazi-actor/src/mgr/bulk_exit.rs @@ -0,0 +1,19 @@ +use anyhow::Result; +use yazi_macro::succ; +use yazi_parser::mgr::BulkExitOpt; +use yazi_shared::data::Data; + +use crate::{Actor, Ctx}; + +pub struct BulkExit; + +impl Actor for BulkExit { + type Options = BulkExitOpt; + + const NAME: &str = "bulk_exit"; + + fn act(cx: &mut Ctx, opt: Self::Options) -> Result { + cx.mgr.batcher.decide(opt.target, opt.accept); + succ!(); + } +} diff --git a/yazi-actor/src/mgr/bulk_rename.rs b/yazi-actor/src/mgr/bulk_rename.rs index 856b3f46..2681b9e6 100644 --- a/yazi-actor/src/mgr/bulk_rename.rs +++ b/yazi-actor/src/mgr/bulk_rename.rs @@ -42,8 +42,10 @@ impl Actor for BulkRename { selected.iter().enumerate().map(|(i, u)| Tuple::new(i, skip_url(u, root))).collect(); let cwd = cx.cwd().clone(); + let batcher = cx.core.mgr.batcher.clone(); tokio::spawn(async move { let tmp = YAZI.preview.tmpfile("bulk"); + Gate::default() .write(true) .create_new(true) @@ -54,10 +56,13 @@ impl Actor for BulkRename { defer! { let tmp = tmp.clone(); + batcher.drain(&tmp); tokio::spawn(async move { Local::regular(&tmp).remove_file().await }); } + + batcher.prime(&tmp); TasksProxy::process_exec( cwd.into(), Splatter::new(&[UrlCow::default(), tmp.as_url().into()]).splat(&opener.run), @@ -79,7 +84,8 @@ impl Actor for BulkRename { .map(|(i, s)| Tuple::new(i, s)) .collect(); - Self::r#do(root, old, new, selected).await + let decision = batcher.drain(&tmp); + Self::r#do(root, old, new, selected, decision).await }); succ!(); } @@ -91,6 +97,7 @@ impl BulkRename { old: Vec, new: Vec, selected: Vec, + decision: Option, ) -> Result<()> { terminal_clear(TTY.writer())?; if old.len() != new.len() { @@ -108,18 +115,7 @@ impl BulkRename { return Ok(()); } - { - let mut w = TTY.lockout(); - for (old, new) in &todo { - writeln!(w, "{} -> {}", old.display(), new.display())?; - } - write!(w, "Continue to rename? (y/N): ")?; - w.flush()?; - } - - let mut buf = [0; 10]; - _ = TTY.reader().read(&mut buf)?; - if buf[0] != b'y' && buf[0] != b'Y' { + if !Self::ask_continue(&todo, decision)? { return Ok(()); } @@ -165,6 +161,25 @@ impl BulkRename { Ok(url.try_replace(take, PathDyn::with(url.kind(), rep)?)?.into_owned()) } + fn ask_continue(todo: &[(Tuple, Tuple)], decision: Option) -> Result { + if let Some(decision) = decision { + return Ok(decision); + } + + { + let mut w = TTY.lockout(); + for (old, new) in todo { + writeln!(w, "{} -> {}", old.display(), new.display())?; + } + write!(w, "Continue to rename? (y/N): ")?; + w.flush()?; + } + + let mut buf = [0; 10]; + _ = TTY.reader().read(&mut buf)?; + Ok(buf[0] == b'y' || buf[0] == b'Y') + } + async fn output_failed(failed: Vec<(Tuple, Tuple, anyhow::Error)>) -> Result<()> { let mut stdout = TTY.lockout(); terminal_clear(&mut *stdout)?; diff --git a/yazi-actor/src/mgr/mod.rs b/yazi-actor/src/mgr/mod.rs index e85db21a..5276e8c0 100644 --- a/yazi-actor/src/mgr/mod.rs +++ b/yazi-actor/src/mgr/mod.rs @@ -1,6 +1,7 @@ yazi_macro::mod_flat!( arrow back + bulk_exit bulk_rename cd close diff --git a/yazi-core/src/mgr/batcher.rs b/yazi-core/src/mgr/batcher.rs new file mode 100644 index 00000000..de3cd7b9 --- /dev/null +++ b/yazi-core/src/mgr/batcher.rs @@ -0,0 +1,33 @@ +use std::{path::{Path, PathBuf}, sync::Arc}; + +use hashbrown::HashMap; +use parking_lot::Mutex; +use yazi_shared::url::AsUrl; + +#[derive(Clone, Default)] +pub struct Batcher { + pending: Arc>>>, +} + +impl Batcher { + pub fn prime(&self, target: T) + where + T: Into, + { + self.pending.lock().insert(target.into(), None); + } + + pub fn drain(&self, target: &Path) -> Option { + self.pending.lock().remove(target).flatten() + } + + pub fn decide(&self, target: T, decision: bool) + where + T: AsUrl, + { + let Some(path) = target.as_url().as_local() else { return }; + if let Some(value) = self.pending.lock().get_mut(path) { + *value = value.or(Some(decision)); + } + } +} diff --git a/yazi-core/src/mgr/mgr.rs b/yazi-core/src/mgr/mgr.rs index 42e2a28b..ad0cd876 100644 --- a/yazi-core/src/mgr/mgr.rs +++ b/yazi-core/src/mgr/mgr.rs @@ -7,13 +7,14 @@ use yazi_fs::Splatable; use yazi_shared::url::{AsUrl, Url, UrlBuf}; use yazi_watcher::Watcher; -use super::{Mimetype, Tabs, Yanked}; +use super::{Batcher, Mimetype, Tabs, Yanked}; use crate::tab::{Folder, Tab}; pub struct Mgr { pub tabs: Tabs, pub yanked: Yanked, + pub batcher: Batcher, pub watcher: Watcher, pub mimetype: Mimetype, } @@ -24,6 +25,7 @@ impl Mgr { tabs: Default::default(), yanked: Default::default(), + batcher: Default::default(), watcher: Watcher::serve(), mimetype: Default::default(), } diff --git a/yazi-core/src/mgr/mod.rs b/yazi-core/src/mgr/mod.rs index fee53c44..19f0aa52 100644 --- a/yazi-core/src/mgr/mod.rs +++ b/yazi-core/src/mgr/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(mgr mimetype tabs yanked); +yazi_macro::mod_flat!(batcher mgr mimetype tabs yanked); diff --git a/yazi-dds/src/spark/spark.rs b/yazi-dds/src/spark/spark.rs index 2a0441d0..a7a1ba9e 100644 --- a/yazi-dds/src/spark/spark.rs +++ b/yazi-dds/src/spark/spark.rs @@ -26,6 +26,7 @@ pub enum Spark<'a> { // Mgr Arrow(yazi_parser::ArrowOpt), Back(yazi_parser::VoidOpt), + BulkExit(yazi_parser::mgr::BulkExitOpt), BulkRename(yazi_parser::VoidOpt), Cd(yazi_parser::mgr::CdOpt), Close(yazi_parser::mgr::CloseOpt), @@ -207,6 +208,7 @@ impl<'a> IntoLua for Spark<'a> { // Mgr Self::Arrow(b) => b.into_lua(lua), Self::Back(b) => b.into_lua(lua), + Self::BulkExit(b) => b.into_lua(lua), Self::BulkRename(b) => b.into_lua(lua), Self::Cd(b) => b.into_lua(lua), Self::Close(b) => b.into_lua(lua), @@ -378,6 +380,7 @@ try_from_spark!(yazi_parser::confirm::ShowOpt, confirm:show); try_from_spark!(yazi_parser::help::ToggleOpt, help:toggle); try_from_spark!(yazi_parser::input::CloseOpt, input:close); try_from_spark!(yazi_widgets::input::InputOpt, input:show); +try_from_spark!(yazi_parser::mgr::BulkExitOpt, mgr:bulk_exit); try_from_spark!(yazi_parser::mgr::CdOpt, mgr:cd); try_from_spark!(yazi_parser::mgr::CloseOpt, mgr:close); try_from_spark!(yazi_parser::mgr::CopyOpt, mgr:copy); diff --git a/yazi-fm/src/executor.rs b/yazi-fm/src/executor.rs index 2f8396f2..05205532 100644 --- a/yazi-fm/src/executor.rs +++ b/yazi-fm/src/executor.rs @@ -124,6 +124,7 @@ impl<'a> Executor<'a> { on!(linemode); on!(search); on!(search_do); + on!(bulk_exit); on!(bulk_rename); // Filter diff --git a/yazi-parser/src/mgr/bulk_exit.rs b/yazi-parser/src/mgr/bulk_exit.rs new file mode 100644 index 00000000..3372b902 --- /dev/null +++ b/yazi-parser/src/mgr/bulk_exit.rs @@ -0,0 +1,29 @@ +use anyhow::bail; +use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; +use yazi_shared::{event::ActionCow, url::UrlCow}; + +#[derive(Debug)] +pub struct BulkExitOpt { + pub target: UrlCow<'static>, + pub accept: bool, +} + +impl TryFrom for BulkExitOpt { + type Error = anyhow::Error; + + fn try_from(mut a: ActionCow) -> Result { + let Ok(target) = a.take_first::() else { + bail!("invalid target in BulkExitOpt"); + }; + + Ok(Self { target, accept: a.bool("accept") }) + } +} + +impl FromLua for BulkExitOpt { + fn from_lua(_: Value, _: &Lua) -> mlua::Result { Err("unsupported".into_lua_err()) } +} + +impl IntoLua for BulkExitOpt { + fn into_lua(self, _: &Lua) -> mlua::Result { Err("unsupported".into_lua_err()) } +} diff --git a/yazi-parser/src/mgr/mod.rs b/yazi-parser/src/mgr/mod.rs index ff9c10e9..60bfeab5 100644 --- a/yazi-parser/src/mgr/mod.rs +++ b/yazi-parser/src/mgr/mod.rs @@ -1,4 +1,5 @@ yazi_macro::mod_flat!( + bulk_exit cd close copy