diff --git a/Cargo.lock b/Cargo.lock index c48fac4..2e1a5a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -362,6 +362,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "felix" version = "2.15.0" @@ -384,6 +390,7 @@ dependencies = [ "serde_yaml", "simplelog", "tar", + "tempfile", "unicode-width 0.2.0", "walkdir", "zip", @@ -1181,6 +1188,20 @@ dependencies = [ "xattr", ] +[[package]] +name = "tempfile" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" +dependencies = [ + "cfg-if", + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "termcolor" version = "1.4.1" diff --git a/Cargo.toml b/Cargo.toml index 6dd4424..3fc9ce3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ zstd = "0.13.2" unicode-width = "0.2.0" git2 = {version = "0.19.0", default-features = false } normpath = "1.3.0" +tempfile = "3.15.0" [dev-dependencies] bwrap = { version = "1.3.0", features = ["use_std"] } diff --git a/src/errors.rs b/src/errors.rs index fa58e79..13bc2ec 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -#[derive(Debug)] +#[derive(Debug, Default)] pub enum FxError { Arg(String), TerminalSizeDetection, @@ -22,6 +22,8 @@ pub enum FxError { Panic, #[cfg(any(target_os = "linux", target_os = "netbsd"))] Nix(String), + #[default] + Unknown, } impl std::error::Error for FxError {} @@ -51,6 +53,7 @@ impl std::fmt::Display for FxError { FxError::Panic => "Error: felix panicked".to_owned(), #[cfg(any(target_os = "linux", target_os = "netbsd"))] FxError::Nix(s) => s.to_owned(), + FxError::Unknown => "Unknown error.".to_owned(), }; write!(f, "{}", printable) } diff --git a/src/op.rs b/src/op.rs index 0590a28..ddc9913 100644 --- a/src/op.rs +++ b/src/op.rs @@ -13,7 +13,7 @@ pub struct Operation { pub enum OpKind { Delete(DeletedFiles), Put(PutFiles), - Rename(RenamedFile), + Rename(Vec<(PathBuf, PathBuf)>), } #[derive(Debug, Clone)] @@ -30,12 +30,6 @@ pub struct PutFiles { pub dir: PathBuf, } -#[derive(Debug, Clone)] -pub struct RenamedFile { - pub original_name: PathBuf, - pub new_name: PathBuf, -} - impl Operation { /// Discard undone operations when new one is pushed. pub fn branch(&mut self) { @@ -63,7 +57,14 @@ fn log(op: &OpKind) { info!("DELETE: {:?}", item_to_pathvec(&op.original)); } OpKind::Rename(op) => { - info!("RENAME: {:?} -> {:?}", op.original_name, op.new_name); + if !op.is_empty() { + info!( + "RENAME: {:?}", + op.iter() + .map(|v| format!("{:?} -> {:?}", v.0, v.1)) + .collect::>() + ); + } } } } @@ -84,8 +85,16 @@ pub fn relog(op: &OpKind, undo: bool) { info!("{} {:?}", result, item_to_pathvec(&op.original)); } OpKind::Rename(op) => { - result.push_str("RENAME"); - info!("{} {:?} -> {:?}", result, op.original_name, op.new_name); + if !op.is_empty() { + result.push_str("RENAME"); + info!( + "{} {:?}", + result, + op.iter() + .map(|v| format!("{:?} -> {:?}", v.0, v.1)) + .collect::>() + ); + } } } } diff --git a/src/run.rs b/src/run.rs index dd9b238..12fd4a9 100644 --- a/src/run.rs +++ b/src/run.rs @@ -657,16 +657,19 @@ fn _run(mut state: State, session_path: PathBuf) -> Result<(), FxError> { } let mut dest: Option = None; if let Ok(item) = state.get_item() { + let mut err: Option = None; match item.file_type { FileType::File => { execute!(screen, EnterAlternateScreen)?; if let Err(e) = state.open_file(item) { - print_warning(e, state.layout.y); - continue; + err = Some(e); } execute!(screen, EnterAlternateScreen)?; hide_cursor(); state.reload(state.layout.y)?; + if let Some(e) = err { + print_warning(e, state.layout.y); + } continue; } FileType::Symlink => match &item.symlink_dir_path { @@ -681,12 +684,14 @@ fn _run(mut state: State, session_path: PathBuf) -> Result<(), FxError> { None => { execute!(screen, EnterAlternateScreen)?; if let Err(e) = state.open_file(item) { - print_warning(e, state.layout.y); - continue; + err = Some(e); } execute!(screen, EnterAlternateScreen)?; hide_cursor(); - state.redraw(state.layout.y); + state.reload(state.layout.y)?; + if let Some(e) = err { + print_warning(e, state.layout.y); + } continue; } }, @@ -1357,8 +1362,37 @@ fn _run(mut state: State, session_path: PathBuf) -> Result<(), FxError> { //rename KeyCode::Char('c') => { - //In visual mode, this is disabled. + //In visual mode, you can rename multiple items in default editor. if state.v_start.is_some() { + let items: Vec = state + .list + .iter() + .filter(|item| item.selected) + .map(ItemBuffer::new) + .collect(); + execute!(screen, EnterAlternateScreen)?; + let result = state.rename_multiple_items(&items); + execute!(screen, EnterAlternateScreen)?; + hide_cursor(); + state.reset_selection(); + state.reload(state.layout.y)?; + match result { + Err(e) => { + print_warning(e, state.layout.y); + } + Ok(result_len) => { + let message = { + match result_len { + 0 => "No item renamed.".to_owned(), + 1 => "1 item renamed.".to_owned(), + count => { + format!("{} items renamed.", count) + } + } + }; + print_info(message, state.layout.y); + } + } continue; } if len == 0 { @@ -1506,12 +1540,10 @@ fn _run(mut state: State, session_path: PathBuf) -> Result<(), FxError> { } state.operations.branch(); - state.operations.push(OpKind::Rename( - RenamedFile { - original_name: item.file_path.clone(), - new_name: to, - }, - )); + state.operations.push(OpKind::Rename(vec![( + item.file_path.clone(), + to, + )])); hide_cursor(); state.reload(state.layout.y)?; @@ -2389,50 +2421,37 @@ fn _run(mut state: State, session_path: PathBuf) -> Result<(), FxError> { } //Execute command as is + let mut err: Option<&str> = None; execute!(screen, EnterAlternateScreen)?; if std::env::set_current_dir(&state.current_dir) .is_err() { - execute!(screen, EnterAlternateScreen)?; - print_warning( - "Cannot execute command", - state.layout.y, - ); - break 'command; - } - if let Ok(sh) = std::env::var("SHELL") { + err = + Some("Changing current directory failed."); + } else if let Ok(sh) = std::env::var("SHELL") { if std::process::Command::new(&sh) .arg("-c") - .arg(&commands.join(" ")) + .arg(commands.join(" ")) .status() .is_err() { - execute!(screen, EnterAlternateScreen)?; - state.redraw(state.layout.y); - print_warning( - "Cannot execute command", - state.layout.y, - ); - break 'command; + err = Some("Command execution failed."); } } else if std::process::Command::new(command) .args(&commands[1..]) .status() .is_err() { - execute!(screen, EnterAlternateScreen)?; - state.redraw(state.layout.y); - print_warning( - "Cannot execute command", - state.layout.y, - ); - break 'command; + err = Some("Command execution failed."); } execute!(screen, EnterAlternateScreen)?; hide_cursor(); info!("SHELL: {:?}", commands); state.reload(state.layout.y)?; + if let Some(e) = err { + print_warning(e, state.layout.y); + } break 'command; } diff --git a/src/state.rs b/src/state.rs index e2eb006..a4fc30f 100644 --- a/src/state.rs +++ b/src/state.rs @@ -190,7 +190,8 @@ impl Registers { } } -/// To avoid cost copying ItemInfo, use ItemBuffer when tinkering with register. +/// To avoid cost copying ItemInfo, use ItemBuffer +/// when tinkering with register or multiple renaming. #[derive(Debug, Clone)] pub struct ItemBuffer { pub file_type: FileType, @@ -505,7 +506,7 @@ impl State { let duration = duration_to_string(start.elapsed()); let delete_message: String = { if total == 1 { - format!("1 item deleted [{}]", duration) + format!("1 item deleted. [{}]", duration) } else { let mut count = total.to_string(); let _ = write!(count, " items deleted [{}]", duration); @@ -921,7 +922,9 @@ impl State { pub fn undo(&mut self, op: &OpKind) -> Result<(), FxError> { match op { OpKind::Rename(op) => { - std::fs::rename(&op.new_name, &op.original_name)?; + for (original, new) in op { + std::fs::rename(new, original)?; + } self.operations.pos += 1; self.update_list()?; self.clear_and_show_headline(); @@ -959,7 +962,9 @@ impl State { pub fn redo(&mut self, op: &OpKind) -> Result<(), FxError> { match op { OpKind::Rename(op) => { - std::fs::rename(&op.original_name, &op.new_name)?; + for (original, new) in op { + std::fs::rename(original, new)?; + } self.operations.pos -= 1; self.update_list()?; self.clear_and_show_headline(); @@ -1287,6 +1292,56 @@ impl State { self.list = result; } + /// Rename selected items at once. + pub fn rename_multiple_items(&mut self, items: &[ItemBuffer]) -> Result { + let names: Vec<&str> = items.iter().map(|item| item.file_name.as_str()).collect(); + let mut file = tempfile::NamedTempFile::new()?; + writeln!(file, "{}", names.join("\n"))?; + + let mut default = Command::new(&self.default); + let path = file.into_temp_path(); + if let Err(e) = default + .arg(&path) + .status() + .map_err(|_| FxError::DefaultEditor) + { + Err(e) + } else { + let new_names = fs::read_to_string(&path)?; + // clean up temp file + path.close()?; + let new_names: Vec<&str> = new_names + .split('\n') + .filter(|name| !name.is_empty()) + .collect(); + if new_names.len() != items.len() { + Err(FxError::Io( + format!( + "Rename failed: Expected {} names, but received {} names", + items.len(), + new_names.len() + ) + .to_string(), + )) + } else { + let mut result: Vec<(PathBuf, PathBuf)> = vec![]; + for (i, new_name) in new_names.iter().enumerate() { + let mut to = self.current_dir.clone(); + to.push(new_name); + if &items[i].file_name != new_name { + std::fs::rename(&items[i].file_path, &to)?; + result.push((items[i].file_path.clone(), to)) + } + } + let len = result.len(); + self.operations.branch(); + self.operations.push(OpKind::Rename(result)); + + Ok(len) + } + } + } + /// Reset all item's selected state and exit the select mode. pub fn reset_selection(&mut self) { for item in self.list.iter_mut() {