123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428 |
- # $1, $2, ... - elements of the list
- # $NLIST_NONSELECTABLE_ELEMENTS - array of indexes (1-based) that cannot be selected
- # $REPLY is the output variable - contains index (1-based) or -1 when no selection
- # $reply (array) is the second part of the output - use the index (REPLY) to get selected element
- #
- # Copy this file into /usr/share/zsh/site-functions/
- # and add 'autoload n-list` to .zshrc
- #
- # This function outputs a list of elements that can be
- # navigated with keyboard. Uses curses library
- emulate -LR zsh
- setopt typesetsilent extendedglob noshortloops
- _nlist_has_terminfo=0
- zmodload zsh/curses
- zmodload zsh/terminfo 2>/dev/null && _nlist_has_terminfo=1
- trap "REPLY=-2; reply=(); return" TERM INT QUIT
- trap "_nlist_exit" EXIT
- # Drawing and input
- autoload n-list-draw n-list-input
- # Cleanup before any exit
- _nlist_exit() {
- setopt localoptions
- setopt extendedglob
- [[ "$REPLY" = -(#c0,1)[0-9]## ]] || REPLY="-1"
- zcurses 2>/dev/null delwin inner
- zcurses 2>/dev/null delwin main
- zcurses 2>/dev/null refresh
- zcurses end
- _nlist_alternate_screen 0
- _nlist_cursor_visibility 1
- unset _nlist_has_terminfo
- }
- # Outputs a message in the bottom of the screen
- _nlist_status_msg() {
- # -1 for border, -1 for 0-based indexing
- zcurses move main $(( term_height - 1 - 1 )) 2
- zcurses clear main eol
- zcurses string main "$1"
- #status_msg_strlen is localized in caller
- status_msg_strlen=$#1
- }
- # Prefer tput, then module terminfo
- _nlist_cursor_visibility() {
- if type tput 2>/dev/null 1>&2; then
- [ "$1" = "1" ] && { tput cvvis; tput cnorm }
- [ "$1" = "0" ] && tput civis
- elif [ "$_nlist_has_terminfo" = "1" ]; then
- [ "$1" = "1" ] && { [ -n $terminfo[cvvis] ] && echo -n $terminfo[cvvis];
- [ -n $terminfo[cnorm] ] && echo -n $terminfo[cnorm] }
- [ "$1" = "0" ] && [ -n $terminfo[civis] ] && echo -n $terminfo[civis]
- fi
- }
- # Reason for this function is that on some systems
- # smcup and rmcup are not knowing why left empty
- _nlist_alternate_screen() {
- [ "$_nlist_has_terminfo" -ne "1" ] && return
- [[ "$1" = "1" && -n "$terminfo[smcup]" ]] && return
- [[ "$1" = "0" && -n "$terminfo[rmcup]" ]] && return
- case "$TERM" in
- *rxvt*)
- [ "$1" = "1" ] && echo -n $'\x1b7\x1b[?47h'
- [ "$1" = "0" ] && echo -n $'\x1b[2J\x1b[?47l\x1b8'
- ;;
- *)
- [ "$1" = "1" ] && echo -n $'\x1b[?1049h'
- [ "$1" = "0" ] && echo -n $'\x1b[?1049l'
- # just to remember two other that work: $'\x1b7\x1b[r\x1b[?47h', $'\x1b[?47l\x1b8'
- ;;
- esac
- }
- _nlist_compute_user_vars_difference() {
- if [[ "${(t)NLIST_NONSELECTABLE_ELEMENTS}" != "array" &&
- "${(t)NLIST_NONSELECTABLE_ELEMENTS}" != "array-local" ]]
- then
- last_element_difference=0
- current_difference=0
- else
- last_element_difference=$#NLIST_NONSELECTABLE_ELEMENTS
- current_difference=0
- local idx
- for idx in "${(n)NLIST_NONSELECTABLE_ELEMENTS[@]}"; do
- [ "$idx" -le "$NLIST_CURRENT_IDX" ] && current_difference+=1 || break
- done
- fi
- }
- # List was processed, check if variables aren't off range
- _nlist_verify_vars() {
- [ "$NLIST_CURRENT_IDX" -gt "$last_element" ] && NLIST_CURRENT_IDX="$last_element"
- [[ "$NLIST_CURRENT_IDX" -eq 0 && "$last_element" -ne 0 ]] && NLIST_CURRENT_IDX=1
- (( NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN=0+((NLIST_CURRENT_IDX-1)/page_height)*page_height+1 ))
- }
- # Compute the variables which are shown to the user
- _nlist_setup_user_vars() {
- if [ "$1" = "1" ]; then
- # Basic values when there are no non-selectables
- NLIST_USER_CURRENT_IDX="$NLIST_CURRENT_IDX"
- NLIST_USER_LAST_ELEMENT="$last_element"
- else
- _nlist_compute_user_vars_difference
- NLIST_USER_CURRENT_IDX=$(( NLIST_CURRENT_IDX - current_difference ))
- NLIST_USER_LAST_ELEMENT=$(( last_element - last_element_difference ))
- fi
- }
- _nlist_colorify_disp_list() {
- local col=$'\x1b[00;34m' reset=$'\x1b[0m'
- [ -n "$NLIST_COLORING_COLOR" ] && col="$NLIST_COLORING_COLOR"
- [ -n "$NLIST_COLORING_END_COLOR" ] && reset="$NLIST_COLORING_END_COLOR"
- if [ "$NLIST_COLORING_MATCH_MULTIPLE" -eq 1 ]; then
- disp_list=( "${(@)disp_list//(#mi)$~NLIST_COLORING_PATTERN/$col${MATCH}$reset}" )
- else
- disp_list=( "${(@)disp_list/(#mi)$~NLIST_COLORING_PATTERN/$col${MATCH}$reset}" )
- fi
- }
- #
- # Main code
- #
- # Check if there is proper input
- if [ "$#" -lt 1 ]; then
- echo "Usage: n-list element_1 ..."
- return 1
- fi
- REPLY="-1"
- typeset -ga reply
- reply=()
- integer term_height="$LINES"
- integer term_width="$COLUMNS"
- if [[ "$term_height" -lt 1 || "$term_width" -lt 1 ]]; then
- local stty_out=$( stty size )
- term_height="${stty_out% *}"
- term_width="${stty_out#* }"
- fi
- integer inner_height=term_height-3
- integer inner_width=term_width-3
- integer page_height=inner_height
- integer page_width=inner_width
- typeset -a list disp_list
- integer last_element=$#
- local action
- local final_key
- integer selection
- integer last_element_difference=0
- integer current_difference=0
- local prev_search_buffer=""
- integer prev_uniq_mode=0
- integer prev_start_idx=-1
- local MBEGIN MEND MATCH mbegin mend match
- # Ability to remember the list between calls
- if [[ -z "$NLIST_REMEMBER_STATE" || "$NLIST_REMEMBER_STATE" -eq 0 || "$NLIST_REMEMBER_STATE" -eq 2 ]]; then
- NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN=1
- NLIST_CURRENT_IDX=1
- NLIST_IS_SEARCH_MODE=0
- NLIST_SEARCH_BUFFER=""
- NLIST_TEXT_OFFSET=0
- NLIST_IS_UNIQ_MODE=0
- # Zero - because it isn't known, unless we
- # confirm that first element is selectable
- NLIST_USER_CURRENT_IDX=0
- [[ ${NLIST_NONSELECTABLE_ELEMENTS[(r)1]} != 1 ]] && NLIST_USER_CURRENT_IDX=1
- NLIST_USER_LAST_ELEMENT=$(( last_element - $#NLIST_NONSELECTABLE_ELEMENTS ))
- # 2 is init once, then remember
- [ "$NLIST_REMEMBER_STATE" -eq 2 ] && NLIST_REMEMBER_STATE=1
- fi
- if [ "$NLIST_START_IN_SEARCH_MODE" -eq 1 ]; then
- NLIST_START_IN_SEARCH_MODE=0
- NLIST_IS_SEARCH_MODE=1
- fi
- if [ -n "$NLIST_SET_SEARCH_TO" ]; then
- NLIST_SEARCH_BUFFER="$NLIST_SET_SEARCH_TO"
- NLIST_SET_SEARCH_TO=""
- fi
- if [ "$NLIST_START_IN_UNIQ_MODE" -eq 1 ]; then
- NLIST_START_IN_UNIQ_MODE=0
- NLIST_IS_UNIQ_MODE=1
- fi
- _nlist_alternate_screen 1
- zcurses init
- zcurses delwin main 2>/dev/null
- zcurses delwin inner 2>/dev/null
- zcurses addwin main "$term_height" "$term_width" 0 0
- zcurses addwin inner "$inner_height" "$inner_width" 1 2
- zcurses bg main white/black
- zcurses bg inner white/black
- if [ "$NLIST_IS_SEARCH_MODE" -ne 1 ]; then
- _nlist_cursor_visibility 0
- fi
- #
- # Listening for input
- #
- local key keypad
- # Clear input buffer
- zcurses timeout main 0
- zcurses input main key keypad
- zcurses timeout main -1
- key=""
- keypad=""
- # This loop makes script faster on some Zsh's (e.g. 5.0.8)
- repeat 1; do
- list=( "$@" )
- done
- last_element="$#list"
- while (( 1 )); do
- # Do searching (filtering with string)
- if [ -n "$NLIST_SEARCH_BUFFER" ]; then
- # Compute new list?
- if [[ "$NLIST_SEARCH_BUFFER" != "$prev_search_buffer" || "$NLIST_IS_UNIQ_MODE" -ne "$prev_uniq_mode" ]]; then
- prev_search_buffer="$NLIST_SEARCH_BUFFER"
- prev_uniq_mode="$NLIST_IS_UNIQ_MODE"
- # regenerating list -> regenerating disp_list
- prev_start_idx=-1
- # Take all elements, including duplicates and non-selectables
- typeset +U list
- repeat 1; do
- list=( "$@" )
- done
- # Remove non-selectable elements
- [ "$#NLIST_NONSELECTABLE_ELEMENTS" -gt 0 ] && for i in "${(nO)NLIST_NONSELECTABLE_ELEMENTS[@]}"; do
- list[$i]=()
- done
- # Remove duplicates
- [ "$NLIST_IS_UNIQ_MODE" -eq 1 ] && typeset -U list
- last_element="$#list"
- # Next do the filtering
- local search_buffer="${NLIST_SEARCH_BUFFER%% ##}"
- search_buffer="${search_buffer## ##}"
- search_buffer="${search_buffer//(#m)[][*?|#~^()><\\]/\\$MATCH}"
- local search_pattern=""
- local colsearch_pattern=""
- if [ -n "$search_buffer" ]; then
- # Patterns will be *foo*~^*bar* and (foo|bar)
- search_pattern="${search_buffer// ##/*~^*}"
- colsearch_pattern="${search_buffer// ##/|}"
- # The repeat will make the matching work on a fresh heap
- repeat 1; do
- list=( "${(@M)list:#(#i)*$~search_pattern*}" )
- done
- last_element="$#list"
- fi
- # Called after processing list
- _nlist_verify_vars
- fi
- _nlist_setup_user_vars 1
- integer end_idx=$(( NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN + page_height - 1 ))
- [ "$end_idx" -gt "$last_element" ] && end_idx=last_element
- if [ "$prev_start_idx" -ne "$NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN" ]; then
- prev_start_idx="$NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN"
- disp_list=( "${(@)list[NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN, end_idx]}" )
- if [ -n "$colsearch_pattern" ]; then
- local red=$'\x1b[00;31m' reset=$'\x1b[00;00m'
- # The repeat will make the matching work on a fresh heap
- repeat 1; do
- disp_list=( "${(@)disp_list//(#mi)($~colsearch_pattern)/$red${MATCH}$reset}" )
- done
- fi
- # We have display list, lets replace newlines with "\n" when needed (1/2)
- [ "$NLIST_REPLACE_NEWLINES" -eq 1 ] && disp_list=( "${(@)disp_list//$'\n'/\\n}" )
- fi
- # Output colored list
- n-list-draw "$(( (NLIST_CURRENT_IDX-1) % page_height + 1 ))" \
- "$page_height" "$page_width" 0 0 "$NLIST_TEXT_OFFSET" inner \
- "$disp_list[@]"
- else
- # There is no search, but there was in previous loop
- # OR
- # Uniq mode was entered or left out
- # -> compute new list
- if [[ -n "$prev_search_buffer" || "$NLIST_IS_UNIQ_MODE" -ne "$prev_uniq_mode" ]]; then
- prev_search_buffer=""
- prev_uniq_mode="$NLIST_IS_UNIQ_MODE"
- # regenerating list -> regenerating disp_list
- prev_start_idx=-1
- # Take all elements, including duplicates and non-selectables
- typeset +U list
- repeat 1; do
- list=( "$@" )
- done
- # Remove non-selectable elements only when in uniq mode
- [ "$NLIST_IS_UNIQ_MODE" -eq 1 ] && [ "$#NLIST_NONSELECTABLE_ELEMENTS" -gt 0 ] &&
- for i in "${(nO)NLIST_NONSELECTABLE_ELEMENTS[@]}"; do
- list[$i]=()
- done
- # Remove duplicates when in uniq mode
- [ "$NLIST_IS_UNIQ_MODE" -eq 1 ] && typeset -U list
- last_element="$#list"
- # Called after processing list
- _nlist_verify_vars
- fi
- # "1" - shouldn't bother with non-selectables
- _nlist_setup_user_vars "$NLIST_IS_UNIQ_MODE"
- integer end_idx=$(( NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN + page_height - 1 ))
- [ "$end_idx" -gt "$last_element" ] && end_idx=last_element
- if [ "$prev_start_idx" -ne "$NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN" ]; then
- prev_start_idx="$NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN"
- disp_list=( "${(@)list[NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN, end_idx]}" )
- [ -n "$NLIST_COLORING_PATTERN" ] && _nlist_colorify_disp_list
- # We have display list, lets replace newlines with "\n" when needed (2/2)
- [ "$NLIST_REPLACE_NEWLINES" -eq 1 ] && disp_list=( "${(@)disp_list//$'\n'/\\n}" )
- fi
- # Output the list
- n-list-draw "$(( (NLIST_CURRENT_IDX-1) % page_height + 1 ))" \
- "$page_height" "$page_width" 0 0 "$NLIST_TEXT_OFFSET" inner \
- "$disp_list[@]"
- fi
- local status_msg_strlen
- if [ "$NLIST_IS_SEARCH_MODE" = "1" ]; then
- local _txt2=""
- [ "$NLIST_IS_UNIQ_MODE" -eq 1 ] && _txt2="[-UNIQ-] "
- _nlist_status_msg "${_txt2}Filtering with: ${NLIST_SEARCH_BUFFER// /+}"
- elif [[ ${NLIST_NONSELECTABLE_ELEMENTS[(r)$NLIST_CURRENT_IDX]} != $NLIST_CURRENT_IDX ||
- -n "$NLIST_SEARCH_BUFFER" || "$NLIST_IS_UNIQ_MODE" -eq 1 ]]; then
- local _txt="" _txt2=""
- [ -n "$NLIST_GREP_STRING" ] && _txt=" [$NLIST_GREP_STRING]"
- [ "$NLIST_IS_UNIQ_MODE" -eq 1 ] && _txt2="[-UNIQ-] "
- _nlist_status_msg "${_txt2}Current #$NLIST_USER_CURRENT_IDX (of #$NLIST_USER_LAST_ELEMENT entries)$_txt"
- else
- _nlist_status_msg ""
- fi
- zcurses border main
- local top_msg="${(C)ZSH_NAME} $ZSH_VERSION, shell level $SHLVL, $USER"
- zcurses move main 0 $(( term_width / 2 - $#top_msg / 2 ))
- zcurses string main $top_msg
- zcurses refresh main inner
- zcurses move main $(( term_height - 1 - 1 )) $(( status_msg_strlen + 2 ))
- # Wait for input
- zcurses input main key keypad
- # Get the special (i.e. "keypad") key or regular key
- if [ -n "$key" ]; then
- final_key="$key"
- elif [ -n "$keypad" ]; then
- final_key="$keypad"
- else
- _nlist_status_msg "Inproper input detected"
- zcurses refresh main inner
- fi
- n-list-input "$NLIST_CURRENT_IDX" "$NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN" \
- "$page_height" "$page_width" "$last_element" "$NLIST_TEXT_OFFSET" \
- "$final_key" "$NLIST_IS_SEARCH_MODE" "$NLIST_SEARCH_BUFFER" \
- "$NLIST_IS_UNIQ_MODE"
- selection="$reply[1]"
- action="$reply[2]"
- NLIST_CURRENT_IDX="$reply[3]"
- NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN="$reply[4]"
- NLIST_TEXT_OFFSET="$reply[5]"
- NLIST_IS_SEARCH_MODE="$reply[6]"
- NLIST_SEARCH_BUFFER="$reply[7]"
- NLIST_IS_UNIQ_MODE="$reply[8]"
- if [ "$action" = "SELECT" ]; then
- REPLY="$selection"
- reply=( "$list[@]" )
- break
- elif [ "$action" = "QUIT" ]; then
- REPLY=-1
- reply=( "$list[@]" )
- break
- elif [ "$action" = "REDRAW" ]; then
- zcurses clear main redraw
- zcurses clear inner redraw
- fi
- done
- # vim: set filetype=zsh:
|