feat: bulk rename (#50)

This commit is contained in:
Collide 2023-08-14 18:11:45 +08:00 committed by GitHub
parent e7672b1f06
commit ebb06789fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 154 additions and 26 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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)?) }