From 08590c6d9e928aaaad454d064364d6e696e4d7f7 Mon Sep 17 00:00:00 2001 From: sxyazi Date: Sun, 13 Aug 2023 22:18:08 +0800 Subject: [PATCH] .. --- Cargo.lock | 37 --------- core/src/external/editor.rs | 46 ----------- core/src/manager/manager.rs | 160 +++++++++++++++++++++--------------- core/src/tasks/tasks.rs | 2 +- shared/src/defer.rs | 8 +- shared/src/fs.rs | 13 ++- shared/src/lib.rs | 2 - shared/src/temp_path.rs | 36 -------- shared/src/term.rs | 11 ++- 9 files changed, 121 insertions(+), 194 deletions(-) delete mode 100644 core/src/external/editor.rs delete mode 100644 shared/src/temp_path.rs diff --git a/Cargo.lock b/Cargo.lock index 0bfc965a..2bc18dec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1315,12 +1315,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - [[package]] name = "proc-macro2" version = "1.0.66" @@ -1357,36 +1351,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - [[package]] name = "ratatui" version = "0.22.0" @@ -1565,7 +1529,6 @@ dependencies = [ "crossterm 0.27.0", "libc", "parking_lot", - "rand", "ratatui", "tokio", ] diff --git a/core/src/external/editor.rs b/core/src/external/editor.rs deleted file mode 100644 index 3b22fc0e..00000000 --- a/core/src/external/editor.rs +++ /dev/null @@ -1,46 +0,0 @@ -use std::{env, ffi::{OsStr, OsString}, path::Path, str::FromStr}; - -use anyhow::{Context, Result}; -use once_cell::sync::OnceCell; -use tokio::process::Command; - -#[derive(Debug)] -struct Editor { - name: String, - options: Vec, -} - -static EDITOR: OnceCell = OnceCell::new(); - -fn try_init() -> Result<&'static Editor> { - EDITOR.get_or_try_init(|| { - env::var_os("EDITOR") - .context("environment variable `EDITOR` is undefined")? - .to_string_lossy() - .parse() - }) -} - -pub async fn edit(file: impl AsRef) -> Result<()> { - let editor = try_init()?; - // TODO: 编辑并返回是否成功 - // let output = Command::new(&editor.name) - // .args(&editor.options) - // .arg(file.as_ref()) - // .kill_on_drop(true) - // .output() - // .await?; - todo!() -} - -impl FromStr for Editor { - type Err = anyhow::Error; - - fn from_str(s: &str) -> std::result::Result { - let mut args = s.split(' '); - Ok(Self { - name: args.next().context("environment variable `EDITOR` is empty")?.to_owned(), - options: args.map(str::to_owned).collect(), - }) - } -} diff --git a/core/src/manager/manager.rs b/core/src/manager/manager.rs index 09030412..f495497d 100644 --- a/core/src/manager/manager.rs +++ b/core/src/manager/manager.rs @@ -1,11 +1,10 @@ -use std::{collections::{BTreeMap, BTreeSet, HashMap, HashSet}, env, mem, path::{Path, PathBuf}, process::exit}; +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 indexmap::IndexSet; -use shared::{temp_path, MIME_DIR}; -use tokio::{fs::{self, OpenOptions}, io::{AsyncReadExt, AsyncWriteExt, BufReader, BufWriter}}; -use tracing::{debug, error}; +use anyhow::{bail, Error, Result}; +use config::{open::Opener, BOOT, OPEN}; +use crossterm::{execute, terminal::{Clear, ClearType}}; +use shared::{in_same_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}; @@ -225,56 +224,103 @@ impl Manager { } pub fn bulk_rename(&self) -> bool { - let selected: Vec<_> = self.selected().iter().map(|&f| f.path.clone()).collect(); + let mut old: Vec<_> = self.selected().iter().map(|&f| f.path()).collect(); + if old.is_empty() { + return false; + } + let root = in_same_root(&old); + if let Some(ref root) = root { + old = old.into_iter().map(|p| p.strip_prefix(root).unwrap().to_owned()).collect(); + } + + let tmp = BOOT.tmpfile(); tokio::spawn(async move { - let files: Vec<_> = - selected.iter().map(|p| p.file_name().unwrap().to_string_lossy()).collect(); - let rename_file_path = temp_path(Some("txt")); - - { - let mut rename_file = - match OpenOptions::new().write(true).create_new(true).open(&rename_file_path).await { - Ok(f) => BufWriter::new(f), - Err(e) => { - error!("failed to open rename buffer: {e}"); - return; - } - }; - - if let Err(e) = rename_file.write_all(files.join("\n").as_bytes()).await { - error!("failed to write content to rename buffer: {e}"); - return; - } - let _ = rename_file.flush().await; - } - - emit!(Open(vec![(rename_file_path.as_os_str().to_owned(), "text/plain".to_owned())], None)); - - let mut buf = String::new(); - { - let mut rename_file = match tokio::fs::File::open(&rename_file_path).await { - Ok(f) => BufReader::new(f), - Err(e) => { - error!("failed to read rename buffer: {e}"); - return; - } - }; - let _ = rename_file.read_to_string(&mut buf).await; - } - let new_names = match parse_new_names(&buf, selected.len()) { - Ok(names) => names, - Err(e) => { - println!("yazi: {e}"); - return; - } + let Some(opener) = OPEN.block_opener("bulk-rename.txt", "text/plain") else { + bail!("No opener for bulk rename"); }; - println!("new names: {new_names:?}"); + + { + let b = old.iter().map(|o| o.as_os_str()).collect::>().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()); + 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: Option, + old: Vec, + new: Vec, + ) -> 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(if let Some(ref root) = root { (root.join(o), root.join(n)) } else { (o, 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(); tokio::spawn(async move { @@ -437,19 +483,3 @@ impl Manager { self.active().mode.is_visual() || self.current().has_selected() } } - -fn parse_new_names(text: &str, count: usize) -> anyhow::Result> { - // NOTE: call `size_hint` on `str::Split` always returns 0 - let new_names: Vec<_> = text.split('\n').collect(); - if new_names.len() != count { - anyhow::bail!("the number of new names doesn't match the number of old names"); - } - - let mut names = IndexSet::with_capacity(count); - for name in new_names { - if !names.insert(name) { - anyhow::bail!("there are more than one new entries named {name:?}"); - } - } - Ok(names) -} diff --git a/core/src/tasks/tasks.rs b/core/src/tasks/tasks.rs index 6e3ac6b5..836e13c4 100644 --- a/core/src/tasks/tasks.rs +++ b/core/src/tasks/tasks.rs @@ -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(); diff --git a/shared/src/defer.rs b/shared/src/defer.rs index 3c054bde..d247eef1 100644 --- a/shared/src/defer.rs +++ b/shared/src/defer.rs @@ -1,13 +1,13 @@ -pub struct Defer(Option); +pub struct Defer T, T>(Option); -impl Defer { +impl T, T> Defer { pub fn new(f: F) -> Self { Defer(Some(f)) } } -impl Drop for Defer { +impl T, T> Drop for Defer { fn drop(&mut self) { if let Some(f) = self.0.take() { - f(); + let _ = f(); } } } diff --git a/shared/src/fs.rs b/shared/src/fs.rs index 75f9ce24..3e2e8793 100644 --- a/shared/src/fs.rs +++ b/shared/src/fs.rs @@ -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,14 @@ pub fn file_mode(mode: u32) -> String { s } + +// Note: files must contain at least one file path +pub fn in_same_root(files: &[PathBuf]) -> Option { + if files.is_empty() { + return None; + } + + let mut files = files.iter(); + let parent = files.next().map(|f| f.parent().unwrap_or(f).to_path_buf()).unwrap(); + if files.all(|f| f.parent().unwrap_or(f) == parent) { Some(parent) } else { None } +} diff --git a/shared/src/lib.rs b/shared/src/lib.rs index d0043c72..b1d7811b 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -4,7 +4,6 @@ mod defer; mod fns; mod fs; mod mime; -mod temp_path; mod term; mod throttle; mod tty; @@ -15,7 +14,6 @@ pub use defer::*; pub use fns::*; pub use fs::*; pub use mime::*; -pub use temp_path::*; pub use term::*; pub use throttle::*; pub use tty::*; diff --git a/shared/src/temp_path.rs b/shared/src/temp_path.rs deleted file mode 100644 index ae70f516..00000000 --- a/shared/src/temp_path.rs +++ /dev/null @@ -1,36 +0,0 @@ -use std::path::PathBuf; - -use rand::{distributions::Alphanumeric, Rng}; - -const FILE_PREFIX: &str = "yazi-"; -const ID_LEN: usize = 10; - -/// generate a path under the system temporary directory -/// -/// `ext`: an ASCII string -pub fn temp_path(ext: Option<&str>) -> PathBuf { - let (ext, suffix_len) = ext.map(|ext| (ext, ext.len())).unwrap_or_default(); - - let mut name = String::with_capacity(FILE_PREFIX.len() + ID_LEN + suffix_len); - name.push_str(FILE_PREFIX); - rand::thread_rng().sample_iter(&Alphanumeric).take(ID_LEN).for_each(|c| name.push(c as char)); - - if !ext.is_empty() { - name.push('.'); - name.push_str(ext); - } - - let tmp = std::env::temp_dir(); - tmp.join(&name) -} - -#[cfg(test)] -mod tests { - use super::temp_path; - - #[test] - fn test_temp_path() { - let p = temp_path(Some("txt")); - println!("{p:?}"); - } -} diff --git a/shared/src/term.rs b/shared/src/term.rs index c140627b..01de6dde 100644 --- a/shared/src/term.rs +++ b/shared/src/term.rs @@ -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)?) }