diff --git a/yazi-core/src/mgr/commands/bulk_rename.rs b/yazi-core/src/mgr/commands/bulk_rename.rs index 78752660..1dd45034 100644 --- a/yazi-core/src/mgr/commands/bulk_rename.rs +++ b/yazi-core/src/mgr/commands/bulk_rename.rs @@ -1,20 +1,26 @@ -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 tokio::{ + fs::{self, OpenOptions}, + io::AsyncWriteExt, +}; +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) { 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"); }; @@ -37,21 +43,25 @@ impl Mgr { .await?; defer! { tokio::spawn(fs::remove_file(tmp.clone())); } - TasksProxy::process_exec(Cow::Borrowed(opener), cwd, vec![ - OsString::new(), - tmp.to_owned().into(), - ]) + TasksProxy::process_exec( + Cow::Borrowed(opener), + cwd, + vec![OsString::new(), tmp.to_owned().into()], + ) .await; - let _permit = HIDER.acquire().await.unwrap(); - 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, new: Vec) -> Result<()> { + async fn bulk_rename_do( + root: PathBuf, + old: Vec, + new: Vec, + sched: Arc, + ) -> Result<()> { if old.len() != new.len() { AppProxy::notify_error( "Bulk rename", @@ -74,46 +84,10 @@ impl Mgr { 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(()) } diff --git a/yazi-core/src/mgr/commands/rename.rs b/yazi-core/src/mgr/commands/rename.rs index bf6e809d..5bf736fe 100644 --- a/yazi-core/src/mgr/commands/rename.rs +++ b/yazi-core/src/mgr/commands/rename.rs @@ -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 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 }; diff --git a/yazi-core/src/tasks/tasks.rs b/yazi-core/src/tasks/tasks.rs index 11a906d2..8138b0e9 100644 --- a/yazi-core/src/tasks/tasks.rs +++ b/yazi-core/src/tasks/tasks.rs @@ -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, + pub scheduler: Arc, handle: JoinHandle<()>, pub visible: bool, diff --git a/yazi-fm/src/executor.rs b/yazi-fm/src/executor.rs index 98119342..3714e6ab 100644 --- a/yazi-fm/src/executor.rs +++ b/yazi-fm/src/executor.rs @@ -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); diff --git a/yazi-scheduler/src/file/file.rs b/yazi-scheduler/src/file/file.rs index 358998c9..a08bb61b 100644 --- a/yazi-scheduler/src/file/file.rs +++ b/yazi-scheduler/src/file/file.rs @@ -4,10 +4,15 @@ 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 +151,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 +350,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 { let meta = fs::symlink_metadata(path).await?; diff --git a/yazi-scheduler/src/file/op.rs b/yazi-scheduler/src/file/op.rs index 0d87fde8..a6e165a1 100644 --- a/yazi-scheduler/src/file/op.rs +++ b/yazi-scheduler/src/file/op.rs @@ -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, +} diff --git a/yazi-scheduler/src/scheduler.rs b/yazi-scheduler/src/scheduler.rs index 9b275456..d1be3e2a 100644 --- a/yazi-scheduler/src/scheduler.rs +++ b/yazi-scheduler/src/scheduler.rs @@ -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::{file::{File, FileOpDelete, FileOpHardlink, FileOpLink, FileOpPaste, FileOpRename, FileOpTrash}, plugin::{Plugin, PluginOpEntry}, prework::{Prework, PreworkOpFetch, PreworkOpLoad, PreworkOpSize}, process::{Process, ProcessOpBg, ProcessOpBlock, ProcessOpOrphan}, TaskKind, TaskOp, HIGH, LOW, NORMAL}; pub struct Scheduler { pub file: Arc, @@ -209,6 +209,18 @@ impl Scheduler { }) } + pub fn file_rename_at(&self, root: &Path, old: &Path, new: &Path) { + let id = self.ongoing.lock().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()); + + 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));