# $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 # # 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 # 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="" list=( "$@" ) 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 list=( "$@" ) # 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// ##/|}" list=( "${(@M)list:#(#i)*$~search_pattern*}" ) 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' disp_list=( "${(@)disp_list//(#mi)($~colsearch_pattern)/$red${MATCH}$reset}" ) 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 list=( "$@" ) # 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 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: