diff --git a/Cargo.lock b/Cargo.lock index 2bc18dec..0bfc965a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1315,6 +1315,12 @@ 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" @@ -1351,6 +1357,36 @@ 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" @@ -1529,6 +1565,7 @@ 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 new file mode 100644 index 00000000..3b22fc0e --- /dev/null +++ b/core/src/external/editor.rs @@ -0,0 +1,46 @@ +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/folder.rs b/core/src/manager/folder.rs index 177da4fd..15280969 100644 --- a/core/src/manager/folder.rs +++ b/core/src/manager/folder.rs @@ -115,12 +115,11 @@ impl Folder { pub fn select(&mut self, idx: Option, state: Option) -> bool { let len = self.files.len(); let mut apply = |idx: usize, state: Option| -> 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; diff --git a/core/src/manager/manager.rs b/core/src/manager/manager.rs index a073b461..09030412 100644 --- a/core/src/manager/manager.rs +++ b/core/src/manager/manager.rs @@ -1,9 +1,11 @@ -use std::{collections::{BTreeMap, BTreeSet, HashMap, HashSet}, env, mem, path::{Path, PathBuf}}; +use std::{collections::{BTreeMap, BTreeSet, HashMap, HashSet}, env, mem, path::{Path, PathBuf}, process::exit}; use anyhow::Error; use config::{open::Opener, OPEN}; -use shared::MIME_DIR; -use tokio::fs; +use indexmap::IndexSet; +use shared::{temp_path, MIME_DIR}; +use tokio::{fs::{self, OpenOptions}, io::{AsyncReadExt, AsyncWriteExt, BufReader, BufWriter}}; +use tracing::{debug, error}; use super::{PreviewData, Tab, Tabs, Watcher}; use crate::{emit, external, files::{File, FilesOp}, input::InputOpt, manager::Folder, select::SelectOpt, tasks::Tasks}; @@ -222,7 +224,56 @@ impl Manager { false } - pub fn bulk_rename(&self) -> bool { false } + pub fn bulk_rename(&self) -> bool { + let selected: Vec<_> = self.selected().iter().map(|&f| f.path.clone()).collect(); + + 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; + } + }; + println!("new names: {new_names:?}"); + }); + + false + } pub fn shell(&self, exec: &str, block: bool, confirm: bool) -> bool { let mut exec = exec.to_owned(); @@ -386,3 +437,19 @@ 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/shared/src/lib.rs b/shared/src/lib.rs index b1d7811b..d0043c72 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -4,6 +4,7 @@ mod defer; mod fns; mod fs; mod mime; +mod temp_path; mod term; mod throttle; mod tty; @@ -14,6 +15,7 @@ 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 new file mode 100644 index 00000000..ae70f516 --- /dev/null +++ b/shared/src/temp_path.rs @@ -0,0 +1,36 @@ +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:?}"); + } +}