feat: new bulk_exit action that customizes the prompt for bulk operations (#3792)

This commit is contained in:
三咲雅 misaki masa 2026-03-21 17:26:44 +08:00 committed by GitHub
parent 7b61eb1595
commit c703332b4b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 121 additions and 15 deletions

View file

@ -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

View file

@ -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<Data> {
cx.mgr.batcher.decide(opt.target, opt.accept);
succ!();
}
}

View file

@ -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<Tuple>,
new: Vec<Tuple>,
selected: Vec<UrlBuf>,
decision: Option<bool>,
) -> 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<bool>) -> Result<bool> {
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)?;

View file

@ -1,6 +1,7 @@
yazi_macro::mod_flat!(
arrow
back
bulk_exit
bulk_rename
cd
close

View file

@ -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<Mutex<HashMap<PathBuf, Option<bool>>>>,
}
impl Batcher {
pub fn prime<T>(&self, target: T)
where
T: Into<PathBuf>,
{
self.pending.lock().insert(target.into(), None);
}
pub fn drain(&self, target: &Path) -> Option<bool> {
self.pending.lock().remove(target).flatten()
}
pub fn decide<T>(&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));
}
}
}

View file

@ -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(),
}

View file

@ -1 +1 @@
yazi_macro::mod_flat!(mgr mimetype tabs yanked);
yazi_macro::mod_flat!(batcher mgr mimetype tabs yanked);

View file

@ -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);

View file

@ -124,6 +124,7 @@ impl<'a> Executor<'a> {
on!(linemode);
on!(search);
on!(search_do);
on!(bulk_exit);
on!(bulk_rename);
// Filter

View file

@ -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<ActionCow> for BulkExitOpt {
type Error = anyhow::Error;
fn try_from(mut a: ActionCow) -> Result<Self, Self::Error> {
let Ok(target) = a.take_first::<UrlCow>() else {
bail!("invalid target in BulkExitOpt");
};
Ok(Self { target, accept: a.bool("accept") })
}
}
impl FromLua for BulkExitOpt {
fn from_lua(_: Value, _: &Lua) -> mlua::Result<Self> { Err("unsupported".into_lua_err()) }
}
impl IntoLua for BulkExitOpt {
fn into_lua(self, _: &Lua) -> mlua::Result<Value> { Err("unsupported".into_lua_err()) }
}

View file

@ -1,4 +1,5 @@
yazi_macro::mod_flat!(
bulk_exit
cd
close
copy