Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
744 lines (640 sloc) 23.9 KB
#
# filter-select
#
# using filter-select, you can incrementaly filter candidate
# and select one with ^N/^P keys.
#
# press enter for filter-select to update $reply and return 0,
# press meta (alt) + enter to update $reply but return 1,
# and press ^C or ^G not to update $reply and return 1.
#
# you can use ^@ to mark items. marked items are stored in $reply_marked.
#
# you can customize keybinds using bindkey command.
# first, you call::
#
# autoload -U filter-select; filter-select -i
#
# to initialize `filterselect` keymap and then do like::
#
# bindkey -M filterselect '^E' accept-search
#
#
# usage:
# filter-select [-t title] [-A assoc-array-name]
# [-d array-of-description] [-D assoc-array-of-descrption]
# [-s initial-filter-contents]
# [-n] [-r] [-m] [-e exit-zle-widget-name]... [--] [arg]...
# filter-select -i
#
# -t title
# title string displayed top of selection.
#
# -A assoc-array-name
# name of associative array that contains candidates.
# this option is designed to speed up history selection.
#
# -d array-of-description
# name of array that contains each candidate's descriptions.
# it is used to display and filter candidates.
#
# if not specified, copied from candidates.
#
# -s initial-filter-contents
# initial contents of the filter buffer that users type into
#
# -n
# assign a number to the description when -d is not specified
#
# -D assoc-array-of-descrption
# same as ``-d`` but associative array.
#
# -r
# reverse order.
#
# -m
# enable mark feature
#
# -e exit-zle-widget-name
# if keys bound to `exit-zle-widget-name` is pressed,
# filter-select exits and set it's name to $reply[1].
#
# args
# selection candidates.
#
# -i
# only initialize `filterselect` keymaps.
#
#
# default key binds in filterselect:
# enter: accept-line (update $reply and return)
# meta + enter: accept-search (update $reply but return 1)
# ^G: send-break (return 0)
# ^H, backspace: backward-delete-char
# ^F, right key: forward-char
# ^B, left key: backward-char
# ^A: beginning-of-line
# ^E: end-of-line
# ^W: backward-kill-word
# ^K: kill-line
# ^U: kill-whole-line
# ^N, down key: down-line-or-history (select next item)
# ^P, up key: up-line-or-history (select previous item)
# ^V, page up key: forward-word (page down)
# ^[V, page down key: backward-word (page up)
# ^[<, home key: beginning-of-history (select first item)
# ^[>, end key: end-of-history (select last item)
#
# available zstyles:
# ':filter-select:highlight' selected
# ':filter-select:highlight' matched
# ':filter-select:highlight' title
# ':filter-select:highlight' error
# ':filter-select' max-lines
# ':filter-select' rotate-list
# ':filter-select' case-insensitive
# ':filter-select' extended-search
#
# example:
# zstyle ':filter-select:highlight' matched fg=yellow,standout
# zstyle ':filter-select' max-lines 10 # use 10 lines for filter-select
# zstyle ':filter-select' max-lines -10 # use $LINES - 10 for filter-select
# zstyle ':filter-select' rotate-list yes # enable rotation for filter-select
# zstyle ':filter-select' case-insensitive yes # enable case-insensitive search
# zstyle ':filter-select' extended-search yes # enable extended search regardless of the case-insensitive style
#
# extended-search:
# If this style set to be true value, the searching bahavior will be
# extended as follows:
#
# ^ Match the beginning of the line if the word begins with ^
# $ Match the end of the line if the word ends with $
# ! Match anything except the word following it if the word begins with !
# so-called smartcase searching
#
# If you want to search these metacharacters, please doubly escape them.
typeset -ga reply_marked
function filter-select() {
emulate -L zsh
setopt local_options extended_glob
# save ZLE related variables
local orig_lbuffer="${LBUFFER}"
local orig_rbuffer="${RBUFFER}"
local orig_predisplay="${PREDISPLAY}"
local orig_postdisplay="${POSTDISPLAY}"
local -a orig_region_highlight words
orig_region_highlight=("${region_highlight[@]}")
local key cand lines selected cand_disp buffer_pre_zle last_buffer initbuffer=''
local opt pattern msg unused title='' exit_pattern nl=$'\n'
local selected_index mark_idx_disp hi start end spec
local desc desc_num desc_disp bounds
local -a displays matched_desc_keys match mbegin mend outs exit_wigdets
local -a init_region_highlight marked_lines
local -A candidates descriptions matched_descs
integer i bottom_lines cursor_line=1 display_head_line=1 cand_num disp_num ii num_desc
integer offset display_bottom_line selected_num rev=0 ret=0 enum=0
integer mark_idx markable=0 is_marked
local hi_selected hi_matched hi_marked hi_title hi_error
zstyle -s ':filter-select:highlight' selected hi_selected || hi_selected='standout'
zstyle -s ':filter-select:highlight' matched hi_matched || hi_matched='fg=magenta,underline'
zstyle -s ':filter-select:highlight' marked hi_marked || hi_marked='fg=blue,standout'
zstyle -s ':filter-select:highlight' title hi_title || hi_title='bold'
zstyle -s ':filter-select:highlight' error hi_error || hi_error='fg=white,bg=red'
integer max_lines
zstyle -s ':filter-select' max-lines max_lines || max_lines=0
local rotate_list
zstyle -b ':filter-select' rotate-list rotate_list
_filter-select-init-keybind
candidates=()
descriptions=()
exit_wigdets=(accept-line accept-search send-break)
while getopts 't:A:d:D:nrme:s:i' opt; do
case "${opt}" in
t)
title="${OPTARG}"
;;
A)
# copy input assc array
candidates=("${(@kvP)${OPTARG}}")
;;
d)
# copy input array
integer i=0
for desc in "${(@P)${OPTARG}}"; do
(( i++ ))
descriptions+=( $i "${desc}" )
done
;;
D)
# copy input assc array
descriptions=("${(@kvP)${OPTARG}}")
;;
n)
enum=1
;;
r)
# reverse ordering
rev=1
;;
m)
# can use set-mark-command
markable=1
;;
e)
exit_wigdets+="${OPTARG}"
;;
s)
initbuffer="${OPTARG}"
;;
i)
# do nothing. only keybinds are initialized
return
esac
done
if (( OPTIND > 1 )); then
shift $(( OPTIND - 1 ))
fi
integer i=0
for cand in "$@"; do
(( i++ ))
candidates+=( $i "${cand}" )
done
if (( ${#descriptions} == 0 )); then
# copy candidates
descriptions=("${(@kv)candidates}")
# add number
if (( enum )); then
num_desc="${#descriptions}"
for i in {1.."$num_desc"}; do
if (( rev )); then
ii="$(($num_desc-$i+1))"
else
ii="$i"
fi
descriptions[$i]="${(r.5.)ii} ${descriptions[$i]}"
done
fi
fi
desc_num="${#descriptions}"
matched_desc_keys=("${(onk@)descriptions}")
if (( rev )); then
matched_desc_keys=("${(Oa@)matched_desc_keys}")
fi
key=''
bounds=''
# clear edit buffer
BUFFER="$initbuffer"
# display original edit buffer's contants as PREDISPLAY
PREDISPLAY="${orig_predisplay}${orig_lbuffer}${orig_rbuffer}${orig_postdisplay}${nl}"
# re-calculate region_highlight
init_region_highlight=()
for hi in "${(@)orig_region_highlight}"; do
if [[ "${hi}" == P* ]]; then
init_region_highlight+="${hi}"
else
print -r -- "${hi}" | read -d ' ' start end spec
init_region_highlight+="P$(( start + ${#orig_predisplay} )) $(( end + ${#orig_predisplay} )) $spec"
fi
done
# prompt for filter-select
PREDISPLAY+="filter: "
# clear strings displayed below the command line
zle -Rc
_filter-select-reset
exit_pattern="(${(j:|:)exit_wigdets})"
while [[ "${bounds}" != ${~exit_pattern} ]]; do
case "${bounds}" in
set-mark-command)
if (( markable )); then
# check if ${selected_index} is already in the marked_lines
if (( ${marked_lines[(ie)${selected_index}]} <= ${#marked_lines} )); then
# remove selected_index
marked_lines=("${(@)marked_lines:#${selected_index}}")
else
marked_lines+="${selected_index}"
fi
fi
;;
*down-line-or-history)
(( cursor_line++ ))
;;
*up-line-or-history)
(( cursor_line-- ))
;;
*forward-word)
(( cursor_line += bottom_lines ))
;;
*backward-word)
(( cursor_line -= bottom_lines ))
;;
beginning-of-history)
(( cursor_line = 1 ))
(( display_head_line = 1 ))
;;
end-of-history)
(( cursor_line = desc_num ))
;;
self-insert|undefined-key)
LBUFFER="${LBUFFER}${key}"
_filter-select-reset
;;
'')
# empty, initial state
;;
*)
buffer_pre_zle="${BUFFER}"
zle "${bounds}"
if [[ "${BUFFER}" != "${buffer_pre_zle}" ]]; then
_filter-select-reset
fi
esac
if (( cursor_line < 1 )); then
(( display_head_line -= 1 - cursor_line ))
if (( display_head_line < 1 )); then
(( display_head_line = 1 ))
fi
if [[ $rotate_list == "yes" ]] && (( selected_num <= 1 )); then
(( cursor_line = bottom_lines ))
(( display_head_line = desc_num - bottom_lines + 1 ))
else
(( cursor_line = 1 ))
fi
elif (( bottom_lines == 0 )); then
(( display_head_line = 1 ))
(( cursor_line = 1 ))
elif (( cursor_line > bottom_lines )); then
(( display_head_line += cursor_line - bottom_lines ))
if (( display_head_line > desc_num - bottom_lines + 1 )); then
(( display_head_line = desc_num - bottom_lines + 1 ))
fi
if [[ $rotate_list == "yes" ]] && (( selected_num >= desc_num )); then
(( cursor_line = 1 ))
(( display_head_line = 1 ))
else
(( cursor_line = bottom_lines ))
fi
fi
if (( ! PENDING )); then
region_highlight=("${(@)init_region_highlight}")
displays=()
offset="${#BUFFER}"
if [[ -n "${title}" ]]; then
offset+=$(( 1 + ${#title} ))
fi
selected=""
selected_num=0
if [[ "${BUFFER}" != "${last_buffer}" ]]; then
if [[ -n "${BUFFER}" ]]; then
if _filter-select-buffer-words words; then
matched_descs=("${(kv@)descriptions}")
for pattern in $words; do
matched_descs=("${(kv@)matched_descs[(R)*${pattern}*]}")
done
matched_desc_keys=("${(onk@)matched_descs}")
else
matched_desc_keys=("${(onk@)descriptions}")
fi
else
matched_desc_keys=("${(onk@)descriptions}")
fi
if (( rev )); then
matched_desc_keys=("${(Oa@)matched_desc_keys}")
fi
last_buffer="${BUFFER}"
fi
# nums pattern matched
desc_num="${#matched_desc_keys}"
# nums displayed
disp_num=0
_filter-select-update-bottom-lines
display_bottom_line=$(( display_head_line + bottom_lines))
if (( desc_num )); then
for i in "${(@)matched_desc_keys[${display_head_line},$(( display_bottom_line - 1 ))]}"; do
(( disp_num++ ))
desc="${descriptions[$i]}"
desc_disp="${desc}"
if zstyle -T ':filter-select' escape-descriptions ; then
# escape \r\n\t\
desc_disp="${desc_disp//\\/\\\\}"
desc_disp="${desc_disp//$'\n'/\\n}"
desc_disp="${desc_disp//$'\r'/\\r}"
desc_disp="${desc_disp//$'\t'/\\t}"
fi
mark_idx="${marked_lines[(ie)${i}]}"
(( is_marked = mark_idx <= ${#marked_lines} ))
if (( is_marked )); then
mark_idx_disp=" (${mark_idx})"
else
mark_idx_disp=""
fi
if (( ${(m)#desc_disp} + ${#mark_idx_disp} > COLUMNS - 1 )); then
# strip long line
desc_disp="${(mr:$(( COLUMNS - ${#mark_idx_disp} - 6 )):::::)desc_disp} ...${mark_idx_disp}"
else
desc_disp="${desc_disp}${mark_idx_disp}"
fi
displays+="${desc_disp}"
if [[ -n "${BUFFER}" ]]; then
# highlight matched words
for pattern in \
"(${(j.|.)${(@M)words:#*'(#e)'}})" \
"(${(j.|.)${(@)words:#(\~*|*'(#e)')}})" ; do
if [[ "$pattern" != '()' ]]; then
region_highlight+=( "${(f)${(S)desc_disp//*(#b)${~pattern}/$(( offset + mbegin[1] )) $(( offset + mend[1] + 1 )) ${hi_matched}${nl}}%$nl*}" )
fi
done
fi
if (( is_marked )); then
region_highlight+="${offset} $(( offset + ${#desc_disp} - ${#mark_idx_disp} + 1 )) ${hi_marked}"
fi
if (( disp_num == cursor_line )); then
region_highlight+="${offset} $(( offset + ${#desc_disp} + 1 )) ${hi_selected}"
selected="${candidates[$i]}"
(( selected_num = display_head_line + disp_num - 1 ))
selected_index="${i}"
fi
(( offset += ${#desc_disp} + 1 )) # +1 -> \n
done
fi
POSTDISPLAY=$'\n'
if [[ -n "${title}" ]]; then
POSTDISPLAY+="${title}"$'\n'
region_highlight+="${#BUFFER} $(( ${#BUFFER} + ${#title} + 1 )) ${hi_title}"
fi
if (( ${#displays} == 0 )); then
if (( ${#candidates} == 0 )); then
msg='no candidate'
else
msg='pattern not found'
fi
POSTDISPLAY+="${msg}"
region_highlight+="${offset} $(( offset + ${#msg} + 1 )) ${hi_error}"
fi
POSTDISPLAY+="${(F)displays}"$'\n'"[${selected_num}/${desc_num}]"
zle -R
fi
_filter-select-read-keys
if [[ $? != 0 ]]; then
# maybe ^C
key=''
bounds=''
break
else
key="${reply}"
# TODO: key sequence
outs=("${(z)$( bindkey -M filterselect -- "${key}" )}")
# XXX: will $outs contains more than two values?
bounds="${outs[2]}"
fi
done
if [[ -z "${key}" && -z "${bounds}" ]]; then
# ^C
reply=()
reply_marked=()
ret=1
elif [[ "${bounds}" == send-break ]]; then
# ^G
reply=()
reply_marked=()
ret=1
elif (( ${#displays} == 0 )); then
# no candidate matches pattern (no candidate selected)
reply=()
reply_marked=()
ret=1
else
reply=("${bounds}" "${selected}")
reply_marked=()
if (( ${#marked_lines} > 0 )); then
for i in "${(@)marked_lines}"; do
reply_marked+="${candidates[${i}]}"
done
fi
ret=0
fi
LBUFFER="${orig_lbuffer}"
RBUFFER="${orig_rbuffer}"
PREDISPLAY="${orig_predisplay}"
POSTDISPLAY="${orig_postdisplay}"
region_highlight=("${orig_region_highlight[@]}")
zle -Rc
zle reset-prompt
return $ret
}
function _filter-select-update-lines() {
# XXX: this function override ${lines}
# that define as local in filter-select
# also use ${title}
local _tmp_postdisplay="${POSTDISPLAY}"
# to re-calculate ${BUFFERLINES}
if [[ -n "${title}" ]]; then
POSTDISPLAY="${title}"$'\n'
else
POSTDISPLAY=""
fi
zle -R
# lines that can be used to display candidates
# -1 for current/total number display area
(( lines = LINES - BUFFERLINES - 1 ))
POSTDISPLAY="${_tmp_postdisplay}"
zle -R
}
function _filter-select-update-bottom-lines() {
# cursor が移動できる一番下の行
# ${max_lines} か ${lines} か ${desc_num} の小さい方を使う
if (( max_lines > 0 && max_lines < lines )); then
(( bottom_lines = max_lines ))
elif (( max_lines < 0 )); then
(( bottom_lines = lines + max_lines ))
else
(( bottom_lines = lines ))
fi
if (( desc_num < bottom_lines )); then
(( bottom_lines = desc_num ))
fi
if (( bottom_lines < 1 )); then
(( bottom_lines = 1 ))
fi
}
function _filter-select-reset() {
display_head_line=1
cursor_line=1
_filter-select-update-lines
_filter-select-update-bottom-lines
}
function _filter-select-buffer-words() {
local place="$1"
local -a a
local MATCH MBEGIN MEND
# split into words using shell's command line parsing,
# unquote the words, remove duplicated,
# escape "(", ")", "[", "]" and "#" to avoid crash
# also escape "|" and "~"
a=("${(@)${(@Qu)${(z)BUFFER}}//(#m)[()[\]#\|~]/\\${MATCH}}")
if ! zstyle -t ':filter-select' extended-search ; then
if zstyle -t ':filter-select' case-insensitive; then
: ${(A)a::=(#i)${^a}}
fi
else
# remove single "\\", "!",
# "^" like the history-incremental-pattern-searches',
# and "!^".
: ${(A)a::=${a:#([\\!^]|'!^')}}
# escape "^" other than the beginning's
# unescape "\\^" one level
: ${(A)a::=${a//(#m)('^'~(#s)'^')/\\${MATCH}}}
: ${(A)a::=${a//(#m)'\\^'/${MATCH#\\}}}
# "!aoe" -> "~*aoe",
# ("a!oe" should be held on, the beginning's "!" only be considered)
# unescape "\\!" one level
: ${(A)a::=${a/(#m)(#s)\!?##/\~\*${MATCH#\!}}}
: ${(A)a::=${a//(#m)'\!'/${MATCH#\\}}} # XXX: not '\\!' though...
# "^abc" -> "(#s)abc",
# ("a^bc" should be held on, the beginning's "^" only be considered)
: ${(A)a::=${a/(#m)(#s)\^?##/(#s)${MATCH#\^}}}
# "xyz$" -> "xyz(#e)",
# ("x$yz" shoud be held on, the ending's "$" only be considered)
# unescape "\\$" one level
: ${(A)a::=${a/(#m)*[^\\]\$(#e)/${MATCH%\$}(#e)}}
: ${(A)a::=${a//(#m)'\$'/${MATCH#\\}}} # XXX: not '\\$' though...
# smartcase searching ("(#i)(#I)Search" searches case sensitively)
: ${(A)a::=${a/(#m)*[[:upper:]##]*/(#I)${MATCH}}}
: ${(A)a::=(#i)${^a}}
# make "~" to be at the beginning
#: ${(A)a::=${a/#(#b)('(#i)'('(#I)')#)'~'/\~${match[1]}}}
: ${(A)a::=${a/#'(#i)(#I)~'/\~(#i)(#I)}}
: ${(A)a::=${a/#'(#i)~'/\~(#i)}}
# fixup the '!^'; "~(#i)*\^" -> "~(#i)(#s)"
# (for example, "!^aoe" -> "~(#i)*\^aoe" -> "~(#i)(#s)aoe")
#: ${(A)a::=${a/#(#b)'~'('(#i)'('(#I)')#)'*\^'/\~${match[1]}(#s)}}
: ${(A)a::=${a/#'~(#i)(#I)*\^'/'~(#i)(#I)(#s)'}}
: ${(A)a::=${a/#'~(#i)*\^'/'~(#i)(#s)'}}
fi
: ${(PA)place::=$a}
(( ${#a} > 1 )) || (( ${#a} == 1 )) && [[ -n "$a" ]]
}
function _filter-select-init-keybind() {
integer fd ret
# be quiet and check filterselect keybind defined
exec {fd}>&2 2>/dev/null
bindkey -l filterselect > /dev/null
ret=$?
exec 2>&${fd} {fd}>&-
if (( ret != 0 )); then
bindkey -N filterselect
bindkey -M filterselect '^J' accept-line
bindkey -M filterselect '^M' accept-line
bindkey -M filterselect '\e^J' accept-search
bindkey -M filterselect '\e^M' accept-search
bindkey -M filterselect '\e^G' send-break
bindkey -M filterselect '^G' send-break
bindkey -M filterselect '^@' set-mark-command
bindkey -M filterselect '^H' backward-delete-char
bindkey -M filterselect '^?' backward-delete-char
bindkey -M filterselect '^F' forward-char
bindkey -M filterselect '\e[C' forward-char
bindkey -M filterselect '\eOC' forward-char
bindkey -M filterselect '^B' backward-char
bindkey -M filterselect '\e[D' backward-char
bindkey -M filterselect '\eOD' backward-char
bindkey -M filterselect '^A' beginning-of-line
bindkey -M filterselect '^E' end-of-line
bindkey -M filterselect '^W' backward-kill-word
bindkey -M filterselect '^K' kill-line
bindkey -M filterselect '^U' kill-whole-line
# move cursor down/up
bindkey -M filterselect '^N' down-line-or-history
bindkey -M filterselect '\e[B' down-line-or-history
bindkey -M filterselect '\eOB' down-line-or-history
bindkey -M filterselect '^P' up-line-or-history
bindkey -M filterselect '\e[A' up-line-or-history
bindkey -M filterselect '\eOA' up-line-or-history
# page down/up
bindkey -M filterselect '^V' forward-word
bindkey -M filterselect '\e[6~' forward-word
bindkey -M filterselect '\eV' backward-word
bindkey -M filterselect '\ev' backward-word
bindkey -M filterselect '\e[5~' backward-word
# home/end
bindkey -M filterselect '\e<' beginning-of-history
bindkey -M filterselect '\e[1~' beginning-of-history
bindkey -M filterselect '\e>' end-of-history
bindkey -M filterselect '\e[4~' end-of-history
fi
}
function _filter-select-read-keys() {
local key key2 key3 nkey
integer ret
read -k key
ret=$?
reply="${key}"
if [[ '#key' -eq '#\\e' ]]; then
# M-...
read -t $(( KEYTIMEOUT / 1000 )) -k key2
if [[ "${key2}" == 'O' ]]; then
# ^[O (SS3) affects next character only.
# Example: cursor keys on some terminals.
read -k key3
ret=$?
reply="${key}${key2}${key3}"
else
if [[ "${key2}" == '[' ]]; then
# ^[[ (CSI) starts a sequence of [0-9;?] terminated by [@-~].
# Examples: Home, End, PgUp, PgDn ...
reply="${key}${key2}"
while true; do
read -k nkey
reply+=$nkey
ret=$?
(( $ret == 0 )) && [[ "${nkey}" =~ '^[0-9;?]$' ]] || break
done
else
reply="${key}${key2}"
fi
fi
else
reply="${key}"
fi
return $ret
}
filter-select "$@"
You can’t perform that action at this time.