diff --git a/contrib/completions/_zoxide b/contrib/completions/_zoxide index 2f4c9f0..79cd8d7 100644 --- a/contrib/completions/_zoxide +++ b/contrib/completions/_zoxide @@ -39,6 +39,17 @@ _arguments "${_arguments_options[@]}" : \ '*::paths:_files -/' \ && ret=0 ;; +(add-alias) +_arguments "${_arguments_options[@]}" : \ +'-p+[Path to add aliases to]:PATH:_files -/' \ +'--path=[Path to add aliases to]:PATH:_files -/' \ +'-h[Print help]' \ +'--help[Print help]' \ +'-V[Print version]' \ +'--version[Print version]' \ +'*::aliases:_default' \ +&& ret=0 +;; (edit) _arguments "${_arguments_options[@]}" : \ '-h[Print help]' \ @@ -193,6 +204,7 @@ _arguments "${_arguments_options[@]}" : \ '(-i --interactive)--list[List all matching directories]' \ '-s[Print score with results]' \ '--score[Print score with results]' \ +'--aliases[Print aliases with results]' \ '-h[Print help]' \ '--help[Print help]' \ '-V[Print version]' \ @@ -208,6 +220,17 @@ _arguments "${_arguments_options[@]}" : \ '--version[Print version]' \ '*::paths:_files -/' \ && ret=0 +;; +(remove-alias) +_arguments "${_arguments_options[@]}" : \ +'-p+[Path to remove aliases from]:PATH:_files -/' \ +'--path=[Path to remove aliases from]:PATH:_files -/' \ +'-h[Print help]' \ +'--help[Print help]' \ +'-V[Print version]' \ +'--version[Print version]' \ +'*::aliases:_default' \ +&& ret=0 ;; esac ;; @@ -218,11 +241,13 @@ esac _zoxide_commands() { local commands; commands=( 'add:Add a new directory or increment its rank' \ +'add-alias:Add aliases for a directory' \ 'edit:Edit the database' \ 'import:Import entries from another application' \ 'init:Generate shell configuration' \ 'query:Search for a directory in the database' \ 'remove:Remove a directory from the database' \ +'remove-alias:Remove aliases from a directory' \ ) _describe -t commands 'zoxide commands' commands "$@" } @@ -231,6 +256,11 @@ _zoxide__subcmd__add_commands() { local commands; commands=() _describe -t commands 'zoxide add commands' commands "$@" } +(( $+functions[_zoxide__subcmd__add-alias_commands] )) || +_zoxide__subcmd__add-alias_commands() { + local commands; commands=() + _describe -t commands 'zoxide add-alias commands' commands "$@" +} (( $+functions[_zoxide__subcmd__edit_commands] )) || _zoxide__subcmd__edit_commands() { local commands; commands=( @@ -318,6 +348,11 @@ _zoxide__subcmd__remove_commands() { local commands; commands=() _describe -t commands 'zoxide remove commands' commands "$@" } +(( $+functions[_zoxide__subcmd__remove-alias_commands] )) || +_zoxide__subcmd__remove-alias_commands() { + local commands; commands=() + _describe -t commands 'zoxide remove-alias commands' commands "$@" +} if [ "$funcstack[1]" = "_zoxide" ]; then _zoxide "$@" diff --git a/contrib/completions/_zoxide.ps1 b/contrib/completions/_zoxide.ps1 index a76c12c..c0c0f10 100644 --- a/contrib/completions/_zoxide.ps1 +++ b/contrib/completions/_zoxide.ps1 @@ -26,11 +26,13 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock { [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') [CompletionResult]::new('add', 'add', [CompletionResultType]::ParameterValue, 'Add a new directory or increment its rank') + [CompletionResult]::new('add-alias', 'add-alias', [CompletionResultType]::ParameterValue, 'Add aliases for a directory') [CompletionResult]::new('edit', 'edit', [CompletionResultType]::ParameterValue, 'Edit the database') [CompletionResult]::new('import', 'import', [CompletionResultType]::ParameterValue, 'Import entries from another application') [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('remove-alias', 'remove-alias', [CompletionResultType]::ParameterValue, 'Remove aliases from a directory') break } 'zoxide;add' { @@ -42,6 +44,15 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock { [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') break } + 'zoxide;add-alias' { + [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Path to add aliases to') + [CompletionResult]::new('--path', '--path', [CompletionResultType]::ParameterName, 'Path to add aliases to') + [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 + } 'zoxide;edit' { [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') @@ -164,6 +175,7 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock { [CompletionResult]::new('--list', '--list', [CompletionResultType]::ParameterName, 'List all matching directories') [CompletionResult]::new('-s', '-s', [CompletionResultType]::ParameterName, 'Print score with results') [CompletionResult]::new('--score', '--score', [CompletionResultType]::ParameterName, 'Print score with results') + [CompletionResult]::new('--aliases', '--aliases', [CompletionResultType]::ParameterName, 'Print aliases with results') [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') @@ -177,6 +189,15 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock { [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') break } + 'zoxide;remove-alias' { + [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Path to remove aliases from') + [CompletionResult]::new('--path', '--path', [CompletionResultType]::ParameterName, 'Path to remove aliases from') + [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..1af8e12 100644 --- a/contrib/completions/zoxide.bash +++ b/contrib/completions/zoxide.bash @@ -19,6 +19,9 @@ _zoxide() { zoxide,add) cmd="zoxide__subcmd__add" ;; + zoxide,add-alias) + cmd="zoxide__subcmd__add__subcmd__alias" + ;; zoxide,edit) cmd="zoxide__subcmd__edit" ;; @@ -34,6 +37,9 @@ _zoxide() { zoxide,remove) cmd="zoxide__subcmd__remove" ;; + zoxide,remove-alias) + cmd="zoxide__subcmd__remove__subcmd__alias" + ;; zoxide__subcmd__edit,decrement) cmd="zoxide__subcmd__edit__subcmd__decrement" ;; @@ -71,7 +77,7 @@ _zoxide() { case "${cmd}" in zoxide) - opts="-h -V --help --version add edit import init query remove" + opts="-h -V --help --version add add-alias edit import init query remove remove-alias" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -106,6 +112,34 @@ _zoxide() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; + zoxide__subcmd__add__subcmd__alias) + opts="-p -h -V --path --help --version ..." + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + --path) + COMPREPLY=() + if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then + compopt -o plusdirs + fi + return 0 + ;; + -p) + COMPREPLY=() + if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then + compopt -o plusdirs + fi + return 0 + ;; + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; zoxide__subcmd__edit) opts="-h -V --help --version decrement delete increment reload" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then @@ -297,7 +331,7 @@ _zoxide() { return 0 ;; zoxide__subcmd__query) - opts="-a -i -l -s -h -V --all --interactive --list --score --exclude --base-dir --help --version [KEYWORDS]..." + opts="-a -i -l -s -h -V --all --interactive --list --score --aliases --exclude --base-dir --help --version [KEYWORDS]..." if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -338,6 +372,34 @@ _zoxide() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; + zoxide__subcmd__remove__subcmd__alias) + opts="-p -h -V --path --help --version ..." + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + --path) + COMPREPLY=() + if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then + compopt -o plusdirs + fi + return 0 + ;; + -p) + 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..de24d81 100644 --- a/contrib/completions/zoxide.elv +++ b/contrib/completions/zoxide.elv @@ -23,11 +23,13 @@ set edit:completion:arg-completer[zoxide] = {|@words| cand -V 'Print version' cand --version 'Print version' cand add 'Add a new directory or increment its rank' + cand add-alias 'Add aliases for a directory' cand edit 'Edit the database' cand import 'Import entries from another application' cand init 'Generate shell configuration' cand query 'Search for a directory in the database' cand remove 'Remove a directory from the database' + cand remove-alias 'Remove aliases from a directory' } &'zoxide;add'= { cand -s 'The rank to increment the entry if it exists or initialize it with if it doesn''t' @@ -37,6 +39,14 @@ set edit:completion:arg-completer[zoxide] = {|@words| cand -V 'Print version' cand --version 'Print version' } + &'zoxide;add-alias'= { + cand -p 'Path to add aliases to' + cand --path 'Path to add aliases to' + cand -h 'Print help' + cand --help 'Print help' + cand -V 'Print version' + cand --version 'Print version' + } &'zoxide;edit'= { cand -h 'Print help' cand --help 'Print help' @@ -146,6 +156,7 @@ set edit:completion:arg-completer[zoxide] = {|@words| cand --list 'List all matching directories' cand -s 'Print score with results' cand --score 'Print score with results' + cand --aliases 'Print aliases with results' cand -h 'Print help' cand --help 'Print help' cand -V 'Print version' @@ -157,6 +168,14 @@ set edit:completion:arg-completer[zoxide] = {|@words| cand -V 'Print version' cand --version 'Print version' } + &'zoxide;remove-alias'= { + cand -p 'Path to remove aliases from' + cand --path 'Path to remove aliases from' + 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..6c77c0c 100644 --- a/contrib/completions/zoxide.fish +++ b/contrib/completions/zoxide.fish @@ -27,14 +27,19 @@ end complete -c zoxide -n "__fish_zoxide_needs_command" -s h -l help -d 'Print help' complete -c zoxide -n "__fish_zoxide_needs_command" -s V -l version -d 'Print version' complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "add" -d 'Add a new directory or increment its rank' +complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "add-alias" -d 'Add aliases for a directory' complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "edit" -d 'Edit the database' complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "import" -d 'Import entries from another application' 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 "remove-alias" -d 'Remove aliases from a directory' 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' +complete -c zoxide -n "__fish_zoxide_using_subcommand add-alias" -s p -l path -d 'Path to add aliases to' -r -f -a "(__fish_complete_directories)" +complete -c zoxide -n "__fish_zoxide_using_subcommand add-alias" -s h -l help -d 'Print help' +complete -c zoxide -n "__fish_zoxide_using_subcommand add-alias" -s V -l version -d 'Print version' complete -c zoxide -n "__fish_zoxide_using_subcommand edit; and not __fish_seen_subcommand_from decrement delete increment reload" -s h -l help -d 'Print help' complete -c zoxide -n "__fish_zoxide_using_subcommand edit; and not __fish_seen_subcommand_from decrement delete increment reload" -s V -l version -d 'Print version' complete -c zoxide -n "__fish_zoxide_using_subcommand edit; and not __fish_seen_subcommand_from decrement delete increment reload" -f -a "decrement" @@ -89,7 +94,11 @@ complete -c zoxide -n "__fish_zoxide_using_subcommand query" -s a -l all -d 'Sho complete -c zoxide -n "__fish_zoxide_using_subcommand query" -s i -l interactive -d 'Use interactive selection' complete -c zoxide -n "__fish_zoxide_using_subcommand query" -s l -l list -d 'List all matching directories' complete -c zoxide -n "__fish_zoxide_using_subcommand query" -s s -l score -d 'Print score with results' +complete -c zoxide -n "__fish_zoxide_using_subcommand query" -l aliases -d 'Print aliases with results' complete -c zoxide -n "__fish_zoxide_using_subcommand query" -s h -l help -d 'Print help' 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 remove-alias" -s p -l path -d 'Path to remove aliases from' -r -f -a "(__fish_complete_directories)" +complete -c zoxide -n "__fish_zoxide_using_subcommand remove-alias" -s h -l help -d 'Print help' +complete -c zoxide -n "__fish_zoxide_using_subcommand remove-alias" -s V -l version -d 'Print version' diff --git a/contrib/completions/zoxide.nu b/contrib/completions/zoxide.nu index 4d07049..23aa5e2 100644 --- a/contrib/completions/zoxide.nu +++ b/contrib/completions/zoxide.nu @@ -14,6 +14,14 @@ module completions { ...paths: path ] + # Add aliases for a directory + export extern "zoxide add-alias" [ + --path(-p): path # Path to add aliases to + --help(-h) # Print help + --version(-V) # Print version + ...aliases: string + ] + # Edit the database export extern "zoxide edit" [ --help(-h) # Print help @@ -116,6 +124,7 @@ module completions { --interactive(-i) # Use interactive selection --list(-l) # List all matching directories --score(-s) # Print score with results + --aliases # Print aliases with results --exclude: path # Exclude the current directory --base-dir: path # Only search within this directory --help(-h) # Print help @@ -130,6 +139,14 @@ module completions { ...paths: path ] + # Remove aliases from a directory + export extern "zoxide remove-alias" [ + --path(-p): path # Path to remove aliases from + --help(-h) # Print help + --version(-V) # Print version + ...aliases: string + ] + } export use completions * diff --git a/contrib/completions/zoxide.ts b/contrib/completions/zoxide.ts index 207da2f..e6d37cf 100644 --- a/contrib/completions/zoxide.ts +++ b/contrib/completions/zoxide.ts @@ -30,6 +30,33 @@ const completion: Fig.Spec = { template: "folders", }, }, + { + name: "add-alias", + description: "Add aliases for a directory", + options: [ + { + name: ["-p", "--path"], + description: "Path to add aliases to", + isRepeatable: true, + args: { + name: "path", + template: "folders", + }, + }, + { + name: ["-h", "--help"], + description: "Print help", + }, + { + name: ["-V", "--version"], + description: "Print version", + }, + ], + args: { + name: "aliases", + isVariadic: true, + }, + }, { name: "edit", description: "Edit the database", @@ -342,6 +369,10 @@ const completion: Fig.Spec = { name: ["-s", "--score"], description: "Print score with results", }, + { + name: "--aliases", + description: "Print aliases with results", + }, { name: ["-h", "--help"], description: "Print help", @@ -377,6 +408,33 @@ const completion: Fig.Spec = { template: "folders", }, }, + { + name: "remove-alias", + description: "Remove aliases from a directory", + options: [ + { + name: ["-p", "--path"], + description: "Path to remove aliases from", + isRepeatable: true, + args: { + name: "path", + template: "folders", + }, + }, + { + name: ["-h", "--help"], + description: "Print help", + }, + { + name: ["-V", "--version"], + description: "Print version", + }, + ], + args: { + name: "aliases", + isVariadic: true, + }, + }, ], options: [ { diff --git a/src/cmd/add_alias.rs b/src/cmd/add_alias.rs new file mode 100644 index 0000000..fb955d0 --- /dev/null +++ b/src/cmd/add_alias.rs @@ -0,0 +1,43 @@ +use std::path::Path; + +use anyhow::{Result, bail}; + +use crate::cmd::{AddAlias, Run}; +use crate::db::Database; +use crate::{config, util}; + +impl Run for AddAlias { + fn run(&self) -> Result<()> { + // These characters can't be printed cleanly to a single line, so they can cause + // confusion when writing to stdout. + const EXCLUDE_CHARS: &[char] = &['\n', '\r']; + + let exclude_dirs = config::exclude_dirs()?; + let max_age = config::maxage()?; + let now = util::current_time()?; + + let mut db = Database::open()?; + + let path = + if config::resolve_symlinks() { util::canonicalize } else { util::resolve_path }( + &self.path, + )?; + let path = util::path_to_str(&path)?; + + // Ignore path if it contains unsupported characters, or if it's in the exclude + // list. + if path.contains(EXCLUDE_CHARS) || exclude_dirs.iter().any(|glob| glob.matches(path)) { + return Ok(()); + } + if !Path::new(path).is_dir() { + bail!("not a directory: {path}"); + } + + db.add_alias_update(path, self.aliases.iter(), now); + + if db.dirty() { + db.age(max_age); + } + db.save() + } +} diff --git a/src/cmd/cmd.rs b/src/cmd/cmd.rs index 0de2ee5..012ddbd 100644 --- a/src/cmd/cmd.rs +++ b/src/cmd/cmd.rs @@ -42,11 +42,13 @@ https://github.com/ajeetdsouza/zoxide )] pub enum Cmd { Add(Add), + AddAlias(AddAlias), Edit(Edit), Import(Import), Init(Init), Query(Query), Remove(Remove), + RemoveAlias(RemoveAlias), } /// Add a new directory or increment its rank @@ -65,6 +67,21 @@ pub struct Add { pub score: Option, } +/// Add aliases for a directory +#[derive(Debug, Parser)] +#[clap( + author, + help_template = HelpTemplate, +)] +pub struct AddAlias { + #[clap(num_args = 1.., required = true)] + pub aliases: Vec, + + /// Path to add aliases to + #[clap(short, long, required = true, value_hint = ValueHint::DirPath)] + pub path: PathBuf, +} + /// Edit the database #[derive(Debug, Parser)] #[clap( @@ -190,6 +207,10 @@ pub struct Query { #[clap(long, short)] pub score: bool, + /// Print aliases with results + #[clap(long)] + pub aliases: bool, + /// Exclude the current directory #[clap(long, value_hint = ValueHint::DirPath, value_name = "path")] pub exclude: Option, @@ -209,3 +230,18 @@ pub struct Remove { #[clap(value_hint = ValueHint::DirPath)] pub paths: Vec, } + +/// Remove aliases from a directory +#[derive(Debug, Parser)] +#[clap( + author, + help_template = HelpTemplate, +)] +pub struct RemoveAlias { + #[clap(num_args = 1.., required = true)] + pub aliases: Vec, + + /// Path to remove aliases from + #[clap(short, long, required = true, value_hint = ValueHint::DirPath)] + pub path: String, +} diff --git a/src/cmd/edit.rs b/src/cmd/edit.rs index 0f37165..1dc3055 100644 --- a/src/cmd/edit.rs +++ b/src/cmd/edit.rs @@ -26,8 +26,12 @@ impl Run for Edit { let stdout = &mut io::stdout().lock(); for dir in db.dirs().iter().rev() { - write!(stdout, "{}\0", dir.display().with_score(now).with_separator('\t')) - .pipe_exit("fzf")?; + write!( + stdout, + "{}\0", + dir.display().with_score(now).with_aliases(true).with_separator('\t') + ) + .pipe_exit("fzf")?; } Ok(()) } @@ -53,9 +57,9 @@ impl Edit { "--bind=\ btab:up,\ ctrl-r:reload(zoxide edit reload),\ -ctrl-d:reload(zoxide edit delete {2..}),\ -ctrl-w:reload(zoxide edit increment {2..}),\ -ctrl-s:reload(zoxide edit decrement {2..}),\ +ctrl-d:reload(zoxide edit delete {3..}),\ +ctrl-w:reload(zoxide edit increment {3..}),\ +ctrl-s:reload(zoxide edit decrement {3..}),\ ctrl-z:ignore,\ double-click:ignore,\ enter:abort,\ @@ -70,7 +74,7 @@ tab:down", ctrl-r:reload \tctrl-d:delete ctrl-w:increment\tctrl-s:decrement - SCORE\tPATH", + SCORE\tALIASES\tPATH", "--info=inline", "--layout=reverse", "--padding=1,0,0,0", diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 5c17474..3b93fe8 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -1,10 +1,12 @@ mod add; +mod add_alias; mod cmd; mod edit; mod import; mod init; mod query; mod remove; +mod remove_alias; use anyhow::Result; @@ -18,11 +20,13 @@ impl Run for Cmd { fn run(&self) -> Result<()> { match self { Cmd::Add(cmd) => cmd.run(), + Cmd::AddAlias(cmd) => cmd.run(), Cmd::Edit(cmd) => cmd.run(), Cmd::Import(cmd) => cmd.run(), Cmd::Init(cmd) => cmd.run(), Cmd::Query(cmd) => cmd.run(), Cmd::Remove(cmd) => cmd.run(), + Cmd::RemoveAlias(cmd) => cmd.run(), } } } diff --git a/src/cmd/query.rs b/src/cmd/query.rs index 6539c2e..282ce4c 100644 --- a/src/cmd/query.rs +++ b/src/cmd/query.rs @@ -35,7 +35,8 @@ impl Query { match stream.next() { Some(dir) if Some(dir.path.as_ref()) == self.exclude.as_deref() => continue, Some(dir) => { - if let Some(selection) = fzf.write(dir, now)? { + // Always enable aliases for interactive queries + if let Some(selection) = fzf.write(dir, now, true)? { break selection; } } @@ -45,9 +46,17 @@ impl Query { if self.score { print!("{selection}"); - } else { + } else if self.aliases { let path = selection.get(7..).context("could not read selection from fzf")?; print!("{path}"); + } else { + let path = selection + .get(7..) + .map(|path| { + path.get(path.find('\t').map(|idx| idx + 1).unwrap_or(0)..).unwrap_or(path) + }) + .context("could not read selection from fzf")?; + print!("{path}"); } Ok(()) } @@ -59,6 +68,7 @@ impl Query { continue; } let dir = if self.score { dir.display().with_score(now) } else { dir.display() }; + let dir = if self.aliases { dir.with_aliases(self.aliases) } else { dir }; writeln!(handle, "{dir}").pipe_exit("stdout")?; } Ok(()) @@ -73,6 +83,7 @@ impl Query { } let dir = if self.score { dir.display().with_score(now) } else { dir.display() }; + let dir = if self.aliases { dir.with_aliases(self.aliases) } else { dir }; writeln!(handle, "{dir}").pipe_exit("stdout") } diff --git a/src/cmd/remove_alias.rs b/src/cmd/remove_alias.rs new file mode 100644 index 0000000..780434d --- /dev/null +++ b/src/cmd/remove_alias.rs @@ -0,0 +1,21 @@ +use anyhow::{Result, bail}; + +use crate::cmd::{RemoveAlias, Run}; +use crate::db::Database; +use crate::util; + +impl Run for RemoveAlias { + fn run(&self) -> Result<()> { + let mut db = Database::open()?; + + if !db.remove_alias(&self.path, self.aliases.iter()) { + let path_abs = util::resolve_path(&self.path)?; + let path_abs = util::path_to_str(&path_abs)?; + if path_abs == self.path || !db.remove_alias(path_abs, self.aliases.iter()) { + bail!("path not found in database: {}", self.path) + } + } + + db.save() + } +} diff --git a/src/db/dir.rs b/src/db/dir.rs index 5d6d62c..a022205 100644 --- a/src/db/dir.rs +++ b/src/db/dir.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::collections::HashSet; use std::fmt::{self, Display, Formatter}; use serde::{Deserialize, Serialize}; @@ -6,19 +7,35 @@ use serde::{Deserialize, Serialize}; use crate::util::{DAY, HOUR, WEEK}; #[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Dir<'a> { +pub struct DirV4<'a> { + #[serde(borrow)] + pub path: Cow<'a, str>, + pub rank: Rank, + pub last_accessed: Epoch, + #[serde(borrow)] + pub aliases: HashSet>, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct DirV3<'a> { #[serde(borrow)] pub path: Cow<'a, str>, pub rank: Rank, pub last_accessed: Epoch, } -impl Dir<'_> { - pub fn display(&self) -> DirDisplay<'_> { - DirDisplay::new(self) +pub trait Dir { + fn path(&self) -> &str; + fn score(&self, now: Epoch) -> Rank; + fn aliases(&self) -> impl Iterator>; +} + +impl Dir for DirV4<'_> { + fn path(&self) -> &str { + &self.path } - pub fn score(&self, now: Epoch) -> Rank { + fn score(&self, now: Epoch) -> Rank { // The older the entry, the lesser its importance. let duration = now.saturating_sub(self.last_accessed); if duration < HOUR { @@ -31,17 +48,53 @@ impl Dir<'_> { self.rank * 0.25 } } + + fn aliases(&self) -> impl Iterator> { + self.aliases.iter() + } } -pub struct DirDisplay<'a> { - dir: &'a Dir<'a>, +impl DirV4<'_> { + pub fn display(&self) -> DirDisplay<'_, Self> { + DirDisplay::new(self) + } +} + +impl Dir for DirV3<'_> { + fn path(&self) -> &str { + &self.path + } + + fn score(&self, now: Epoch) -> Rank { + // The older the entry, the lesser its importance. + let duration = now.saturating_sub(self.last_accessed); + if duration < HOUR { + self.rank * 4.0 + } else if duration < DAY { + self.rank * 2.0 + } else if duration < WEEK { + self.rank * 0.5 + } else { + self.rank * 0.25 + } + } + + fn aliases(&self) -> impl Iterator> { + let arr: &[&str] = &[]; + arr.iter() + } +} + +pub struct DirDisplay<'a, T: Dir> { + dir: &'a T, now: Option, separator: char, + aliases: bool, } -impl<'a> DirDisplay<'a> { - fn new(dir: &'a Dir) -> Self { - Self { dir, separator: ' ', now: None } +impl<'a, T: Dir> DirDisplay<'a, T> { + fn new(dir: &'a T) -> Self { + Self { dir, separator: ' ', now: None, aliases: false } } pub fn with_score(mut self, now: Epoch) -> Self { @@ -53,15 +106,28 @@ impl<'a> DirDisplay<'a> { self.separator = separator; self } + + pub fn with_aliases(mut self, enable: bool) -> Self { + self.aliases = enable; + self + } } -impl Display for DirDisplay<'_> { +impl<'a, T: Dir> Display for DirDisplay<'a, T> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { if let Some(now) = self.now { let score = self.dir.score(now).clamp(0.0, 9999.0); write!(f, "{score:>6.1}{}", self.separator)?; } - write!(f, "{}", self.dir.path) + + if self.aliases { + for alias in self.dir.aliases() { + write!(f, "{} ", alias.as_ref())?; + } + write!(f, "{}", self.separator)?; + } + + write!(f, "{}", self.dir.path()) } } diff --git a/src/db/mod.rs b/src/db/mod.rs index 1856fda..fc13a30 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,6 +1,7 @@ mod dir; mod stream; +use std::collections::HashSet; use std::path::{Path, PathBuf}; use std::{fs, io}; @@ -8,7 +9,8 @@ use anyhow::{Context, Result, bail}; use bincode::Options; use ouroboros::self_referencing; -pub use crate::db::dir::{Dir, Epoch, Rank}; +use crate::db::dir::{Dir, DirV3}; +pub use crate::db::dir::{DirV4, Epoch, Rank}; pub use crate::db::stream::{Stream, StreamOptions}; use crate::{config, util}; @@ -18,12 +20,13 @@ pub struct Database { bytes: Vec, #[borrows(bytes)] #[covariant] - pub dirs: Vec>, + pub dirs: Vec>, dirty: bool, } impl Database { - const VERSION: u32 = 3; + const PREV_VERSION: u32 = 3; + const VERSION: u32 = 4; pub fn open() -> Result { let data_dir = config::data_dir()?; @@ -67,10 +70,15 @@ impl Database { /// Increments the rank of a directory, or creates it if it does not exist. pub fn add(&mut self, path: impl AsRef + Into, by: Rank, now: Epoch) { self.with_dirs_mut(|dirs| match dirs.iter_mut().find(|dir| dir.path == path.as_ref()) { - Some(dir) => dir.rank = (dir.rank + by).max(0.0), - None => { - dirs.push(Dir { path: path.into().into(), rank: by.max(0.0), last_accessed: now }) + Some(dir) => { + dir.rank = (dir.rank + by).max(0.0); } + None => dirs.push(DirV4 { + path: path.into().into(), + rank: by.max(0.0), + last_accessed: now, + aliases: HashSet::new(), + }), }); self.with_dirty_mut(|dirty| *dirty = true); } @@ -81,7 +89,12 @@ impl Database { /// afterward. pub fn add_unchecked(&mut self, path: impl AsRef + Into, rank: Rank, now: Epoch) { self.with_dirs_mut(|dirs| { - dirs.push(Dir { path: path.into().into(), rank, last_accessed: now }) + dirs.push(DirV4 { + path: path.into().into(), + rank, + last_accessed: now, + aliases: HashSet::new(), + }) }); self.with_dirty_mut(|dirty| *dirty = true); } @@ -94,13 +107,50 @@ impl Database { dir.rank = (dir.rank + by).max(0.0); dir.last_accessed = now; } - None => { - dirs.push(Dir { path: path.into().into(), rank: by.max(0.0), last_accessed: now }) - } + None => dirs.push(DirV4 { + path: path.into().into(), + rank: by.max(0.0), + last_accessed: now, + aliases: HashSet::new(), + }), }); self.with_dirty_mut(|dirty| *dirty = true); } + /// Adds aliases to a directory and updates its last_accessed, or + /// creates it and adds aliases to it if it does not exist. + pub fn add_alias_update( + &mut self, + path: impl AsRef + Into, + aliases: impl Iterator + Into>, + now: Epoch, + ) { + let mut is_dirty = false; + self.with_dirs_mut(|dirs| match dirs.iter_mut().find(|dir| dir.path == path.as_ref()) { + Some(dir) => { + let starting_len = dir.aliases.len(); + dir.aliases.extend(aliases.map(|alias| alias.into().into())); + dir.last_accessed = now; + + is_dirty = dir.aliases.len() > starting_len; + } + None => { + let mut set = HashSet::new(); + set.extend(aliases.map(|alias| alias.into().into())); + + dirs.push(DirV4 { + path: path.into().into(), + rank: 0.0, + last_accessed: now, + aliases: set, + }); + + is_dirty = true; + } + }); + self.with_dirty_mut(|dirty| *dirty |= is_dirty); + } + /// Removes the directory with `path` from the store. This does not preserve /// ordering, but is O(1). pub fn remove(&mut self, path: impl AsRef) -> bool { @@ -118,6 +168,28 @@ impl Database { self.with_dirty_mut(|dirty| *dirty = true); } + /// Removes aliases from a directory + pub fn remove_alias( + &mut self, + path: impl AsRef, + aliases: impl Iterator>, + ) -> bool { + let res = self.with_dirs_mut(|dirs| { + match dirs.iter_mut().find(|dir| dir.path == path.as_ref()) { + Some(dir) => { + let mut res = false; + aliases.for_each(|alias| { + res |= dir.aliases.remove(alias.as_ref()); + }); + res + } + None => false, + } + }); + self.with_dirty_mut(|dirty| *dirty |= res); + res + } + pub fn age(&mut self, max_age: Rank) { let mut dirty = false; self.with_dirs_mut(|dirs| { @@ -173,7 +245,7 @@ impl Database { pub fn sort_by_score(&mut self, now: Epoch) { self.with_dirs_mut(|dirs| { - dirs.sort_unstable_by(|dir1: &Dir, dir2: &Dir| { + dirs.sort_unstable_by(|dir1: &DirV4, dir2: &DirV4| { dir1.score(now).total_cmp(&dir2.score(now)) }) }); @@ -184,11 +256,11 @@ impl Database { *self.borrow_dirty() } - pub fn dirs(&self) -> &[Dir<'_>] { + pub fn dirs(&self) -> &[DirV4<'_>] { self.borrow_dirs() } - fn serialize(dirs: &[Dir<'_>]) -> Result> { + fn serialize(dirs: &[DirV4<'_>]) -> Result> { (|| -> bincode::Result<_> { // Preallocate buffer with combined size of sections. let buffer_size = @@ -204,7 +276,7 @@ impl Database { .context("could not serialize database") } - fn deserialize(bytes: &[u8]) -> Result>> { + fn deserialize(bytes: &[u8]) -> Result>> { // Assume a maximum size for the database. This prevents bincode from throwing // strange errors when it encounters invalid data. const MAX_SIZE: u64 = 32 << 20; // 32 MiB @@ -223,8 +295,27 @@ impl Database { Self::VERSION => { deserializer.deserialize(bytes_dirs).context("could not deserialize database")? } + Self::PREV_VERSION => { + let old_dirs = deserializer + .deserialize::>(bytes_dirs) + .context("could not deserialize v3 database")?; + + old_dirs + .into_iter() + .map(|dir: DirV3| DirV4 { + path: dir.path, + rank: dir.rank, + last_accessed: dir.last_accessed, + aliases: HashSet::new(), + }) + .collect() + } version => { - bail!("unsupported version (got {version}, supports {})", Self::VERSION) + bail!( + "unsupported version (got {version}, supports {}, {})", + Self::VERSION, + Self::PREV_VERSION + ) } }; @@ -260,6 +351,33 @@ mod tests { } } + #[test] + fn add_alias() { + let data_dir = tempfile::tempdir().unwrap(); + let path = if cfg!(windows) { r"C:\foo\bar" } else { "/foo/bar" }; + let now = 946684800; + + { + let mut db = Database::open_dir(data_dir.path()).unwrap(); + db.add_alias_update(path, ["bar", "fb"].into_iter(), now); + db.add_alias_update(path, ["foobar"].into_iter(), now); + db.save().unwrap(); + } + + { + let db = Database::open_dir(data_dir.path()).unwrap(); + assert_eq!(db.dirs().len(), 1); + + let mut aliases = HashSet::from(["bar", "fb", "foobar"]); + let dir = &db.dirs()[0]; + assert_eq!(dir.path, path); + assert!( + dir.aliases().all(|alias| aliases.remove(alias.as_ref())) && aliases.is_empty() + ); + assert_eq!(dir.last_accessed, now); + } + } + #[test] fn remove() { let data_dir = tempfile::tempdir().unwrap(); @@ -285,4 +403,34 @@ mod tests { db.save().unwrap(); } } + + #[test] + fn remove_alias() { + let data_dir = tempfile::tempdir().unwrap(); + let path = if cfg!(windows) { r"C:\foo\bar" } else { "/foo/bar" }; + let now = 946684800; + + { + let mut db = Database::open_dir(data_dir.path()).unwrap(); + db.add_alias_update(path, ["fb", "bar", "foobar"].into_iter(), now); + db.save().unwrap(); + } + + { + let mut db = Database::open_dir(data_dir.path()).unwrap(); + assert!(db.remove_alias(path, ["bar", "foobar"].into_iter())); + db.save().unwrap(); + } + + { + let mut db = Database::open_dir(data_dir.path()).unwrap(); + let mut aliases = HashSet::from(["fb"]); + assert_eq!(db.dirs().len(), 1); + assert!( + db.dirs()[0].aliases().all(|alias| aliases.remove(alias.as_ref())) + && aliases.is_empty() + ); + db.save().unwrap(); + } + } } diff --git a/src/db/stream.rs b/src/db/stream.rs index 24c84e0..e68046c 100644 --- a/src/db/stream.rs +++ b/src/db/stream.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; +use std::collections::HashSet; use std::iter::Rev; use std::ops::Range; use std::path::Path; @@ -5,7 +7,7 @@ use std::{fs, path}; use glob::Pattern; -use crate::db::{Database, Dir, Epoch}; +use crate::db::{Database, DirV4, Epoch}; use crate::util::{self, MONTH}; pub struct Stream<'a> { @@ -21,29 +23,32 @@ impl<'a> Stream<'a> { Stream { db, idxs, options } } - pub fn next(&mut self) -> Option<&Dir<'_>> { + pub fn next(&mut self) -> Option<&DirV4<'_>> { while let Some(idx) = self.idxs.next() { let dir = &self.db.dirs()[idx]; - if !self.filter_by_keywords(&dir.path) { - continue; - } - - if !self.filter_by_base_dir(&dir.path) { - continue; - } - - if !self.filter_by_exclude(&dir.path) { - self.db.swap_remove(idx); - continue; - } - - // Exists queries are slow, this should always be checked last. - if !self.filter_by_exists(&dir.path) { - if dir.last_accessed < self.options.ttl { - self.db.swap_remove(idx); + // Return dir if any keyword is an alias + if !self.match_aliases(&dir.aliases) { + if !self.filter_by_keywords(&dir.path) { + continue; + } + + if !self.filter_by_base_dir(&dir.path) { + continue; + } + + if !self.filter_by_exclude(&dir.path) { + self.db.swap_remove(idx); + continue; + } + + // Exists queries are slow, this should always be checked last. + if !self.filter_by_exists(&dir.path) { + if dir.last_accessed < self.options.ttl { + self.db.swap_remove(idx); + } + continue; } - continue; } let dir = &self.db.dirs()[idx]; @@ -104,6 +109,16 @@ impl<'a> Stream<'a> { true } + + fn match_aliases(&self, aliases: &HashSet>) -> bool { + for keyword in &self.options.keywords { + if aliases.contains(keyword.as_str()) { + return true; + } + } + + false + } } pub struct StreamOptions { @@ -202,10 +217,32 @@ mod tests { #[case(&["foo", "o", "bar"], "/foo/bar", false)] #[case(&["/foo/", "/bar"], "/foo/bar", false)] #[case(&["/foo/", "/bar"], "/foo/baz/bar", true)] + // Aliases + // Case normalization + #[case(&["fOo", "BaR"], "ALIASES=foo,bar", true)] + #[case(&["foo", "BaR"], "ALIASES=foo,bar", true)] + // Exact matches + #[case(&["fo", "ar"], "ALIASES=foo,bar", false)] + #[case(&["foo", "bar"], "ALIASES=foo,bar", true)] + // Mixed aliases and paths + #[case(&["/foo/", "bar", "/baz"], "ALIASES=foo,bar", true)] fn query(#[case] keywords: &[&str], #[case] path: &str, #[case] is_match: bool) { let db = &mut Database::new(PathBuf::new(), Vec::new(), |_| Vec::new(), false); let options = StreamOptions::new(0).with_keywords(keywords.iter()); let stream = Stream::new(db, options); - assert_eq!(is_match, stream.filter_by_keywords(path)); + assert_eq!( + is_match, + if path.starts_with("ALIASES=") { + stream.match_aliases( + &path + .trim_start_matches("ALIASES=") + .split(",") + .map(Cow::Borrowed) + .collect::>>(), + ) + } else { + stream.filter_by_keywords(path) + } + ); } } diff --git a/src/import.rs b/src/import.rs index b68f564..2eda0bc 100644 --- a/src/import.rs +++ b/src/import.rs @@ -18,7 +18,7 @@ use std::path::PathBuf; use anyhow::Result; use crate::config; -use crate::db::{Database, Dir}; +use crate::db::{Database, DirV4}; pub(crate) trait Importer { /// Yields directory entries to be imported. @@ -26,7 +26,7 @@ pub(crate) trait Importer { /// The outer `Result` reports failure to fetch the input (e.g. missing /// file, subprocess errored). The per-item `Result` reports a malformed /// row, which doesn't necessarily abort the whole import. - fn dirs(&self) -> Result, ImportError>>>; + fn dirs(&self) -> Result, ImportError>>>; } /// A single record that failed to import. diff --git a/src/import/atuin.rs b/src/import/atuin.rs index 513efe5..310e850 100644 --- a/src/import/atuin.rs +++ b/src/import/atuin.rs @@ -1,18 +1,19 @@ use std::borrow::Cow; +use std::collections::HashSet; use std::io::{BufRead, BufReader}; use std::process::{Child, ChildStdout, Command, Stdio}; use std::str; use anyhow::{Context, Result, anyhow}; -use crate::db::{Dir, Epoch}; +use crate::db::{DirV4, Epoch}; use crate::import::{ImportError, Importer}; #[derive(clap::Args, Clone, Debug)] pub(crate) struct Atuin {} impl Importer for Atuin { - fn dirs(&self) -> Result, ImportError>>> { + fn dirs(&self) -> Result, ImportError>>> { // atuin renders `{time}` as `YYYY-MM-DD HH:MM:SS` in UTC. let mut child = Command::new("atuin") .args(["history", "list", "--format={time}\t{directory}", "--print0"]) @@ -46,7 +47,7 @@ impl Iter { ImportError { path: None, line_num: self.line_num, source } } - fn parse_line(&self, line: &[u8]) -> Result, ImportError> { + fn parse_line(&self, line: &[u8]) -> Result, ImportError> { let line = str::from_utf8(line).map_err(|e| self.err(anyhow!(e).context("invalid utf-8")))?; @@ -60,17 +61,18 @@ impl Iter { .assume_utc() .unix_timestamp(); - let dir = Dir { + let dir = DirV4 { path: Cow::Owned(path.to_string()), rank: 1.0, last_accessed: timestamp as Epoch, + aliases: HashSet::new(), }; Ok(dir) } } impl Iterator for Iter { - type Item = Result, ImportError>; + type Item = Result, ImportError>; fn next(&mut self) -> Option { loop { diff --git a/src/import/autojump.rs b/src/import/autojump.rs index f269e1f..df59fb9 100644 --- a/src/import/autojump.rs +++ b/src/import/autojump.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::collections::HashSet; use std::fs::File; use std::io::{BufRead, BufReader}; use std::path::PathBuf; @@ -6,14 +7,14 @@ use std::{env, str}; use anyhow::{Context, Result, anyhow}; -use crate::db::Dir; +use crate::db::DirV4; use crate::import::{ImportError, Importer}; #[derive(clap::Args, Clone, Debug)] pub(crate) struct Autojump {} impl Importer for Autojump { - fn dirs(&self) -> Result, ImportError>>> { + fn dirs(&self) -> Result, ImportError>>> { let path = data_path()?; let file = File::open(&path).with_context(|| format!("could not read {path:?}"))?; let reader = BufReader::new(file); @@ -37,7 +38,7 @@ impl Iter { ImportError { path: Some(self.path.clone()), line_num: self.line_num, source } } - fn parse_line(&self, line: &[u8]) -> Result, ImportError> { + fn parse_line(&self, line: &[u8]) -> Result, ImportError> { let line = str::from_utf8(line).map_err(|e| self.err(anyhow!(e).context("invalid utf-8")))?; @@ -52,12 +53,17 @@ impl Iter { // take a while to normalize. let rank = sigmoid(rank); - Ok(Dir { path: Cow::Owned(path.to_string()), rank, last_accessed: 0 }) + Ok(DirV4 { + path: Cow::Owned(path.to_string()), + rank, + last_accessed: 0, + aliases: HashSet::new(), + }) } } impl Iterator for Iter { - type Item = Result, ImportError>; + type Item = Result, ImportError>; fn next(&mut self) -> Option { loop { diff --git a/src/import/fasd.rs b/src/import/fasd.rs index ab0788d..1dba739 100644 --- a/src/import/fasd.rs +++ b/src/import/fasd.rs @@ -5,14 +5,14 @@ use std::path::PathBuf; use anyhow::{Context, Result}; -use crate::db::Dir; +use crate::db::DirV4; use crate::import::{ImportError, Importer, z}; #[derive(clap::Args, Clone, Debug)] pub(crate) struct Fasd {} impl Importer for Fasd { - fn dirs(&self) -> Result, ImportError>>> { + fn dirs(&self) -> Result, ImportError>>> { let path = data_path()?; let file = File::open(&path).with_context(|| format!("could not read {path:?}"))?; let reader = BufReader::new(file); diff --git a/src/import/z.rs b/src/import/z.rs index 511980c..43e51c5 100644 --- a/src/import/z.rs +++ b/src/import/z.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::collections::HashSet; use std::fs::File; use std::io::{BufRead, BufReader}; use std::path::PathBuf; @@ -6,14 +7,14 @@ use std::{env, str}; use anyhow::{Context, Result, anyhow}; -use crate::db::Dir; +use crate::db::DirV4; use crate::import::{ImportError, Importer}; #[derive(clap::Args, Clone, Debug)] pub(crate) struct Z {} impl Importer for Z { - fn dirs(&self) -> Result, ImportError>>> { + fn dirs(&self) -> Result, ImportError>>> { let path = data_path()?; let file = File::open(&path).with_context(|| format!("could not read {path:?}"))?; let reader = BufReader::new(file); @@ -37,7 +38,7 @@ impl Iter { ImportError { path: Some(self.path.clone()), line_num: self.line_num, source } } - fn parse_line(&self, line: &[u8]) -> Result, ImportError> { + fn parse_line(&self, line: &[u8]) -> Result, ImportError> { let line = str::from_utf8(line).map_err(|e| self.err(anyhow!(e).context("invalid utf-8")))?; let err = || self.err(anyhow!("invalid entry: {line}")); @@ -54,12 +55,17 @@ impl Iter { let path = split.next().ok_or_else(err)?; - Ok(Dir { path: Cow::Owned(path.to_string()), rank, last_accessed }) + Ok(DirV4 { + path: Cow::Owned(path.to_string()), + rank, + last_accessed, + aliases: HashSet::new(), + }) } } impl Iterator for Iter { - type Item = Result, ImportError>; + type Item = Result, ImportError>; fn next(&mut self) -> Option { loop { diff --git a/src/import/z_lua.rs b/src/import/z_lua.rs index 0ea4584..364c9db 100644 --- a/src/import/z_lua.rs +++ b/src/import/z_lua.rs @@ -6,14 +6,14 @@ use std::path::PathBuf; use anyhow::{Context, Result}; -use crate::db::Dir; +use crate::db::DirV4; use crate::import::{ImportError, Importer, z}; #[derive(clap::Args, Clone, Debug)] pub(crate) struct ZLua {} impl Importer for ZLua { - fn dirs(&self) -> Result, ImportError>>> { + fn dirs(&self) -> Result, ImportError>>> { let path = data_path()?; let err = match File::open(&path) { Ok(file) => return Ok(z::Iter::new(BufReader::new(file), path)), diff --git a/src/import/zsh_z.rs b/src/import/zsh_z.rs index 652faf8..7d3474c 100644 --- a/src/import/zsh_z.rs +++ b/src/import/zsh_z.rs @@ -5,14 +5,14 @@ use std::path::PathBuf; use anyhow::{Context, Result}; -use crate::db::Dir; +use crate::db::DirV4; use crate::import::{ImportError, Importer, z}; #[derive(clap::Args, Clone, Debug)] pub(crate) struct ZshZ {} impl Importer for ZshZ { - fn dirs(&self) -> Result, ImportError>>> { + fn dirs(&self) -> Result, ImportError>>> { let path = data_path()?; let file = File::open(&path).with_context(|| format!("could not read {path:?}"))?; let reader = BufReader::new(file); diff --git a/src/util.rs b/src/util.rs index 4c0a27c..3579323 100644 --- a/src/util.rs +++ b/src/util.rs @@ -10,7 +10,7 @@ use std::{env, mem}; use anyhow::anyhow; use anyhow::{Context, Result, bail}; -use crate::db::{Dir, Epoch}; +use crate::db::{DirV4, Epoch}; use crate::error::SilentExit; pub const SECOND: Epoch = 1; @@ -41,7 +41,7 @@ impl Fzf { cmd.args([ // Search mode "--delimiter=\t", - "--nth=2", + "--nth=2,3", // Scripting "--read0", ]) @@ -60,9 +60,9 @@ impl Fzf { self.args([ // Non-POSIX args are only available on certain operating systems. if cfg!(target_os = "linux") { - r"--preview=\command -p ls -Cp --color=always --group-directories-first {2..}" + r"--preview=\command -p ls -Cp --color=always --group-directories-first {3..}" } else { - r"--preview=\command -p ls -Cp {2..}" + r"--preview=\command -p ls -Cp {3..}" }, // Rounded edges don't display correctly on some terminals. "--preview-window=down,30%,sharp", @@ -121,9 +121,13 @@ impl Fzf { pub struct FzfChild(Child); impl FzfChild { - pub fn write(&mut self, dir: &Dir, now: Epoch) -> Result> { + pub fn write(&mut self, dir: &DirV4, now: Epoch, aliases: bool) -> Result> { let handle = self.0.stdin.as_mut().unwrap(); - match write!(handle, "{}\0", dir.display().with_score(now).with_separator('\t')) { + match write!( + handle, + "{}\0", + dir.display().with_score(now).with_aliases(aliases).with_separator('\t') + ) { Ok(()) => Ok(None), Err(e) if e.kind() == io::ErrorKind::BrokenPipe => self.wait().map(Some), Err(e) => Err(e).context("could not write to fzf"),