Skip to content

Commit

Permalink
Merge pull request #306 from kyoheiu/feature/rename-multiple-items
Browse files Browse the repository at this point in the history
Feature: rename multiple items
  • Loading branch information
kyoheiu authored Jan 11, 2025
2 parents c35aeca + 0ad66fc commit 871584e
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 50 deletions.
21 changes: 21 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
5 changes: 4 additions & 1 deletion src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::path::PathBuf;

#[derive(Debug)]
#[derive(Debug, Default)]
pub enum FxError {
Arg(String),
TerminalSizeDetection,
Expand All @@ -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 {}
Expand Down Expand Up @@ -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)
}
Expand Down
29 changes: 19 additions & 10 deletions src/op.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pub struct Operation {
pub enum OpKind {
Delete(DeletedFiles),
Put(PutFiles),
Rename(RenamedFile),
Rename(Vec<(PathBuf, PathBuf)>),
}

#[derive(Debug, Clone)]
Expand All @@ -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) {
Expand Down Expand Up @@ -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::<Vec<String>>()
);
}
}
}
}
Expand All @@ -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::<Vec<String>>()
);
}
}
}
}
Expand Down
89 changes: 54 additions & 35 deletions src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -657,16 +657,19 @@ fn _run(mut state: State, session_path: PathBuf) -> Result<(), FxError> {
}
let mut dest: Option<PathBuf> = None;
if let Ok(item) = state.get_item() {
let mut err: Option<FxError> = 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 {
Expand All @@ -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;
}
},
Expand Down Expand Up @@ -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<ItemBuffer> = 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 {
Expand Down Expand Up @@ -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)?;
Expand Down Expand Up @@ -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;
}

Expand Down
63 changes: 59 additions & 4 deletions src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -1287,6 +1292,56 @@ impl State {
self.list = result;
}

/// Rename selected items at once.
pub fn rename_multiple_items(&mut self, items: &[ItemBuffer]) -> Result<usize, FxError> {
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() {
Expand Down

0 comments on commit 871584e

Please sign in to comment.