Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions yazi-config/preset/yazi-default.toml
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,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"
Expand Down
12 changes: 8 additions & 4 deletions yazi-config/src/popup/confirm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,14 @@ pub struct Confirm {
pub overwrite_offset: Offset,

// quit
pub quit_title: String,
pub quit_content: String,
pub quit_origin: Origin,
pub quit_offset: Offset,
pub quit_title: String,
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 {
Expand Down
15 changes: 15 additions & 0 deletions yazi-config/src/popup/options.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::path::PathBuf;

use ratatui::{text::{Line, Text}, widgets::{Paragraph, Wrap}};
use yazi_shared::url::Url;

Expand Down Expand Up @@ -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 { "" })
}
Expand Down
97 changes: 25 additions & 72 deletions yazi-core/src/mgr/commands/bulk_rename.rs
Original file line number Diff line number Diff line change
@@ -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");
};
Expand Down Expand Up @@ -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(());
}

Expand All @@ -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")));
}
}

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())?;
for (old, new) in todo {
sched.file_rename_at(&root, &old, &new);
}
writeln!(stdout, "\nPress ENTER to exit")?;

stdout.flush()?;
TTY.reader().read_exact(&mut [0])?;
Ok(())
}

Expand Down
6 changes: 3 additions & 3 deletions yazi-core/src/mgr/commands/rename.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 };
Expand Down
4 changes: 2 additions & 2 deletions yazi-core/src/tasks/tasks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ use yazi_shared::event::Cmd;
use super::{TASKS_BORDER, TASKS_PADDING, TASKS_PERCENT, TasksProgress};

pub struct Tasks {
pub(super) scheduler: Arc<Scheduler>,
handle: JoinHandle<()>,
pub scheduler: Arc<Scheduler>,
handle: JoinHandle<()>,

pub visible: bool,
pub cursor: usize,
Expand Down
21 changes: 20 additions & 1 deletion yazi-dds/src/pump.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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;

Expand All @@ -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 =
Expand All @@ -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;
Expand All @@ -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;
}
}
7 changes: 3 additions & 4 deletions yazi-fm/src/app/commands/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion yazi-fm/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
32 changes: 30 additions & 2 deletions yazi-scheduler/src/file/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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(())
}
Expand Down Expand Up @@ -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?;
Expand Down
Loading
Loading