From af4439de883e5cb05fcc7d4b8c045320e3f0d4a9 Mon Sep 17 00:00:00 2001 From: cecilia Date: Mon, 15 Jun 2026 14:18:03 +0200 Subject: [PATCH] Add feature to batch-rename entries This closes #916 Signed-off-by: cecilia --- contrib/completions/_zoxide | 18 ++++++++++ contrib/completions/_zoxide.ps1 | 12 +++++++ contrib/completions/zoxide.bash | 33 ++++++++++++++++- contrib/completions/zoxide.elv | 11 ++++++ contrib/completions/zoxide.fish | 6 ++++ contrib/completions/zoxide.nu | 9 +++++ contrib/completions/zoxide.ts | 34 ++++++++++++++++++ src/cmd/cmd.rs | 21 +++++++++++ src/cmd/mod.rs | 2 ++ src/cmd/rename.rs | 64 +++++++++++++++++++++++++++++++++ 10 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 src/cmd/rename.rs diff --git a/contrib/completions/_zoxide b/contrib/completions/_zoxide index 2f4c9f0..fad7804 100644 --- a/contrib/completions/_zoxide +++ b/contrib/completions/_zoxide @@ -208,6 +208,18 @@ _arguments "${_arguments_options[@]}" : \ '--version[Print version]' \ '*::paths:_files -/' \ && ret=0 +;; +(rename) +_arguments "${_arguments_options[@]}" : \ +'--old-name=[]:old_name:_files -/' \ +'--new-name=[]:new_name:_files -/' \ +'-f[skip y/n]' \ +'--force[skip y/n]' \ +'-h[Print help]' \ +'--help[Print help]' \ +'-V[Print version]' \ +'--version[Print version]' \ +&& ret=0 ;; esac ;; @@ -223,6 +235,7 @@ _zoxide_commands() { 'init:Generate shell configuration' \ 'query:Search for a directory in the database' \ 'remove:Remove a directory from the database' \ +'rename:Batch rename paths by replacing with ' \ ) _describe -t commands 'zoxide commands' commands "$@" } @@ -318,6 +331,11 @@ _zoxide__subcmd__remove_commands() { local commands; commands=() _describe -t commands 'zoxide remove commands' commands "$@" } +(( $+functions[_zoxide__subcmd__rename_commands] )) || +_zoxide__subcmd__rename_commands() { + local commands; commands=() + _describe -t commands 'zoxide rename commands' commands "$@" +} if [ "$funcstack[1]" = "_zoxide" ]; then _zoxide "$@" diff --git a/contrib/completions/_zoxide.ps1 b/contrib/completions/_zoxide.ps1 index a76c12c..c172663 100644 --- a/contrib/completions/_zoxide.ps1 +++ b/contrib/completions/_zoxide.ps1 @@ -31,6 +31,7 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock { [CompletionResult]::new('init', 'init', [CompletionResultType]::ParameterValue, 'Generate shell configuration') [CompletionResult]::new('query', 'query', [CompletionResultType]::ParameterValue, 'Search for a directory in the database') [CompletionResult]::new('remove', 'remove', [CompletionResultType]::ParameterValue, 'Remove a directory from the database') + [CompletionResult]::new('rename', 'rename', [CompletionResultType]::ParameterValue, 'Batch rename paths by replacing with ') break } 'zoxide;add' { @@ -177,6 +178,17 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock { [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') break } + 'zoxide;rename' { + [CompletionResult]::new('--old-name', '--old-name', [CompletionResultType]::ParameterName, 'old-name') + [CompletionResult]::new('--new-name', '--new-name', [CompletionResultType]::ParameterName, 'new-name') + [CompletionResult]::new('-f', '-f', [CompletionResultType]::ParameterName, 'skip y/n') + [CompletionResult]::new('--force', '--force', [CompletionResultType]::ParameterName, 'skip y/n') + [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') + [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') + break + } }) $completions.Where{ $_.CompletionText -like "$wordToComplete*" } | diff --git a/contrib/completions/zoxide.bash b/contrib/completions/zoxide.bash index 1e6fdcf..2ef362f 100644 --- a/contrib/completions/zoxide.bash +++ b/contrib/completions/zoxide.bash @@ -34,6 +34,9 @@ _zoxide() { zoxide,remove) cmd="zoxide__subcmd__remove" ;; + zoxide,rename) + cmd="zoxide__subcmd__rename" + ;; zoxide__subcmd__edit,decrement) cmd="zoxide__subcmd__edit__subcmd__decrement" ;; @@ -71,7 +74,7 @@ _zoxide() { case "${cmd}" in zoxide) - opts="-h -V --help --version add edit import init query remove" + opts="-h -V --help --version add edit import init query remove rename" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -338,6 +341,34 @@ _zoxide() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; + zoxide__subcmd__rename) + opts="-f -h -V --old-name --new-name --force --help --version" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + --old-name) + COMPREPLY=() + if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then + compopt -o plusdirs + fi + return 0 + ;; + --new-name) + COMPREPLY=() + if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then + compopt -o plusdirs + fi + return 0 + ;; + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; esac } diff --git a/contrib/completions/zoxide.elv b/contrib/completions/zoxide.elv index 5ba33fa..17ceaea 100644 --- a/contrib/completions/zoxide.elv +++ b/contrib/completions/zoxide.elv @@ -28,6 +28,7 @@ set edit:completion:arg-completer[zoxide] = {|@words| cand init 'Generate shell configuration' cand query 'Search for a directory in the database' cand remove 'Remove a directory from the database' + cand rename 'Batch rename paths by replacing with ' } &'zoxide;add'= { cand -s 'The rank to increment the entry if it exists or initialize it with if it doesn''t' @@ -157,6 +158,16 @@ set edit:completion:arg-completer[zoxide] = {|@words| cand -V 'Print version' cand --version 'Print version' } + &'zoxide;rename'= { + cand --old-name 'old-name' + cand --new-name 'new-name' + cand -f 'skip y/n' + cand --force 'skip y/n' + cand -h 'Print help' + cand --help 'Print help' + cand -V 'Print version' + cand --version 'Print version' + } ] $completions[$command] } diff --git a/contrib/completions/zoxide.fish b/contrib/completions/zoxide.fish index 6d7e5b2..2c71e2b 100644 --- a/contrib/completions/zoxide.fish +++ b/contrib/completions/zoxide.fish @@ -32,6 +32,7 @@ complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "import" -d 'Import en complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "init" -d 'Generate shell configuration' complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "query" -d 'Search for a directory in the database' complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "remove" -d 'Remove a directory from the database' +complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "rename" -d 'Batch rename paths by replacing with ' complete -c zoxide -n "__fish_zoxide_using_subcommand add" -s s -l score -d 'The rank to increment the entry if it exists or initialize it with if it doesn\'t' -r complete -c zoxide -n "__fish_zoxide_using_subcommand add" -s h -l help -d 'Print help' complete -c zoxide -n "__fish_zoxide_using_subcommand add" -s V -l version -d 'Print version' @@ -93,3 +94,8 @@ complete -c zoxide -n "__fish_zoxide_using_subcommand query" -s h -l help -d 'Pr complete -c zoxide -n "__fish_zoxide_using_subcommand query" -s V -l version -d 'Print version' complete -c zoxide -n "__fish_zoxide_using_subcommand remove" -s h -l help -d 'Print help' complete -c zoxide -n "__fish_zoxide_using_subcommand remove" -s V -l version -d 'Print version' +complete -c zoxide -n "__fish_zoxide_using_subcommand rename" -l old-name -r -f -a "(__fish_complete_directories)" +complete -c zoxide -n "__fish_zoxide_using_subcommand rename" -l new-name -r -f -a "(__fish_complete_directories)" +complete -c zoxide -n "__fish_zoxide_using_subcommand rename" -s f -l force -d 'skip y/n' +complete -c zoxide -n "__fish_zoxide_using_subcommand rename" -s h -l help -d 'Print help' +complete -c zoxide -n "__fish_zoxide_using_subcommand rename" -s V -l version -d 'Print version' diff --git a/contrib/completions/zoxide.nu b/contrib/completions/zoxide.nu index 4d07049..9d16cd0 100644 --- a/contrib/completions/zoxide.nu +++ b/contrib/completions/zoxide.nu @@ -130,6 +130,15 @@ module completions { ...paths: path ] + # Batch rename paths by replacing with + export extern "zoxide rename" [ + --old-name: path + --new-name: path + --force(-f) # skip y/n + --help(-h) # Print help + --version(-V) # Print version + ] + } export use completions * diff --git a/contrib/completions/zoxide.ts b/contrib/completions/zoxide.ts index 207da2f..6ec60b0 100644 --- a/contrib/completions/zoxide.ts +++ b/contrib/completions/zoxide.ts @@ -377,6 +377,40 @@ const completion: Fig.Spec = { template: "folders", }, }, + { + name: "rename", + description: "Batch rename paths by replacing with ", + options: [ + { + name: "--old-name", + isRepeatable: true, + args: { + name: "old_name", + template: "folders", + }, + }, + { + name: "--new-name", + isRepeatable: true, + args: { + name: "new_name", + template: "folders", + }, + }, + { + name: ["-f", "--force"], + description: "skip y/n", + }, + { + name: ["-h", "--help"], + description: "Print help", + }, + { + name: ["-V", "--version"], + description: "Print version", + }, + ], + }, ], options: [ { diff --git a/src/cmd/cmd.rs b/src/cmd/cmd.rs index 0de2ee5..6ec7ec8 100644 --- a/src/cmd/cmd.rs +++ b/src/cmd/cmd.rs @@ -47,6 +47,7 @@ pub enum Cmd { Init(Init), Query(Query), Remove(Remove), + Rename(Rename), } /// Add a new directory or increment its rank @@ -209,3 +210,23 @@ pub struct Remove { #[clap(value_hint = ValueHint::DirPath)] pub paths: Vec, } + +/// Batch rename paths by replacing with +#[derive(Debug, Parser)] +#[clap( + author, + help_template = HelpTemplate, +)] +pub struct Rename { + // old name + #[clap(long, value_hint = ValueHint::DirPath, value_name = "old_name")] + pub old_name: String, + + // new name + #[clap(long, value_hint = ValueHint::DirPath, value_name = "new_name")] + pub new_name: String, + + /// skip y/n + #[clap(long, short)] + pub force: bool, +} diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 5c17474..ac9bd8b 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -5,6 +5,7 @@ mod import; mod init; mod query; mod remove; +mod rename; use anyhow::Result; @@ -23,6 +24,7 @@ impl Run for Cmd { Cmd::Init(cmd) => cmd.run(), Cmd::Query(cmd) => cmd.run(), Cmd::Remove(cmd) => cmd.run(), + Cmd::Rename(cmd) => cmd.run(), } } } diff --git a/src/cmd/rename.rs b/src/cmd/rename.rs new file mode 100644 index 0000000..103308e --- /dev/null +++ b/src/cmd/rename.rs @@ -0,0 +1,64 @@ +use std::io::{self, Read, Write}; + +use anyhow::{Result, bail}; + +use crate::cmd::{Rename, Run}; +use crate::db::{Database, Epoch, Rank}; +use crate::error::BrokenPipeHandler; + +struct Data { + pub old_path: String, + pub new_path: String, + pub rank: Rank, + pub last_accessed: Epoch, +} + +impl Run for Rename { + fn run(&self) -> Result<()> { + let mut db = Database::open()?; + + let mut to_edit = Vec::::new(); + + for path in db.dirs() { + let old = path.display().to_string(); + let new = path.display().to_string().replace(&self.old_name, &self.new_name); + + if new != old { + writeln!(io::stdout(), "{old} -> {new}") + .pipe_exit("stdout") + .expect("cannot write to stdout."); + + to_edit.push(Data { + old_path: old, + new_path: new, + rank: path.rank, + last_accessed: path.last_accessed, + }); + } + } + + for item in to_edit.iter() { + db.remove(&item.old_path); + db.add(&item.new_path, item.rank, item.last_accessed); + } + + if to_edit.is_empty() { + bail!("No entries to rename."); + } + + if self.force { + return db.save(); + } + + writeln!(io::stdout(), "Rename {} entries? (y/n)", to_edit.len()) + .pipe_exit("stdout") + .expect("cannot write to stdout."); + + let mut buf = [0]; + std::io::stdin().read_exact(&mut buf).expect("input expected"); + match buf[0] as char { + 'y' | 'Y' => db.save(), + _ => bail!("Rename aborted."), + } + } +}