Add scd plugin for smart change of directory.
Synced with the scd-tracker branch pavoljuhas/oh-my-zsh@2f78243cad.
This commit is contained in:
parent
6952105bfe
commit
c1c107cfb9
@ -111,8 +111,7 @@ SCD_MEANLIFE</dt><dd>
|
|||||||
|
|
||||||
SCD_THRESHOLD</dt><dd>
|
SCD_THRESHOLD</dt><dd>
|
||||||
threshold for cumulative directory likelihood. Directories with
|
threshold for cumulative directory likelihood. Directories with
|
||||||
lower likelihood are excluded unless they are the only match to
|
a lower likelihood compared to the best match are excluded (0.005).
|
||||||
scd patterns.
|
|
||||||
</dd><dt>
|
</dd><dt>
|
||||||
|
|
||||||
SCD_SCRIPT</dt><dd>
|
SCD_SCRIPT</dt><dd>
|
||||||
|
267
plugins/scd/scd
267
plugins/scd/scd
@ -1,10 +1,11 @@
|
|||||||
#!/bin/zsh -f
|
#!/bin/zsh -f
|
||||||
|
|
||||||
emulate -L zsh
|
emulate -L zsh
|
||||||
|
local EXIT=return
|
||||||
if [[ $(whence -w $0) == *:' 'command ]]; then
|
if [[ $(whence -w $0) == *:' 'command ]]; then
|
||||||
emulate -R zsh
|
emulate -R zsh
|
||||||
alias return=exit
|
|
||||||
local RUNNING_AS_COMMAND=1
|
local RUNNING_AS_COMMAND=1
|
||||||
|
EXIT=exit
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local DOC='scd -- smart change to a recently used directory
|
local DOC='scd -- smart change to a recently used directory
|
||||||
@ -37,8 +38,9 @@ local SCD_ALIAS=~/.scdalias.zsh
|
|||||||
local ICASE a d m p i tdir maxrank threshold
|
local ICASE a d m p i tdir maxrank threshold
|
||||||
local opt_help opt_add opt_unindex opt_recursive opt_verbose
|
local opt_help opt_add opt_unindex opt_recursive opt_verbose
|
||||||
local opt_alias opt_unalias opt_list
|
local opt_alias opt_unalias opt_list
|
||||||
local -A drank dalias dkey
|
local -A drank dalias
|
||||||
local dmatching
|
local dmatching
|
||||||
|
local last_directory
|
||||||
|
|
||||||
setopt extendedhistory extendedglob noautonamedirs brace_ccl
|
setopt extendedhistory extendedglob noautonamedirs brace_ccl
|
||||||
|
|
||||||
@ -56,11 +58,11 @@ zparseopts -D -- a=opt_add -add=opt_add -unindex=opt_unindex \
|
|||||||
r=opt_recursive -recursive=opt_recursive \
|
r=opt_recursive -recursive=opt_recursive \
|
||||||
-alias:=opt_alias -unalias=opt_unalias -list=opt_list \
|
-alias:=opt_alias -unalias=opt_unalias -list=opt_list \
|
||||||
v=opt_verbose -verbose=opt_verbose h=opt_help -help=opt_help \
|
v=opt_verbose -verbose=opt_verbose h=opt_help -help=opt_help \
|
||||||
|| return $?
|
|| $EXIT $?
|
||||||
|
|
||||||
if [[ -n $opt_help ]]; then
|
if [[ -n $opt_help ]]; then
|
||||||
print $DOC
|
print $DOC
|
||||||
return
|
$EXIT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# load directory aliases if they exist
|
# load directory aliases if they exist
|
||||||
@ -79,8 +81,8 @@ _scd_Y19oug_abspath() {
|
|||||||
# define directory alias
|
# define directory alias
|
||||||
if [[ -n $opt_alias ]]; then
|
if [[ -n $opt_alias ]]; then
|
||||||
if [[ -n $1 && ! -d $1 ]]; then
|
if [[ -n $1 && ! -d $1 ]]; then
|
||||||
print -u2 "'$1' is not a directory"
|
print -u2 "'$1' is not a directory."
|
||||||
return 1
|
$EXIT 1
|
||||||
fi
|
fi
|
||||||
a=${opt_alias[-1]#=}
|
a=${opt_alias[-1]#=}
|
||||||
_scd_Y19oug_abspath d ${1:-$PWD}
|
_scd_Y19oug_abspath d ${1:-$PWD}
|
||||||
@ -93,19 +95,19 @@ if [[ -n $opt_alias ]]; then
|
|||||||
hash -d -- $a=$d
|
hash -d -- $a=$d
|
||||||
hash -dL >| $SCD_ALIAS
|
hash -dL >| $SCD_ALIAS
|
||||||
)
|
)
|
||||||
return $?
|
$EXIT $?
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# undefine directory alias
|
# undefine directory alias
|
||||||
if [[ -n $opt_unalias ]]; then
|
if [[ -n $opt_unalias ]]; then
|
||||||
if [[ -n $1 && ! -d $1 ]]; then
|
if [[ -n $1 && ! -d $1 ]]; then
|
||||||
print -u2 "'$1' is not a directory"
|
print -u2 "'$1' is not a directory."
|
||||||
return 1
|
$EXIT 1
|
||||||
fi
|
fi
|
||||||
_scd_Y19oug_abspath a ${1:-$PWD}
|
_scd_Y19oug_abspath a ${1:-$PWD}
|
||||||
a=$(print -rD ${a})
|
a=$(print -rD ${a})
|
||||||
if [[ $a != [~][^/]## ]]; then
|
if [[ $a != [~][^/]## ]]; then
|
||||||
return
|
$EXIT
|
||||||
fi
|
fi
|
||||||
a=${a#[~]}
|
a=${a#[~]}
|
||||||
# unalias in the current shell, update alias file if successful
|
# unalias in the current shell, update alias file if successful
|
||||||
@ -118,35 +120,39 @@ if [[ -n $opt_unalias ]]; then
|
|||||||
hash -dL >| $SCD_ALIAS
|
hash -dL >| $SCD_ALIAS
|
||||||
)
|
)
|
||||||
fi
|
fi
|
||||||
return $?
|
$EXIT $?
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Rewrite the history file if it is at least 20% oversized
|
# Rewrite directory index if it is at least 20% oversized
|
||||||
if [[ -s $SCD_HISTFILE ]] && \
|
if [[ -s $SCD_HISTFILE ]] && \
|
||||||
(( $(wc -l <$SCD_HISTFILE) > 1.2 * $SCD_HISTSIZE )); then
|
(( $(wc -l <$SCD_HISTFILE) > 1.2 * $SCD_HISTSIZE )); then
|
||||||
m=( ${(f)"$(<$SCD_HISTFILE)"} )
|
m=( ${(f)"$(<$SCD_HISTFILE)"} )
|
||||||
print -lr -- ${m[-$SCD_HISTSIZE,-1]} >| ${SCD_HISTFILE}
|
print -lr -- ${m[-$SCD_HISTSIZE,-1]} >| ${SCD_HISTFILE}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Determine the last recorded directory
|
||||||
|
if [[ -s ${SCD_HISTFILE} ]]; then
|
||||||
|
last_directory=${"$(tail -1 ${SCD_HISTFILE})"#*;}
|
||||||
|
fi
|
||||||
|
|
||||||
# Internal functions are prefixed with "_scd_Y19oug_".
|
# Internal functions are prefixed with "_scd_Y19oug_".
|
||||||
# The "record" function adds a non-repeating directory to the history
|
# The "record" function adds its arguments to the directory index.
|
||||||
# and turns on history writing.
|
|
||||||
_scd_Y19oug_record() {
|
_scd_Y19oug_record() {
|
||||||
while [[ -n $1 && $1 == ${history[$HISTCMD]} ]]; do
|
while [[ -n $last_directory && $1 == $last_directory ]]; do
|
||||||
shift
|
shift
|
||||||
done
|
done
|
||||||
if [[ $# != 0 ]]; then
|
if [[ $# -gt 0 ]]; then
|
||||||
( umask 077; : >>| $SCD_HISTFILE )
|
( umask 077
|
||||||
p=": ${EPOCHSECONDS}:0;"
|
p=": ${EPOCHSECONDS}:0;"
|
||||||
print -lr -- ${p}${^*} >> $SCD_HISTFILE
|
print -lr -- ${p}${^*} >>| $SCD_HISTFILE )
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ -n $opt_add ]]; then
|
if [[ -n $opt_add ]]; then
|
||||||
for a; do
|
for d; do
|
||||||
if [[ ! -d $a ]]; then
|
if [[ ! -d $d ]]; then
|
||||||
print -u 2 "Directory $a does not exist"
|
print -u2 "Directory '$d' does not exist."
|
||||||
return 2
|
$EXIT 2
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
_scd_Y19oug_abspath m ${*:-$PWD}
|
_scd_Y19oug_abspath m ${*:-$PWD}
|
||||||
@ -158,13 +164,13 @@ if [[ -n $opt_add ]]; then
|
|||||||
print "[done]"
|
print "[done]"
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
return
|
$EXIT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# take care of removing entries from the directory index
|
# take care of removing entries from the directory index
|
||||||
if [[ -n $opt_unindex ]]; then
|
if [[ -n $opt_unindex ]]; then
|
||||||
if [[ ! -s $SCD_HISTFILE ]]; then
|
if [[ ! -s $SCD_HISTFILE ]]; then
|
||||||
return
|
$EXIT
|
||||||
fi
|
fi
|
||||||
# expand existing directories in the argument list
|
# expand existing directories in the argument list
|
||||||
for i in {1..$#}; do
|
for i in {1..$#}; do
|
||||||
@ -190,51 +196,41 @@ if [[ -n $opt_unindex ]]; then
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
{ print $0 }
|
{ print $0 }
|
||||||
' $SCD_HISTFILE ${*:-$PWD} )" || return $?
|
' $SCD_HISTFILE ${*:-$PWD} )" || $EXIT $?
|
||||||
: >| ${SCD_HISTFILE}
|
: >| ${SCD_HISTFILE}
|
||||||
[[ ${#m} == 0 ]] || print -r -- $m >> ${SCD_HISTFILE}
|
[[ ${#m} == 0 ]] || print -r -- $m >> ${SCD_HISTFILE}
|
||||||
return
|
$EXIT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# The "action" function is called when there is just one target directory.
|
# The "action" function is called when there is just one target directory.
|
||||||
_scd_Y19oug_action() {
|
_scd_Y19oug_action() {
|
||||||
if [[ -n $opt_list ]]; then
|
cd $1 || return $?
|
||||||
for d; do
|
|
||||||
a=${(k)dalias[(r)${d}]}
|
|
||||||
print -r -- "# $a"
|
|
||||||
print -r -- $d
|
|
||||||
done
|
|
||||||
elif [[ $# == 1 ]]; then
|
|
||||||
if [[ -z $SCD_SCRIPT && -n $RUNNING_AS_COMMAND ]]; then
|
if [[ -z $SCD_SCRIPT && -n $RUNNING_AS_COMMAND ]]; then
|
||||||
print -u2 "Warning: running as command with SCD_SCRIPT undefined."
|
print -u2 "Warning: running as command with SCD_SCRIPT undefined."
|
||||||
fi
|
fi
|
||||||
[[ -n $SCD_SCRIPT ]] && (umask 077;
|
if [[ -n $SCD_SCRIPT ]]; then
|
||||||
print -r "cd ${(q)1}" >| $SCD_SCRIPT)
|
print -r "cd ${(q)1}" >| $SCD_SCRIPT
|
||||||
[[ -N $SCD_HISTFILE ]] && touch -a $SCD_HISTFILE
|
|
||||||
cd $1
|
|
||||||
# record the new directory unless already done in some chpwd hook
|
|
||||||
[[ -N $SCD_HISTFILE ]] || _scd_Y19oug_record $PWD
|
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# handle different argument scenarios ----------------------------------------
|
# Match and rank patterns to the index file
|
||||||
|
# set global arrays dmatching and drank
|
||||||
|
_scd_Y19oug_match() {
|
||||||
|
## single argument that is an existing directory or directory alias
|
||||||
|
if [[ $# == 1 ]] && \
|
||||||
|
[[ -d ${d::=$1} || -d ${d::=${nameddirs[$1]}} ]] && [[ -x $d ]];
|
||||||
|
then
|
||||||
|
_scd_Y19oug_abspath dmatching $d
|
||||||
|
drank[${dmatching[1]}]=1
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
## single argument that is an existing directory
|
# ignore case unless there is an argument with an uppercase letter
|
||||||
if [[ $# == 1 && -d $1 && -x $1 ]]; then
|
[[ "$*" == *[[:upper:]]* ]] || ICASE='(#i)'
|
||||||
_scd_Y19oug_action $1
|
|
||||||
return $?
|
|
||||||
## single argument that is an alias
|
|
||||||
elif [[ $# == 1 && -d ${d::=${nameddirs[$1]}} ]]; then
|
|
||||||
_scd_Y19oug_action $d
|
|
||||||
return $?
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ignore case unless there is an argument with an uppercase letter
|
# calculate rank of all directories in the SCD_HISTFILE and keep it as drank
|
||||||
[[ "$*" == *[[:upper:]]* ]] || ICASE='(#i)'
|
# include a dummy entry for splitting of an empty string is buggy
|
||||||
|
[[ -s $SCD_HISTFILE ]] && drank=( ${(f)"$(
|
||||||
# calculate rank of all directories in the SCD_HISTFILE and keep it as drank
|
|
||||||
# include a dummy entry for splitting of an empty string is buggy
|
|
||||||
[[ -s $SCD_HISTFILE ]] && drank=( ${(f)"$(
|
|
||||||
print -l /dev/null -10
|
print -l /dev/null -10
|
||||||
<$SCD_HISTFILE \
|
<$SCD_HISTFILE \
|
||||||
awk -v epochseconds=$EPOCHSECONDS -v meanlife=$SCD_MEANLIFE '
|
awk -v epochseconds=$EPOCHSECONDS -v meanlife=$SCD_MEANLIFE '
|
||||||
@ -248,103 +244,110 @@ fi
|
|||||||
}
|
}
|
||||||
END { for (di in ptot) { print di; print ptot[di]; } }'
|
END { for (di in ptot) { print di; print ptot[di]; } }'
|
||||||
)"}
|
)"}
|
||||||
)
|
)
|
||||||
unset "drank[/dev/null]"
|
unset "drank[/dev/null]"
|
||||||
|
|
||||||
# filter drank to the entries that match all arguments
|
# filter drank to the entries that match all arguments
|
||||||
for a; do
|
for a; do
|
||||||
p=${ICASE}"*${a}*"
|
p=${ICASE}"*${a}*"
|
||||||
drank=( ${(kv)drank[(I)${~p}]} )
|
drank=( ${(kv)drank[(I)${~p}]} )
|
||||||
done
|
done
|
||||||
|
|
||||||
# build a list of matching directories reverse-sorted by their probabilities
|
# build a list of matching directories reverse-sorted by their probabilities
|
||||||
dmatching=( ${(f)"$(
|
dmatching=( ${(f)"$(
|
||||||
for d p in ${(kv)drank}; do
|
for d p in ${(kv)drank}; do
|
||||||
print -r -- "$p $d";
|
print -r -- "$p $d";
|
||||||
done | sort -grk1 | cut -d ' ' -f 2-
|
done | sort -grk1 | cut -d ' ' -f 2-
|
||||||
)"}
|
)"}
|
||||||
)
|
)
|
||||||
|
|
||||||
# if some directory paths match all patterns in order, discard all others
|
# if some directory paths match all patterns in order, discard all others
|
||||||
p=${ICASE}"*${(j:*:)argv}*"
|
p=${ICASE}"*${(j:*:)argv}*"
|
||||||
m=( ${(M)dmatching:#${~p}} )
|
m=( ${(M)dmatching:#${~p}} )
|
||||||
[[ -d ${m[1]} ]] && dmatching=( $m )
|
[[ -d ${m[1]} ]] && dmatching=( $m )
|
||||||
# if some directory names match last pattern, discard all others
|
# if some directory names match last pattern, discard all others
|
||||||
p=${ICASE}"*${(j:*:)argv}[^/]#"
|
p=${ICASE}"*${(j:*:)argv}[^/]#"
|
||||||
m=( ${(M)dmatching:#${~p}} )
|
m=( ${(M)dmatching:#${~p}} )
|
||||||
[[ -d ${m[1]} ]] && dmatching=( $m )
|
[[ -d ${m[1]} ]] && dmatching=( $m )
|
||||||
# if some directory names match all patterns, discard all others
|
# if some directory names match all patterns, discard all others
|
||||||
m=( $dmatching )
|
m=( $dmatching )
|
||||||
for a; do
|
for a; do
|
||||||
p=${ICASE}"*/[^/]#${a}[^/]#"
|
p=${ICASE}"*/[^/]#${a}[^/]#"
|
||||||
m=( ${(M)m:#${~p}} )
|
m=( ${(M)m:#${~p}} )
|
||||||
done
|
done
|
||||||
[[ -d ${m[1]} ]] && dmatching=( $m )
|
[[ -d ${m[1]} ]] && dmatching=( $m )
|
||||||
# if some directory names match all patterns in order, discard all others
|
# if some directory names match all patterns in order, discard all others
|
||||||
p=${ICASE}"/*${(j:[^/]#:)argv}[^/]#"
|
p=${ICASE}"/*${(j:[^/]#:)argv}[^/]#"
|
||||||
m=( ${(M)dmatching:#${~p}} )
|
m=( ${(M)dmatching:#${~p}} )
|
||||||
[[ -d ${m[1]} ]] && dmatching=( $m )
|
[[ -d ${m[1]} ]] && dmatching=( $m )
|
||||||
|
|
||||||
# do not match $HOME or $PWD when run without arguments
|
# do not match $HOME or $PWD when run without arguments
|
||||||
if [[ $# == 0 ]]; then
|
if [[ $# == 0 ]]; then
|
||||||
dmatching=( ${dmatching:#(${HOME}|${PWD})} )
|
dmatching=( ${dmatching:#(${HOME}|${PWD})} )
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# keep at most SCD_MENUSIZE of matching and valid directories
|
# keep at most SCD_MENUSIZE of matching and valid directories
|
||||||
m=( )
|
m=( )
|
||||||
for d in $dmatching; do
|
for d in $dmatching; do
|
||||||
[[ ${#m} == $SCD_MENUSIZE ]] && break
|
[[ ${#m} == $SCD_MENUSIZE ]] && break
|
||||||
[[ -d $d && -x $d ]] && m+=$d
|
[[ -d $d && -x $d ]] && m+=$d
|
||||||
done
|
done
|
||||||
dmatching=( $m )
|
dmatching=( $m )
|
||||||
|
|
||||||
# find the maximum rank
|
# find the maximum rank
|
||||||
maxrank=0.0
|
maxrank=0.0
|
||||||
for d in $dmatching; do
|
for d in $dmatching; do
|
||||||
[[ ${drank[$d]} -lt maxrank ]] || maxrank=${drank[$d]}
|
[[ ${drank[$d]} -lt maxrank ]] || maxrank=${drank[$d]}
|
||||||
done
|
done
|
||||||
|
|
||||||
# discard all directories below the rank threshold
|
# discard all directories below the rank threshold
|
||||||
threshold=$(( maxrank * SCD_THRESHOLD ))
|
threshold=$(( maxrank * SCD_THRESHOLD ))
|
||||||
dmatching=( ${^dmatching}(Ne:'(( ${drank[$REPLY]} >= threshold ))':) )
|
dmatching=( ${^dmatching}(Ne:'(( ${drank[$REPLY]} >= threshold ))':) )
|
||||||
|
}
|
||||||
|
|
||||||
|
_scd_Y19oug_match $*
|
||||||
|
|
||||||
## process whatever directories that remained
|
## process whatever directories that remained
|
||||||
case ${#dmatching} in
|
if [[ ${#dmatching} == 0 ]]; then
|
||||||
(0)
|
print -u2 "No matching directory."
|
||||||
print -u2 "no matching directory"
|
$EXIT 1
|
||||||
return 1
|
fi
|
||||||
;;
|
|
||||||
(1)
|
## build formatted directory aliases for selection menu or list display
|
||||||
|
for d in $dmatching; do
|
||||||
|
if [[ -n ${opt_verbose} ]]; then
|
||||||
|
dalias[$d]=$(printf "%.3g %s" ${drank[$d]} $d)
|
||||||
|
else
|
||||||
|
dalias[$d]=$(print -Dr -- $d)
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
## process the --list option
|
||||||
|
if [[ -n $opt_list ]]; then
|
||||||
|
for d in $dmatching; do
|
||||||
|
print -r -- "# ${dalias[$d]}"
|
||||||
|
print -r -- $d
|
||||||
|
done
|
||||||
|
$EXIT
|
||||||
|
fi
|
||||||
|
|
||||||
|
## process single directory match
|
||||||
|
if [[ ${#dmatching} == 1 ]]; then
|
||||||
_scd_Y19oug_action $dmatching
|
_scd_Y19oug_action $dmatching
|
||||||
return $?
|
$EXIT $?
|
||||||
;;
|
fi
|
||||||
(*)
|
|
||||||
# build a list of strings to be displayed in the selection menu
|
## here we have multiple matches - display selection menu
|
||||||
m=( ${(f)"$(print -lD ${dmatching})"} )
|
a=( {a-z} {A-Z} )
|
||||||
if [[ -n $opt_verbose ]]; then
|
p=( )
|
||||||
for i in {1..${#dmatching}}; do
|
for i in {1..${#dmatching}}; do
|
||||||
d=${dmatching[i]}
|
|
||||||
m[i]=$(printf "%.3g %s" ${drank[$d]} $d)
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
# build a map of string names to actual directory paths
|
|
||||||
for i in {1..${#m}}; dalias[${m[i]}]=${dmatching[i]}
|
|
||||||
# opt_list - output matching directories and exit
|
|
||||||
if [[ -n $opt_list ]]; then
|
|
||||||
_scd_Y19oug_action ${dmatching}
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
# finally use the selection menu to get the answer
|
|
||||||
a=( {a-z} {A-Z} )
|
|
||||||
p=( )
|
|
||||||
for i in {1..${#m}}; do
|
|
||||||
[[ -n ${a[i]} ]] || break
|
[[ -n ${a[i]} ]] || break
|
||||||
dkey[${a[i]}]=${dalias[$m[i]]}
|
p+="${a[i]}) ${dalias[${dmatching[i]}]}"
|
||||||
p+="${a[i]}) ${m[i]}"
|
done
|
||||||
done
|
|
||||||
print -c -r -- $p
|
print -c -r -- $p
|
||||||
if read -s -k 1 d && [[ -n ${dkey[$d]} ]]; then
|
|
||||||
_scd_Y19oug_action ${dkey[$d]}
|
if read -s -k 1 d && [[ ${i::=${a[(I)$d]}} -gt 0 ]]; then
|
||||||
fi
|
_scd_Y19oug_action ${dmatching[i]}
|
||||||
return $?
|
$EXIT $?
|
||||||
esac
|
fi
|
||||||
|
Loading…
Reference in New Issue
Block a user