This commit is contained in:
Collide 2025-05-20 15:42:48 +08:00 committed by GitHub
commit 8a1429f641
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 154 additions and 91 deletions

View file

@ -211,6 +211,11 @@ quit_content = "The following tasks are still running, are you sure you want to
quit_origin = "center"
quit_offset = [ 0, 0, 50, 15 ]
# bulk rename
bulk_rename_title = "Rename {n} file{s}?"
bulk_rename_origin = "center"
bulk_rename_offset = [0, 0, 70, 30]
[pick]
open_title = "Open with:"
open_origin = "hovered"

View file

@ -26,6 +26,10 @@ pub struct Confirm {
pub quit_content: String,
pub quit_origin: Origin,
pub quit_offset: Offset,
// bulk rename
pub bulk_rename_title: String,
pub bulk_rename_origin: Origin,
pub bulk_rename_offset: Offset,
}
impl Confirm {

View file

@ -1,3 +1,5 @@
use std::path::PathBuf;
use ratatui::{text::{Line, Text}, widgets::{Paragraph, Wrap}};
use yazi_shared::url::Url;
@ -154,6 +156,19 @@ impl ConfirmCfg {
)
}
pub fn bulk_rename(todo: &[(PathBuf, PathBuf)]) -> Self {
let count = todo.len();
Self::new(
Self::replace_number(&YAZI.confirm.bulk_rename_title, count),
(YAZI.confirm.bulk_rename_origin, YAZI.confirm.bulk_rename_offset),
None,
Some(Text::from_iter(
todo.iter().map(|(old, new)| format!("{} -> {}", old.display(), new.display())),
)),
)
}
fn replace_number(tpl: &str, n: usize) -> String {
tpl.replace("{n}", &n.to_string()).replace("{s}", if n > 1 { "s" } else { "" })
}

View file

@ -1,20 +1,17 @@
use std::{borrow::Cow, collections::HashMap, ffi::{OsStr, OsString}, io::{Read, Write}, path::PathBuf};
use std::{borrow::Cow, collections::HashMap, ffi::{OsStr, OsString}, path::PathBuf, sync::Arc};
use anyhow::{Result, anyhow};
use crossterm::{execute, style::Print};
use anyhow::Result;
use scopeguard::defer;
use tokio::{fs::{self, OpenOptions}, io::AsyncWriteExt};
use yazi_config::YAZI;
use yazi_dds::Pubsub;
use yazi_fs::{File, FilesOp, max_common_root, maybe_exists, paths_to_same_file};
use yazi_proxy::{AppProxy, HIDER, TasksProxy, WATCHER};
use yazi_shared::{terminal_clear, url::Url};
use yazi_term::tty::TTY;
use yazi_config::{YAZI, popup::ConfirmCfg};
use yazi_fs::max_common_root;
use yazi_proxy::{AppProxy, ConfirmProxy, TasksProxy};
use yazi_scheduler::Scheduler;
use crate::mgr::Mgr;
impl Mgr {
pub(super) fn bulk_rename(&self) {
pub(super) fn bulk_rename(&self, sched: Arc<Scheduler>) {
let Some(opener) = YAZI.opener.block(YAZI.open.all("bulk-rename.txt", "text/plain")) else {
return AppProxy::notify_warn("Bulk rename", "No text opener found");
};
@ -43,24 +40,27 @@ impl Mgr {
])
.await;
let _permit = HIDER.acquire().await.unwrap();
defer!(AppProxy::resume());
AppProxy::stop().await;
let new: Vec<_> =
fs::read_to_string(&tmp).await?.lines().take(old.len()).map(PathBuf::from).collect();
Self::bulk_rename_do(root, old, new).await
Self::bulk_rename_do(root, old, new, sched).await
});
}
async fn bulk_rename_do(root: PathBuf, old: Vec<PathBuf>, new: Vec<PathBuf>) -> Result<()> {
terminal_clear(TTY.writer())?;
async fn bulk_rename_do(
root: PathBuf,
old: Vec<PathBuf>,
new: Vec<PathBuf>,
sched: Arc<Scheduler>,
) -> Result<()> {
if old.len() != new.len() {
#[rustfmt::skip]
let s = format!("Number of new and old file names mismatch (New: {}, Old: {}).\nPress <Enter> to exit...", new.len(), old.len());
execute!(TTY.writer(), Print(s))?;
TTY.reader().read_exact(&mut [0])?;
AppProxy::notify_error(
"Bulk rename",
format!(
"Number of new and old file names mismatch (New: {}, Old: {})",
new.len(),
old.len()
),
);
return Ok(());
}
@ -70,61 +70,14 @@ impl Mgr {
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 !ConfirmProxy::show(ConfirmCfg::bulk_rename(&todo)).await {
return Ok(());
}
let permit = WATCHER.acquire().await.unwrap();
let (mut failed, mut succeeded) = (Vec::new(), HashMap::with_capacity(todo.len()));
for (o, n) in todo {
let (old, new) = (root.join(&o), root.join(&n));
if maybe_exists(&new).await && !paths_to_same_file(&old, &new).await {
failed.push((o, n, anyhow!("Destination already exists")));
} else if let Err(e) = fs::rename(&old, &new).await {
failed.push((o, n, e.into()));
} else if let Ok(f) = File::from(new.into()).await {
succeeded.insert(Url::from(old), f);
} else {
failed.push((o, n, anyhow!("Failed to retrieve file info")));
}
for (old, new) in todo {
sched.file_rename_at(&root, &old, &new);
}
if !succeeded.is_empty() {
Pubsub::pub_from_bulk(succeeded.iter().map(|(o, n)| (o, &n.url)).collect());
FilesOp::rename(succeeded);
}
drop(permit);
if !failed.is_empty() {
Self::output_failed(failed).await?;
}
Ok(())
}
async fn output_failed(failed: Vec<(PathBuf, PathBuf, anyhow::Error)>) -> Result<()> {
let mut stdout = TTY.lockout();
terminal_clear(&mut *stdout)?;
writeln!(stdout, "Failed to rename:")?;
for (old, new, err) in failed {
writeln!(stdout, "{} -> {}: {err}", old.display(), new.display())?;
}
writeln!(stdout, "\nPress ENTER to exit")?;
stdout.flush()?;
TTY.reader().read_exact(&mut [0])?;
Ok(())
}

View file

@ -8,7 +8,7 @@ use yazi_fs::{File, FilesOp, maybe_exists, ok_or_not_found, paths_to_same_file,
use yazi_proxy::{ConfirmProxy, InputProxy, TabProxy, WATCHER};
use yazi_shared::{Id, event::CmdCow, url::{Url, UrnBuf}};
use crate::mgr::Mgr;
use crate::{mgr::Mgr, tasks::Tasks};
struct Opt {
hovered: bool,
@ -30,11 +30,11 @@ impl From<CmdCow> for Opt {
impl Mgr {
#[yazi_codegen::command]
pub fn rename(&mut self, opt: Opt) {
pub fn rename(&mut self, opt: Opt, tasks: &Tasks) {
if !self.active_mut().try_escape_visual() {
return;
} else if !opt.hovered && !self.active().selected.is_empty() {
return self.bulk_rename();
return self.bulk_rename(tasks.scheduler.clone());
}
let Some(hovered) = self.hovered() else { return };

View file

@ -10,7 +10,7 @@ use yazi_shared::event::Cmd;
use super::{TASKS_BORDER, TASKS_PADDING, TASKS_PERCENT, TasksProgress};
pub struct Tasks {
pub(super) scheduler: Arc<Scheduler>,
pub scheduler: Arc<Scheduler>,
handle: JoinHandle<()>,
pub visible: bool,

View file

@ -1,9 +1,10 @@
use std::time::Duration;
use std::{collections::HashMap, time::Duration};
use parking_lot::Mutex;
use tokio::{pin, select, sync::mpsc};
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
use tokio_util::sync::CancellationToken;
use yazi_fs::{File, FilesOp};
use yazi_shared::{RoCell, url::Url};
use crate::{Pubsub, body::BodyMoveItem};
@ -12,6 +13,7 @@ static CT: RoCell<CancellationToken> = RoCell::new();
static MOVE_TX: Mutex<Option<mpsc::UnboundedSender<BodyMoveItem>>> = Mutex::new(None);
static TRASH_TX: Mutex<Option<mpsc::UnboundedSender<Url>>> = Mutex::new(None);
static DELETE_TX: Mutex<Option<mpsc::UnboundedSender<Url>>> = Mutex::new(None);
static BULK_RENAME_TX: Mutex<Option<mpsc::UnboundedSender<(Url, File)>>> = Mutex::new(None);
pub struct Pump;
@ -37,15 +39,24 @@ impl Pump {
}
}
#[inline]
pub fn push_bulk_rename_pair(from: Url, to: File) {
if let Some(tx) = &*BULK_RENAME_TX.lock() {
tx.send((from, to)).ok();
}
}
pub(super) fn serve() {
let (move_tx, move_rx) = mpsc::unbounded_channel();
let (trash_tx, trash_rx) = mpsc::unbounded_channel();
let (delete_tx, delete_rx) = mpsc::unbounded_channel();
let (bulk_rename_tx, bulk_rename_rx) = mpsc::unbounded_channel();
CT.with(<_>::default);
MOVE_TX.lock().replace(move_tx);
TRASH_TX.lock().replace(trash_tx);
DELETE_TX.lock().replace(delete_tx);
BULK_RENAME_TX.lock().replace(bulk_rename_tx);
tokio::spawn(async move {
let move_rx =
@ -54,16 +65,23 @@ impl Pump {
UnboundedReceiverStream::new(trash_rx).chunks_timeout(1000, Duration::from_millis(500));
let delete_rx =
UnboundedReceiverStream::new(delete_rx).chunks_timeout(1000, Duration::from_millis(500));
let bulk_rename_rx = UnboundedReceiverStream::new(bulk_rename_rx)
.chunks_timeout(1000, Duration::from_millis(500));
pin!(move_rx);
pin!(trash_rx);
pin!(delete_rx);
pin!(bulk_rename_rx);
loop {
select! {
Some(items) = move_rx.next() => Pubsub::pub_from_move(items),
Some(urls) = trash_rx.next() => Pubsub::pub_from_trash(urls),
Some(urls) = delete_rx.next() => Pubsub::pub_from_delete(urls),
Some(items) = bulk_rename_rx.next() => {
Pubsub::pub_from_bulk(items.iter().map(|(from, to)| (from, &to.url)).collect());
FilesOp::rename(HashMap::from_iter(items));
}
else => {
CT.cancel();
break;
@ -77,6 +95,7 @@ impl Pump {
drop(MOVE_TX.lock().take());
drop(TRASH_TX.lock().take());
drop(DELETE_TX.lock().take());
drop(BULK_RENAME_TX.lock().take());
CT.cancelled().await;
}
}

View file

@ -79,12 +79,11 @@ impl App {
fn routine(push: bool, cursor: Option<(Position, SetCursorStyle)>) {
static COUNT: AtomicU8 = AtomicU8::new(0);
if push && COUNT.fetch_add(1, Ordering::Relaxed) != 0 {
return;
} else if !push && COUNT.fetch_sub(1, Ordering::Relaxed) != 1 {
if (push && COUNT.fetch_add(1, Ordering::Relaxed) != 0)
|| (!push && COUNT.fetch_sub(1, Ordering::Relaxed) != 1)
{
return;
}
_ = if push {
queue!(TTY.writer(), BeginSynchronizedUpdate)
} else if let Some((Position { x, y }, shape)) = cursor {

View file

@ -115,7 +115,7 @@ impl<'a> Executor<'a> {
on!(MGR, remove, &self.app.cx.tasks);
on!(MGR, remove_do, &self.app.cx.tasks);
on!(MGR, create);
on!(MGR, rename);
on!(MGR, rename, &self.app.cx.tasks);
on!(ACTIVE, copy);
on!(ACTIVE, shell);
on!(ACTIVE, hidden);

View file

@ -4,10 +4,10 @@ use anyhow::{Result, anyhow};
use tokio::{fs::{self, DirEntry}, io::{self, ErrorKind::{AlreadyExists, NotFound}}, sync::mpsc};
use tracing::warn;
use yazi_config::YAZI;
use yazi_fs::{SizeCalculator, cha::Cha, copy_with_progress, maybe_exists, ok_or_not_found, path_relative_to, skip_path};
use yazi_fs::{SizeCalculator, cha::Cha, copy_with_progress, maybe_exists, ok_or_not_found, path_relative_to, paths_to_same_file, skip_path};
use yazi_shared::url::Url;
use super::{FileOp, FileOpDelete, FileOpHardlink, FileOpLink, FileOpPaste, FileOpTrash};
use super::{FileOp, FileOpDelete, FileOpHardlink, FileOpLink, FileOpPaste, FileOpRename, FileOpTrash};
use crate::{LOW, NORMAL, TaskOp, TaskProg};
pub struct File {
@ -146,6 +146,26 @@ impl File {
.await??;
self.prog.send(TaskProg::Adv(task.id, 1, task.length))?;
}
FileOp::Rename(task) => {
if maybe_exists(&task.to).await && !paths_to_same_file(&task.from, &task.to).await {
let e = anyhow!("Destination already exists");
self.fail(task.id, format!("An error occurred while renaming: {e:?}"))?;
return Err(e);
}
if let Err(e) = fs::rename(&task.from, &task.to).await {
self.fail(task.id, format!("An error occurred while renaming: {e}"))?;
return Err(e.into());
}
if yazi_fs::File::from(task.to).await.is_err() {
let e = anyhow!("Failed to retrieve file info");
self.fail(task.id, format!("An error occurred while renaming: {e:?}"))?;
return Err(e);
}
self.prog.send(TaskProg::Adv(task.id, 1, 0))?;
}
}
Ok(())
}
@ -325,6 +345,14 @@ impl File {
self.succ(id)
}
pub async fn rename(&self, task: FileOpRename) -> Result<()> {
let id = task.id;
self.prog.send(TaskProg::New(id, 0))?;
self.queue(FileOp::Rename(task), NORMAL).await?;
self.succ(id)
}
#[inline]
async fn cha(path: &Path, follow: bool) -> io::Result<Cha> {
let meta = fs::symlink_metadata(path).await?;

View file

@ -8,6 +8,7 @@ pub enum FileOp {
Hardlink(FileOpHardlink),
Delete(FileOpDelete),
Trash(FileOpTrash),
Rename(FileOpRename),
}
impl FileOp {
@ -18,6 +19,7 @@ impl FileOp {
Self::Hardlink(op) => op.id,
Self::Delete(op) => op.id,
Self::Trash(op) => op.id,
Self::Rename(op) => op.id,
}
}
}
@ -105,3 +107,10 @@ pub struct FileOpTrash {
pub target: Url,
pub length: u64,
}
// --- Rename
#[derive(Clone, Debug)]
pub struct FileOpRename {
pub id: usize,
pub from: Url,
pub to: Url,
}

View file

@ -1,4 +1,4 @@
use std::{ffi::OsString, future::Future, sync::Arc, time::Duration};
use std::{ffi::OsString, future::Future, path::Path, sync::Arc, time::Duration};
use anyhow::Result;
use futures::{FutureExt, future::BoxFuture};
@ -11,7 +11,7 @@ use yazi_proxy::{MgrProxy, options::{PluginOpt, ProcessExecOpt}};
use yazi_shared::{Throttle, url::Url};
use super::{Ongoing, TaskProg, TaskStage};
use crate::{HIGH, LOW, NORMAL, TaskKind, TaskOp, file::{File, FileOpDelete, FileOpHardlink, FileOpLink, FileOpPaste, FileOpTrash}, plugin::{Plugin, PluginOpEntry}, prework::{Prework, PreworkOpFetch, PreworkOpLoad, PreworkOpSize}, process::{Process, ProcessOpBg, ProcessOpBlock, ProcessOpOrphan}};
use crate::{HIGH, LOW, NORMAL, TaskKind, TaskOp, file::{File, FileOpDelete, FileOpHardlink, FileOpLink, FileOpPaste, FileOpRename, FileOpTrash}, plugin::{Plugin, PluginOpEntry}, prework::{Prework, PreworkOpFetch, PreworkOpLoad, PreworkOpSize}, process::{Process, ProcessOpBg, ProcessOpBlock, ProcessOpOrphan}};
pub struct Scheduler {
pub file: Arc<File>,
@ -209,6 +209,37 @@ impl Scheduler {
})
}
pub fn file_rename_at(&self, root: &Path, old: &Path, new: &Path) {
let mut ongoing = self.ongoing.lock();
let id = ongoing.add(
TaskKind::User,
format!("Rename at {}: {} -> {} ", root.display(), old.display(), new.display()),
);
let (from, to): (Url, Url) = (root.join(old).into(), root.join(new).into());
ongoing.hooks.insert(id, {
let from = from.clone();
let to = to.clone();
let ongoing = self.ongoing.clone();
Box::new(move |canceled: bool| {
async move {
if !canceled {
if let Ok(to) = yazi_fs::File::from(to).await {
Pump::push_bulk_rename_pair(from, to);
}
}
ongoing.lock().try_remove(id, TaskStage::Hooked);
}
.boxed()
})
});
let file = self.file.clone();
self.send_micro(id, LOW, async move { file.rename(FileOpRename { id, from, to }).await });
}
pub fn plugin_micro(&self, opt: PluginOpt) {
let id = self.ongoing.lock().add(TaskKind::User, format!("Run micro plugin `{}`", opt.id));