mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41: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
|
## TODO
|
||||||
|
|
||||||
- [x] Add example config for general usage, currently please see my [another repo](https://github.com/sxyazi/dotfiles/tree/main/yazi) instead
|
See [Feature requests](https://github.com/sxyazi/yazi/issues/51) for more details.
|
||||||
- [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
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,8 @@ impl Boot {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[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();
|
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 {
|
pub fn select(&mut self, idx: Option<usize>, state: Option<bool>) -> bool {
|
||||||
let len = self.files.len();
|
let len = self.files.len();
|
||||||
let mut apply = |idx: usize, state: Option<bool>| -> bool {
|
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;
|
self.files[idx].is_selected = !self.files[idx].is_selected;
|
||||||
return true;
|
return true;
|
||||||
}
|
};
|
||||||
|
|
||||||
let state = state.unwrap();
|
|
||||||
if state != self.files[idx].is_selected {
|
if state != self.files[idx].is_selected {
|
||||||
self.files[idx].is_selected = state;
|
self.files[idx].is_selected = state;
|
||||||
return true;
|
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 anyhow::{bail, Error, Result};
|
||||||
use config::{open::Opener, OPEN};
|
use config::{open::Opener, BOOT, OPEN};
|
||||||
use shared::MIME_DIR;
|
use shared::{max_common_root, Defer, Term, MIME_DIR};
|
||||||
use tokio::fs;
|
use tokio::{fs::{self, OpenOptions}, io::{AsyncReadExt, AsyncWriteExt}};
|
||||||
|
|
||||||
use super::{PreviewData, Tab, Tabs, Watcher};
|
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 {
|
pub struct Manager {
|
||||||
tabs: Tabs,
|
tabs: Tabs,
|
||||||
|
|
@ -222,7 +222,97 @@ impl Manager {
|
||||||
false
|
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 {
|
pub fn shell(&self, exec: &str, block: bool, confirm: bool) -> bool {
|
||||||
let mut exec = exec.to_owned();
|
let mut exec = exec.to_owned();
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ impl Tasks {
|
||||||
emit!(Stop(true)).await;
|
emit!(Stop(true)).await;
|
||||||
let _defer = Defer::new(|| {
|
let _defer = Defer::new(|| {
|
||||||
disable_raw_mode().ok();
|
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();
|
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)) }
|
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) {
|
fn drop(&mut self) {
|
||||||
if let Some(f) = self.0.take() {
|
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 anyhow::Result;
|
||||||
use tokio::{fs, io, select, sync::{mpsc, oneshot}, time};
|
use tokio::{fs, io, select, sync::{mpsc, oneshot}, time};
|
||||||
|
|
@ -149,3 +149,40 @@ pub fn file_mode(mode: u32) -> String {
|
||||||
|
|
||||||
s
|
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 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};
|
use ratatui::{backend::CrosstermBackend, Terminal};
|
||||||
|
|
||||||
pub struct Term {
|
pub struct Term {
|
||||||
|
|
@ -32,6 +32,13 @@ impl Term {
|
||||||
Ok(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 move_to(x: u16, y: u16) -> Result<()> { Ok(execute!(stdout(), MoveTo(x, y))?) }
|
||||||
|
|
||||||
pub fn set_cursor_block() -> Result<()> { Ok(execute!(stdout(), SetCursorStyle::BlinkingBlock)?) }
|
pub fn set_cursor_block() -> Result<()> { Ok(execute!(stdout(), SetCursorStyle::BlinkingBlock)?) }
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue