mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-21 23:01:05 +00:00
feat: bulk rename (#50)
This commit is contained in:
parent
e7672b1f06
commit
ebb06789fc
8 changed files with 154 additions and 26 deletions
|
|
@ -143,12 +143,7 @@ it will automatically use the "Window system protocol" to display images -- this
|
|||
|
||||
## TODO
|
||||
|
||||
- [x] Add example config for general usage, currently please see my [another repo](https://github.com/sxyazi/dotfiles/tree/main/yazi) instead
|
||||
- [x] Integration with fzf, zoxide for fast directory navigation
|
||||
- [x] Integration with fd, rg for fuzzy file searching
|
||||
- [x] Documentation of commands and options
|
||||
- [x] Support for Überzug++ for image previews with X11/wayland environment
|
||||
- [ ] Batch renaming support
|
||||
See [Feature requests](https://github.com/sxyazi/yazi/issues/51) for more details.
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
|
|
@ -48,8 +48,8 @@ impl Boot {
|
|||
}
|
||||
|
||||
#[inline]
|
||||
pub fn tmpfile(&self) -> PathBuf {
|
||||
pub fn tmpfile(&self, prefix: &str) -> PathBuf {
|
||||
let nanos = SystemTime::now().duration_since(time::UNIX_EPOCH).unwrap().as_nanos();
|
||||
self.cache_dir.join(format!("{:x}", Md5::new_with_prefix(nanos.to_le_bytes()).finalize()))
|
||||
self.cache_dir.join(format!("{prefix}-{}", nanos / 1000))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,12 +115,11 @@ impl Folder {
|
|||
pub fn select(&mut self, idx: Option<usize>, state: Option<bool>) -> bool {
|
||||
let len = self.files.len();
|
||||
let mut apply = |idx: usize, state: Option<bool>| -> bool {
|
||||
if state.is_none() {
|
||||
let Some(state) = state else {
|
||||
self.files[idx].is_selected = !self.files[idx].is_selected;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
let state = state.unwrap();
|
||||
if state != self.files[idx].is_selected {
|
||||
self.files[idx].is_selected = state;
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
use std::{collections::{BTreeMap, BTreeSet, HashMap, HashSet}, env, mem, path::{Path, PathBuf}};
|
||||
use std::{collections::{BTreeMap, BTreeSet, HashMap, HashSet}, env, ffi::OsStr, io::{stdout, Write}, mem, os::unix::prelude::OsStrExt, path::{Path, PathBuf}};
|
||||
|
||||
use anyhow::Error;
|
||||
use config::{open::Opener, OPEN};
|
||||
use shared::MIME_DIR;
|
||||
use tokio::fs;
|
||||
use anyhow::{bail, Error, Result};
|
||||
use config::{open::Opener, BOOT, OPEN};
|
||||
use shared::{max_common_root, Defer, Term, MIME_DIR};
|
||||
use tokio::{fs::{self, OpenOptions}, io::{AsyncReadExt, AsyncWriteExt}};
|
||||
|
||||
use super::{PreviewData, Tab, Tabs, Watcher};
|
||||
use crate::{emit, external, files::{File, FilesOp}, input::InputOpt, manager::Folder, select::SelectOpt, tasks::Tasks};
|
||||
use crate::{emit, external::{self, ShellOpt}, files::{File, FilesOp}, input::InputOpt, manager::Folder, select::SelectOpt, tasks::Tasks, Event, BLOCKER};
|
||||
|
||||
pub struct Manager {
|
||||
tabs: Tabs,
|
||||
|
|
@ -222,7 +222,97 @@ impl Manager {
|
|||
false
|
||||
}
|
||||
|
||||
pub fn bulk_rename(&self) -> bool { false }
|
||||
pub fn bulk_rename(&self) -> bool {
|
||||
let mut old: Vec<_> = self.selected().iter().map(|&f| f.path()).collect();
|
||||
|
||||
let root = max_common_root(&old);
|
||||
old = old.into_iter().map(|p| p.strip_prefix(&root).unwrap().to_owned()).collect();
|
||||
|
||||
let tmp = BOOT.tmpfile("bulk");
|
||||
tokio::spawn(async move {
|
||||
let Some(opener) = OPEN.block_opener("bulk.txt", "text/plain") else {
|
||||
bail!("No opener for bulk rename");
|
||||
};
|
||||
|
||||
{
|
||||
let b = old.iter().map(|o| o.as_os_str()).collect::<Vec<_>>().join(OsStr::new("\n"));
|
||||
let mut f = OpenOptions::new().write(true).create_new(true).open(&tmp).await?;
|
||||
f.write_all(b.as_bytes()).await?;
|
||||
}
|
||||
|
||||
let _guard = BLOCKER.acquire().await.unwrap();
|
||||
let _defer = Defer::new(|| {
|
||||
Event::Stop(false, None).emit();
|
||||
tokio::spawn(fs::remove_file(tmp.clone()))
|
||||
});
|
||||
emit!(Stop(true)).await;
|
||||
|
||||
let mut child = external::shell(ShellOpt {
|
||||
cmd: (*opener.exec).into(),
|
||||
args: vec![tmp.to_owned().into()],
|
||||
piped: false,
|
||||
})?;
|
||||
child.wait().await?;
|
||||
|
||||
let new: Vec<_> = fs::read_to_string(&tmp).await?.lines().map(|l| l.into()).collect();
|
||||
Self::bulk_rename_do(root, old, new).await
|
||||
});
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
async fn bulk_rename_do(root: PathBuf, old: Vec<PathBuf>, new: Vec<PathBuf>) -> Result<()> {
|
||||
Term::clear()?;
|
||||
if old.len() != new.len() {
|
||||
println!("Number of old and new differ, press ENTER to exit");
|
||||
tokio::io::stdin().read_exact(&mut [0]).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut todo = Vec::with_capacity(old.len());
|
||||
for (o, n) in old.into_iter().zip(new) {
|
||||
if n != o {
|
||||
stdout().write_all(o.as_os_str().as_bytes())?;
|
||||
stdout().write_all(b" -> ")?;
|
||||
stdout().write_all(n.as_os_str().as_bytes())?;
|
||||
stdout().write_all(b"\n")?;
|
||||
todo.push((root.join(o), root.join(n)));
|
||||
}
|
||||
}
|
||||
if todo.is_empty() {
|
||||
return Ok(());
|
||||
} else {
|
||||
print!("Continue to rename? (y/N): ");
|
||||
stdout().flush()?;
|
||||
}
|
||||
|
||||
let mut buf = [0];
|
||||
tokio::io::stdin().read_exact(&mut buf).await?;
|
||||
if buf[0] != b'y' && buf[0] != b'Y' {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut failed = Vec::new();
|
||||
for (o, n) in todo {
|
||||
if let Err(e) = fs::rename(&o, &n).await {
|
||||
failed.push((o, n, e));
|
||||
}
|
||||
}
|
||||
|
||||
if !failed.is_empty() {
|
||||
Term::clear()?;
|
||||
println!("Failed to rename:");
|
||||
for (o, n, e) in failed {
|
||||
stdout().write_all(o.as_os_str().as_bytes())?;
|
||||
stdout().write_all(b" -> ")?;
|
||||
stdout().write_all(n.as_os_str().as_bytes())?;
|
||||
stdout().write_fmt(format_args!(": {e}\n"))?;
|
||||
}
|
||||
println!("\nPress ENTER to exit");
|
||||
tokio::io::stdin().read_exact(&mut [0]).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn shell(&self, exec: &str, block: bool, confirm: bool) -> bool {
|
||||
let mut exec = exec.to_owned();
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ impl Tasks {
|
|||
emit!(Stop(true)).await;
|
||||
let _defer = Defer::new(|| {
|
||||
disable_raw_mode().ok();
|
||||
Event::Stop(false, None).emit()
|
||||
Event::Stop(false, None).emit();
|
||||
});
|
||||
|
||||
stdout().write_all("\n".repeat(tty_size().ws_row as usize).as_bytes()).ok();
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
pub struct Defer<F: FnOnce()>(Option<F>);
|
||||
pub struct Defer<F: FnOnce() -> T, T>(Option<F>);
|
||||
|
||||
impl<F: FnOnce()> Defer<F> {
|
||||
impl<F: FnOnce() -> T, T> Defer<F, T> {
|
||||
pub fn new(f: F) -> Self { Defer(Some(f)) }
|
||||
}
|
||||
|
||||
impl<F: FnOnce()> Drop for Defer<F> {
|
||||
impl<F: FnOnce() -> T, T> Drop for Defer<F, T> {
|
||||
fn drop(&mut self) {
|
||||
if let Some(f) = self.0.take() {
|
||||
f();
|
||||
let _ = f();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use std::{collections::VecDeque, path::Path};
|
||||
use std::{collections::VecDeque, path::{Path, PathBuf}};
|
||||
|
||||
use anyhow::Result;
|
||||
use tokio::{fs, io, select, sync::{mpsc, oneshot}, time};
|
||||
|
|
@ -149,3 +149,40 @@ pub fn file_mode(mode: u32) -> String {
|
|||
|
||||
s
|
||||
}
|
||||
|
||||
// Find the max common root of a list of files
|
||||
// e.g. /a/b/c, /a/b/d -> /a/b
|
||||
// /aa/bb/cc, /aa/dd/ee -> /aa
|
||||
pub fn max_common_root(files: &[PathBuf]) -> PathBuf {
|
||||
if files.is_empty() {
|
||||
return PathBuf::new();
|
||||
}
|
||||
|
||||
let mut it = files.iter().map(|p| p.components());
|
||||
let mut root = it.next().unwrap().collect::<PathBuf>();
|
||||
for components in it {
|
||||
let mut new_root = PathBuf::new();
|
||||
for (a, b) in root.components().zip(components) {
|
||||
if a != b {
|
||||
break;
|
||||
}
|
||||
new_root.push(a);
|
||||
}
|
||||
root = new_root;
|
||||
}
|
||||
root
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_max_common_root() {
|
||||
assert_eq!(max_common_root(&[]).as_os_str(), "");
|
||||
assert_eq!(max_common_root(&["".into()]).as_os_str(), "");
|
||||
assert_eq!(max_common_root(&["/a/b".into()]).as_os_str(), "/a/b");
|
||||
assert_eq!(max_common_root(&["/a/b/c".into(), "/a/b/d".into()]).as_os_str(), "/a/b");
|
||||
assert_eq!(max_common_root(&["/aa/bb/cc".into(), "/aa/dd/ee".into()]).as_os_str(), "/aa");
|
||||
assert_eq!(
|
||||
max_common_root(&["/aa/bb/cc".into(), "/aa/bb/cc/dd/ee".into(), "/aa/bb/cc/ff".into()])
|
||||
.as_os_str(),
|
||||
"/aa/bb/cc"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use std::{io::{stdout, Stdout}, ops::{Deref, DerefMut}};
|
||||
use std::{io::{stdout, Stdout, Write}, ops::{Deref, DerefMut}};
|
||||
|
||||
use anyhow::Result;
|
||||
use crossterm::{cursor::{MoveTo, SetCursorStyle}, event::{DisableBracketedPaste, DisableFocusChange, EnableBracketedPaste, EnableFocusChange, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags}, execute, queue, terminal::{disable_raw_mode, enable_raw_mode, supports_keyboard_enhancement, EnterAlternateScreen, LeaveAlternateScreen}};
|
||||
use crossterm::{cursor::{MoveTo, SetCursorStyle}, event::{DisableBracketedPaste, DisableFocusChange, EnableBracketedPaste, EnableFocusChange, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags}, execute, queue, terminal::{disable_raw_mode, enable_raw_mode, supports_keyboard_enhancement, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen}};
|
||||
use ratatui::{backend::CrosstermBackend, Terminal};
|
||||
|
||||
pub struct Term {
|
||||
|
|
@ -32,6 +32,13 @@ impl Term {
|
|||
Ok(term)
|
||||
}
|
||||
|
||||
pub fn clear() -> Result<()> {
|
||||
execute!(stdout(), Clear(ClearType::All))?;
|
||||
println!();
|
||||
stdout().flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn move_to(x: u16, y: u16) -> Result<()> { Ok(execute!(stdout(), MoveTo(x, y))?) }
|
||||
|
||||
pub fn set_cursor_block() -> Result<()> { Ok(execute!(stdout(), SetCursorStyle::BlinkingBlock)?) }
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue