mirror of
https://github.com/lotabout/skim.git
synced 2026-09-10 07:16:23 -04:00
* fix(zsh): don't relay completion items through a named pipe `_skim_complete` handed its candidates to skim through a fifo at the fixed path `$TMPDIR/skim-complete-fifo-$$`, created by `_skim_feed_fifo` and fed by a backgrounded `cat`. Opening a fifo for reading blocks until a writer opens the other end, so if the feeder ever fails to get there -- it dies, or a stale fifo left behind by an interrupted completion makes `mkfifo` fail, or the leftover in the world-writable temp dir belongs to someone else -- the widget blocks forever with nothing drawn and no way to interrupt it. Interrupted completions also leaked the fifos that set this up. That rendezvous is the only step of the completion path that can block indefinitely, and `kill` is the only completion that reaches it without the `**` trigger, so `kill<TAB>` is where it surfaces. The fifo is not needed in the first place: the `_skim_complete_*` helpers pass their candidates on `_skim_complete`'s stdin, and the command substitution that runs skim inherits fd 0, so skim can read them directly. (fzf relays through a fifo because its completion functions cannot pass their own stdin along; this file is zsh-only and has no such constraint.) Verified in a pty, in tmux and under kitty that `kill`, `ssh`, `export`, `unset`, `unalias` and the path/dir completions all still populate skim, that multi-select still runs through the `_post` filters (`kill<TAB>` still inserts bare PIDs), and that no fifos are left in the temp dir. Refs: https://github.com/skim-rs/skim/issues/1161 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PFgaE1MjSaUGAGtR4aqVzT * cleanup comms --------- Co-authored-by: Claude <noreply@anthropic.com>
420 lines
12 KiB
Bash
420 lines
12 KiB
Bash
# skim key bindings for zsh
|
|
#
|
|
# - $SKIM_TMUX_OPTS
|
|
# - $SKIM_CTRL_T_COMMAND
|
|
# - $SKIM_CTRL_T_OPTS
|
|
# - $SKIM_CTRL_R_OPTS
|
|
# - $SKIM_CTRL_R_IDX_COLOR (default: \033[2m, set NO_COLOR to disable)
|
|
# - $SKIM_CTRL_R_DATE_COLOR (default: \033[32m, set NO_COLOR to disable)
|
|
# - $SKIM_ALT_C_COMMAND
|
|
# - $SKIM_ALT_C_OPTS
|
|
# - $SKIM_COMPLETION_TRIGGER (default: '**')
|
|
# - $SKIM_COMPLETION_OPTS (default: empty)
|
|
|
|
# Key bindings
|
|
# ------------
|
|
|
|
if 'zmodload' 'zsh/parameter' 2>'/dev/null' && (( ${+options} )); then
|
|
__skim_key_bindings_options="options=(${(j: :)${(kv)options[@]}})"
|
|
__skim_completion_options="options=(${(j: :)${(kv)options[@]}})"
|
|
else
|
|
() {
|
|
__skim_key_bindings_options="setopt"
|
|
'local' '__skim_opt'
|
|
for __skim_opt in "${(@)${(@f)$(set -o)}%% *}"; do
|
|
if [[ -o "$__skim_opt" ]]; then
|
|
__skim_key_bindings_options+=" -o $__skim_opt"
|
|
else
|
|
__skim_key_bindings_options+=" +o $__skim_opt"
|
|
fi
|
|
done
|
|
}
|
|
() {
|
|
'local' '__skim_opt'
|
|
__skim_completion_options="setopt"
|
|
for __skim_opt in "${(@)${(@f)$(set -o)}%% *}"; do
|
|
if [[ -o "$__skim_opt" ]]; then
|
|
__skim_completion_options+=" -o $__skim_opt"
|
|
else
|
|
__skim_completion_options+=" +o $__skim_opt"
|
|
fi
|
|
done
|
|
}
|
|
fi
|
|
|
|
'emulate' 'zsh' '-o' 'no_aliases'
|
|
|
|
{
|
|
|
|
[[ -o interactive ]] || return 0
|
|
|
|
# CTRL-T - Paste the selected file path(s) into the command line
|
|
__fsel() {
|
|
local cmd="${SKIM_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
|
|
-o -type f -print \
|
|
-o -type d -print \
|
|
-o -type l -print 2> /dev/null | cut -b3-"}"
|
|
setopt localoptions pipefail no_aliases 2> /dev/null
|
|
REPORTTIME_=$REPORTTIME
|
|
unset REPORTTIME
|
|
eval "$cmd" | SKIM_DEFAULT_OPTIONS="--reverse $SKIM_DEFAULT_OPTIONS $SKIM_CTRL_T_OPTS" $(__skimcmd) -m "$@" | while read item; do
|
|
echo -n "${(q)item} "
|
|
done
|
|
local ret=$?
|
|
echo
|
|
if ! [ -z $REPORTTIME_ ]; then
|
|
REPORTTIME=$REPORTTIME_
|
|
fi
|
|
unset REPORTTIME_
|
|
return $ret
|
|
}
|
|
|
|
__skimcmd() {
|
|
[ -n "$TMUX_PANE" ] && { [ "${SKIM_TMUX:-0}" != 0 ] || [ -n "$SKIM_TMUX_OPTS" ]; } &&
|
|
echo "sk --tmux=${SKIM_TMUX_OPTS:-center,${SKIM_TMUX_HEIGHT:-40%}}" || echo "sk"
|
|
}
|
|
|
|
skim-file-widget() {
|
|
LBUFFER="${LBUFFER}$(__fsel)"
|
|
local ret=$?
|
|
zle reset-prompt
|
|
return $ret
|
|
}
|
|
zle -N skim-file-widget
|
|
bindkey '^T' skim-file-widget
|
|
|
|
# Ensure precmds are run after cd
|
|
skim-redraw-prompt() {
|
|
local precmd
|
|
for precmd in $precmd_functions; do
|
|
$precmd
|
|
done
|
|
zle reset-prompt
|
|
}
|
|
zle -N skim-redraw-prompt
|
|
|
|
# ALT-C - cd into the selected directory
|
|
skim-cd-widget() {
|
|
local cmd="${SKIM_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
|
|
-o -type d -print 2> /dev/null | cut -b3-"}"
|
|
setopt localoptions pipefail no_aliases 2> /dev/null
|
|
REPORTTIME_=$REPORTTIME
|
|
unset REPORTTIME
|
|
local dir="$(eval "$cmd" | SKIM_DEFAULT_OPTIONS="--reverse $SKIM_DEFAULT_OPTIONS $SKIM_ALT_C_OPTS" $(__skimcmd) --no-multi)"
|
|
if ! [ -z $REPORTTIME_ ]; then
|
|
REPORTTIME=$REPORTTIME_
|
|
fi
|
|
unset REPORTTIME_
|
|
if [[ -z "$dir" ]]; then
|
|
zle redisplay
|
|
return 0
|
|
fi
|
|
if [ -z "$BUFFER" ]; then
|
|
BUFFER="cd ${(q)dir}"
|
|
zle accept-line
|
|
else
|
|
print -sr "cd ${(q)dir}"
|
|
cd "$dir"
|
|
fi
|
|
local ret=$?
|
|
unset dir # ensure this doesn't end up appearing in prompt expansion
|
|
zle skim-redraw-prompt
|
|
tput cnorm
|
|
return $ret
|
|
}
|
|
zle -N skim-cd-widget
|
|
bindkey '\ec' skim-cd-widget
|
|
|
|
# CTRL-R - Paste the selected command from history into the command line
|
|
skim-history-widget() {
|
|
local selected num
|
|
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null
|
|
local c_idx='' c_date='' c_reset='' ansi_opt=''
|
|
if [[ ! -v NO_COLOR ]]; then
|
|
c_idx="${SKIM_CTRL_R_IDX_COLOR:-\033[2m}"
|
|
c_date="${SKIM_CTRL_R_DATE_COLOR:-\033[32m}"
|
|
c_reset='\033[0m'
|
|
ansi_opt='--ansi'
|
|
fi
|
|
local awk_filter='{ $1=$1; cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) { idx=$1; sub(idx, c_idx idx c_reset); print } }'
|
|
local n=2 fc_opts=''
|
|
if [[ -o extended_history ]]; then
|
|
local today=$(date +%Y-%m-%d)
|
|
# For today's commands, replace date ($2) with "today", otherwise remove time ($3).
|
|
# And filter out duplicates.
|
|
awk_filter='{
|
|
$1=$1;
|
|
cmd = $0; sub(/^[ \t]*[0-9]+\**[ \t]+[^ \t]+[ \t]+[^ \t]+[ \t]+/, "", cmd)
|
|
if (!seen[cmd]++) {
|
|
time = $3; date = $2; idx = $1
|
|
if (date == today) sub(date " " time " ", c_date "today@" time c_reset "\t")
|
|
else sub(date " " time " ", c_date date c_reset "\t")
|
|
sub(idx, c_idx idx c_reset)
|
|
print
|
|
}
|
|
}'
|
|
fc_opts='-i'
|
|
n=3
|
|
fi
|
|
selected=( $(fc -rl $fc_opts 1 | awk -v c_idx="$c_idx" -v c_date="$c_date" -v c_reset="$c_reset" -v today="$today" "$awk_filter" | sed 's/\\n/\\n\t/g' |
|
|
SKIM_DEFAULT_OPTIONS="$SKIM_DEFAULT_OPTIONS -n$n..,.. --bind=ctrl-r:toggle-sort $SKIM_CTRL_R_OPTS --query=${(qqq)LBUFFER} --no-multi $ansi_opt --tabstop=20 --multiline" $(__skimcmd)) )
|
|
local ret=$?
|
|
if [ -n "$selected" ]; then
|
|
num=$selected[1]
|
|
if [ -n "$num" ]; then
|
|
zle vi-fetch-history -n $num
|
|
fi
|
|
fi
|
|
zle reset-prompt
|
|
tput cnorm
|
|
return $ret
|
|
}
|
|
zle -N skim-history-widget
|
|
bindkey '^R' skim-history-widget
|
|
|
|
# Completion
|
|
# To use custom commands instead of find, override _skim_compgen_{path,dir}
|
|
if ! declare -f _skim_compgen_path > /dev/null; then
|
|
_skim_compgen_path() {
|
|
echo "$1"
|
|
command find -L "$1" \
|
|
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
|
|
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
|
|
}
|
|
fi
|
|
|
|
if ! declare -f _skim_compgen_dir > /dev/null; then
|
|
_skim_compgen_dir() {
|
|
command find -L "$1" \
|
|
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
|
|
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
|
|
}
|
|
fi
|
|
|
|
###########################################################
|
|
|
|
__skim_comprun() {
|
|
if [[ "$(type _skim_comprun 2>&1)" =~ function ]]; then
|
|
_skim_comprun "$@"
|
|
elif [ -n "$TMUX_PANE" ] && { [ "${SKIM_TMUX:-0}" != 0 ] || [ -n "$SKIM_TMUX_OPTS" ]; }; then
|
|
shift
|
|
if [ -n "$SKIM_TMUX_OPTS" ]; then
|
|
sk --tmux=${(Q)${(Z+n+)SKIM_TMUX_OPTS}} "$@"
|
|
else
|
|
sk --tmux=${SKIM_TMUX_OPTS:-center,${SKIM_TMUX_HEIGHT:-40%}} "$@"
|
|
fi
|
|
else
|
|
shift
|
|
sk "$@"
|
|
fi
|
|
}
|
|
|
|
# Extract the name of the command. e.g. foo=1 bar baz**<tab>
|
|
__skim_extract_command() {
|
|
local token tokens
|
|
tokens=(${(z)1})
|
|
for token in $tokens; do
|
|
token=${(Q)token}
|
|
if [[ "$token" =~ [[:alnum:]] && ! "$token" =~ "=" ]]; then
|
|
echo "$token"
|
|
return
|
|
fi
|
|
done
|
|
echo "${tokens[1]}"
|
|
}
|
|
|
|
__skim_generic_path_completion() {
|
|
local base lbuf cmd compgen skim_opts suffix tail dir leftover matches
|
|
base=$1
|
|
lbuf=$2
|
|
cmd=$(__skim_extract_command "$lbuf")
|
|
compgen=$3
|
|
skim_opts=$4
|
|
suffix=$5
|
|
tail=$6
|
|
|
|
setopt localoptions nonomatch
|
|
eval "base=$base"
|
|
[[ $base = *"/"* ]] && dir="$base"
|
|
while [ 1 ]; do
|
|
if [[ -z "$dir" || -d ${dir} ]]; then
|
|
leftover=${base/#"$dir"}
|
|
leftover=${leftover/#\/}
|
|
[ -z "$dir" ] && dir='.'
|
|
[ "$dir" != "/" ] && dir="${dir/%\//}"
|
|
matches=$(eval "$compgen $(printf %q "$dir")" | SKIM_DEFAULT_OPTIONS="--reverse $SKIM_DEFAULT_OPTIONS $SKIM_COMPLETION_OPTS" __skim_comprun "$cmd" ${(Q)${(Z+n+)skim_opts}} -q "$leftover" | while read item; do
|
|
echo -n "${(q)item}$suffix "
|
|
done)
|
|
matches=${matches% }
|
|
if [ -n "$matches" ]; then
|
|
LBUFFER="$lbuf$matches$tail"
|
|
fi
|
|
zle reset-prompt
|
|
break
|
|
fi
|
|
dir=$(dirname "$dir")
|
|
dir=${dir%/}/
|
|
done
|
|
}
|
|
|
|
_skim_path_completion() {
|
|
__skim_generic_path_completion "$1" "$2" _skim_compgen_path \
|
|
"-m" "" " "
|
|
}
|
|
|
|
_skim_dir_completion() {
|
|
__skim_generic_path_completion "$1" "$2" _skim_compgen_dir \
|
|
"" "/" ""
|
|
}
|
|
|
|
_skim_complete() {
|
|
setopt localoptions ksh_arrays
|
|
# Split arguments around --
|
|
local args rest str_arg i sep
|
|
args=("$@")
|
|
sep=
|
|
for i in {0..${#args[@]}}; do
|
|
if [[ "${args[$i]}" = -- ]]; then
|
|
sep=$i
|
|
break
|
|
fi
|
|
done
|
|
if [[ -n "$sep" ]]; then
|
|
str_arg=
|
|
rest=("${args[@]:$((sep + 1)):${#args[@]}}")
|
|
args=("${args[@]:0:$sep}")
|
|
else
|
|
str_arg=$1
|
|
args=()
|
|
shift
|
|
rest=("$@")
|
|
fi
|
|
|
|
local lbuf cmd matches post
|
|
lbuf=${rest[0]}
|
|
cmd=$(__skim_extract_command "$lbuf")
|
|
post="${funcstack[1]}_post"
|
|
type $post > /dev/null 2>&1 || post=cat
|
|
|
|
matches=$(SKIM_DEFAULT_OPTIONS="--reverse $SKIM_DEFAULT_OPTIONS $SKIM_COMPLETION_OPTS $str_arg" __skim_comprun "$cmd" "${args[@]}" -q "${(Q)prefix}" | $post | tr '\n' ' ')
|
|
if [ -n "$matches" ]; then
|
|
LBUFFER="$lbuf$matches"
|
|
fi
|
|
zle reset-prompt
|
|
}
|
|
|
|
_skim_complete_telnet() {
|
|
_skim_complete --no-multi -- "$@" < <(
|
|
command grep -sv '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0' |
|
|
awk '{if (length($2) > 0) {print $2}}' | sort -u
|
|
)
|
|
}
|
|
|
|
_skim_complete_ssh() {
|
|
_skim_complete --no-multi -- "$@" < <(
|
|
setopt localoptions nonomatch
|
|
command cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?]') \
|
|
<(command grep -osE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \
|
|
<(command grep -sv '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') |
|
|
awk '{if (length($2) > 0) {print $2}}' | sort -u
|
|
)
|
|
}
|
|
|
|
_skim_complete_export() {
|
|
_skim_complete -m -- "$@" < <(
|
|
declare -xp | sed 's/=.*//' | sed 's/.* //'
|
|
)
|
|
}
|
|
|
|
_skim_complete_unset() {
|
|
_skim_complete -m -- "$@" < <(
|
|
declare -xp | sed 's/=.*//' | sed 's/.* //'
|
|
)
|
|
}
|
|
|
|
_skim_complete_unalias() {
|
|
_skim_complete --no-multi -- "$@" < <(
|
|
alias | sed 's/=.*//'
|
|
)
|
|
}
|
|
|
|
_skim_complete_kill() {
|
|
_skim_complete -m --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
|
|
command ps -ef | sed 1d
|
|
)
|
|
}
|
|
|
|
_skim_complete_kill_post() {
|
|
awk '{print $2}'
|
|
}
|
|
|
|
skim-completion() {
|
|
local tokens cmd prefix trigger tail matches lbuf d_cmds
|
|
setopt localoptions noshwordsplit noksh_arrays noposixbuiltins
|
|
|
|
# http://zsh.sourceforge.net/FAQ/zshfaq03.html
|
|
# http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags
|
|
tokens=(${(z)LBUFFER})
|
|
if [ ${#tokens} -lt 1 ]; then
|
|
zle ${skim_default_completion:-expand-or-complete}
|
|
return
|
|
fi
|
|
|
|
cmd=$(__skim_extract_command "$LBUFFER")
|
|
|
|
# Explicitly allow for empty trigger.
|
|
trigger=${SKIM_COMPLETION_TRIGGER-'**'}
|
|
[ -z "$trigger" -a ${LBUFFER[-1]} = ' ' ] && tokens+=("")
|
|
|
|
# When the trigger starts with ';', it becomes a separate token
|
|
if [[ ${LBUFFER} = *"${tokens[-2]}${tokens[-1]}" ]]; then
|
|
tokens[-2]="${tokens[-2]}${tokens[-1]}"
|
|
tokens=(${tokens[0,-2]})
|
|
fi
|
|
|
|
lbuf=$LBUFFER
|
|
tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))}
|
|
# Kill completion (do not require trigger sequence)
|
|
if [ "$cmd" = kill -a ${LBUFFER[-1]} = ' ' ]; then
|
|
tail=$trigger
|
|
tokens+=$trigger
|
|
lbuf="$lbuf$trigger"
|
|
fi
|
|
|
|
# Trigger sequence given
|
|
if [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then
|
|
d_cmds=(${=SKIM_COMPLETION_DIR_COMMANDS:-cd pushd rmdir})
|
|
|
|
[ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}}
|
|
[ -n "${tokens[-1]}" ] && lbuf=${lbuf:0:-${#tokens[-1]}}
|
|
|
|
if eval "type _skim_complete_${cmd} > /dev/null"; then
|
|
prefix="$prefix" eval _skim_complete_${cmd} ${(q)lbuf}
|
|
elif [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then
|
|
_skim_dir_completion "$prefix" "$lbuf"
|
|
else
|
|
_skim_path_completion "$prefix" "$lbuf"
|
|
fi
|
|
tput cnorm
|
|
# Fall back to default completion
|
|
else
|
|
zle ${skim_default_completion:-expand-or-complete}
|
|
fi
|
|
}
|
|
|
|
[ -z "$skim_default_completion" ] && {
|
|
binding=$(bindkey '^I')
|
|
[[ $binding =~ 'undefined-key' ]] || skim_default_completion=$binding[(s: :w)2]
|
|
unset binding
|
|
}
|
|
|
|
zle -N skim-completion
|
|
bindkey '^I' skim-completion
|
|
|
|
} always {
|
|
eval $__skim_key_bindings_options
|
|
'unset' '__skim_key_bindings_options'
|
|
eval $__skim_completion_options
|
|
'unset' '__skim_completion_options'
|
|
}
|