Merge 88aac1850f into ee594e6906
This commit is contained in:
commit
9093d282ab
|
|
@ -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 "$@"
|
||||
|
|
|
|||
|
|
@ -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*" } |
|
||||
|
|
|
|||
|
|
@ -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 <ALIASES>..."
|
||||
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 <ALIASES>..."
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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 *
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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<f64>,
|
||||
}
|
||||
|
||||
/// 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<String>,
|
||||
|
||||
/// 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<String>,
|
||||
|
|
@ -209,3 +230,18 @@ pub struct Remove {
|
|||
#[clap(value_hint = ValueHint::DirPath)]
|
||||
pub paths: Vec<String>,
|
||||
}
|
||||
|
||||
/// 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<String>,
|
||||
|
||||
/// Path to remove aliases from
|
||||
#[clap(short, long, required = true, value_hint = ValueHint::DirPath)]
|
||||
pub path: String,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Cow<'a, str>>,
|
||||
}
|
||||
|
||||
#[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<Item = impl AsRef<str>>;
|
||||
}
|
||||
|
||||
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<Item = impl AsRef<str>> {
|
||||
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<Item = impl AsRef<str>> {
|
||||
let arr: &[&str] = &[];
|
||||
arr.iter()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DirDisplay<'a, T: Dir> {
|
||||
dir: &'a T,
|
||||
now: Option<Epoch>,
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
178
src/db/mod.rs
178
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<u8>,
|
||||
#[borrows(bytes)]
|
||||
#[covariant]
|
||||
pub dirs: Vec<Dir<'this>>,
|
||||
pub dirs: Vec<DirV4<'this>>,
|
||||
dirty: bool,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
const VERSION: u32 = 3;
|
||||
const PREV_VERSION: u32 = 3;
|
||||
const VERSION: u32 = 4;
|
||||
|
||||
pub fn open() -> Result<Self> {
|
||||
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<str> + Into<String>, 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<str> + Into<String>, 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<str> + Into<String>,
|
||||
aliases: impl Iterator<Item = impl AsRef<str> + Into<String>>,
|
||||
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<str>) -> 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<str>,
|
||||
aliases: impl Iterator<Item = impl AsRef<str>>,
|
||||
) -> 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<Vec<u8>> {
|
||||
fn serialize(dirs: &[DirV4<'_>]) -> Result<Vec<u8>> {
|
||||
(|| -> 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<Vec<Dir<'_>>> {
|
||||
fn deserialize(bytes: &[u8]) -> Result<Vec<DirV4<'_>>> {
|
||||
// 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::<Vec<DirV3>>(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Cow<'a, str>>) -> 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::<HashSet<Cow<'_, str>>>(),
|
||||
)
|
||||
} else {
|
||||
stream.filter_by_keywords(path)
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<impl Iterator<Item = Result<Dir<'static>, ImportError>>>;
|
||||
fn dirs(&self) -> Result<impl Iterator<Item = Result<DirV4<'static>, ImportError>>>;
|
||||
}
|
||||
|
||||
/// A single record that failed to import.
|
||||
|
|
|
|||
|
|
@ -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<impl Iterator<Item = Result<Dir<'static>, ImportError>>> {
|
||||
fn dirs(&self) -> Result<impl Iterator<Item = Result<DirV4<'static>, 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<Dir<'static>, ImportError> {
|
||||
fn parse_line(&self, line: &[u8]) -> Result<DirV4<'static>, 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<Dir<'static>, ImportError>;
|
||||
type Item = Result<DirV4<'static>, ImportError>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
|
|
|
|||
|
|
@ -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<impl Iterator<Item = Result<Dir<'static>, ImportError>>> {
|
||||
fn dirs(&self) -> Result<impl Iterator<Item = Result<DirV4<'static>, 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<R: BufRead> Iter<R> {
|
|||
ImportError { path: Some(self.path.clone()), line_num: self.line_num, source }
|
||||
}
|
||||
|
||||
fn parse_line(&self, line: &[u8]) -> Result<Dir<'static>, ImportError> {
|
||||
fn parse_line(&self, line: &[u8]) -> Result<DirV4<'static>, ImportError> {
|
||||
let line =
|
||||
str::from_utf8(line).map_err(|e| self.err(anyhow!(e).context("invalid utf-8")))?;
|
||||
|
||||
|
|
@ -52,12 +53,17 @@ impl<R: BufRead> Iter<R> {
|
|||
// 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<R: BufRead> Iterator for Iter<R> {
|
||||
type Item = Result<Dir<'static>, ImportError>;
|
||||
type Item = Result<DirV4<'static>, ImportError>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
|
|
|
|||
|
|
@ -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<impl Iterator<Item = Result<Dir<'static>, ImportError>>> {
|
||||
fn dirs(&self) -> Result<impl Iterator<Item = Result<DirV4<'static>, ImportError>>> {
|
||||
let path = data_path()?;
|
||||
let file = File::open(&path).with_context(|| format!("could not read {path:?}"))?;
|
||||
let reader = BufReader::new(file);
|
||||
|
|
|
|||
|
|
@ -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<impl Iterator<Item = Result<Dir<'static>, ImportError>>> {
|
||||
fn dirs(&self) -> Result<impl Iterator<Item = Result<DirV4<'static>, 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<R: BufRead> Iter<R> {
|
|||
ImportError { path: Some(self.path.clone()), line_num: self.line_num, source }
|
||||
}
|
||||
|
||||
fn parse_line(&self, line: &[u8]) -> Result<Dir<'static>, ImportError> {
|
||||
fn parse_line(&self, line: &[u8]) -> Result<DirV4<'static>, 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<R: BufRead> Iter<R> {
|
|||
|
||||
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<R: BufRead> Iterator for Iter<R> {
|
||||
type Item = Result<Dir<'static>, ImportError>;
|
||||
type Item = Result<DirV4<'static>, ImportError>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
|
|
|
|||
|
|
@ -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<impl Iterator<Item = Result<Dir<'static>, ImportError>>> {
|
||||
fn dirs(&self) -> Result<impl Iterator<Item = Result<DirV4<'static>, ImportError>>> {
|
||||
let path = data_path()?;
|
||||
let err = match File::open(&path) {
|
||||
Ok(file) => return Ok(z::Iter::new(BufReader::new(file), path)),
|
||||
|
|
|
|||
|
|
@ -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<impl Iterator<Item = Result<Dir<'static>, ImportError>>> {
|
||||
fn dirs(&self) -> Result<impl Iterator<Item = Result<DirV4<'static>, ImportError>>> {
|
||||
let path = data_path()?;
|
||||
let file = File::open(&path).with_context(|| format!("could not read {path:?}"))?;
|
||||
let reader = BufReader::new(file);
|
||||
|
|
|
|||
16
src/util.rs
16
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<Option<String>> {
|
||||
pub fn write(&mut self, dir: &DirV4, now: Epoch, aliases: bool) -> Result<Option<String>> {
|
||||
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"),
|
||||
|
|
|
|||
Loading…
Reference in New Issue