Parent directory

n-list

17490 bytes
  1# $1, $2, ... - elements of the list
  2# $NLIST_NONSELECTABLE_ELEMENTS - array of indexes (1-based) that cannot be selected
  3# $REPLY is the output variable - contains index (1-based) or -1 when no selection
  4# $reply (array) is the second part of the output - use the index (REPLY) to get selected element
  5#
  6# Copy this file into /usr/share/zsh/site-functions/
  7# and add 'autoload n-list` to .zshrc
  8#
  9# This function outputs a list of elements that can be
 10# navigated with keyboard. Uses curses library
 11
 12emulate -LR zsh
 13
 14setopt typesetsilent extendedglob noshortloops
 15
 16_nlist_has_terminfo=0
 17
 18zmodload zsh/curses
 19zmodload zsh/terminfo 2>/dev/null && _nlist_has_terminfo=1
 20
 21trap "REPLY=-2; reply=(); return" TERM INT QUIT
 22trap "_nlist_exit" EXIT
 23
 24# Drawing and input
 25autoload n-list-draw n-list-input
 26
 27# Cleanup before any exit
 28_nlist_exit() {
 29    setopt localoptions
 30    setopt extendedglob
 31
 32    [[ "$REPLY" = -(#c0,1)[0-9]## || "$REPLY" = F<-> || "$REPLY" = "EDIT" || "$REPLY" = "HELP" ]] || REPLY="-1"
 33    zcurses 2>/dev/null delwin inner
 34    zcurses 2>/dev/null delwin main
 35    zcurses 2>/dev/null refresh
 36    zcurses end
 37    _nlist_alternate_screen 0
 38    _nlist_cursor_visibility 1
 39    unset _nlist_has_terminfo
 40}
 41
 42# Outputs a message in the bottom of the screen
 43_nlist_status_msg() {
 44    # -1 for border, -1 for 0-based indexing
 45    zcurses move main $(( term_height - 1 - 1 )) 2
 46    zcurses clear main eol
 47    zcurses string main "$1"
 48    #status_msg_strlen is localized in caller
 49    status_msg_strlen=$#1
 50}
 51
 52# Prefer tput, then module terminfo
 53_nlist_cursor_visibility() {
 54    if type tput 2>/dev/null 1>&2; then
 55        [ "$1" = "1" ] && { tput cvvis; tput cnorm }
 56        [ "$1" = "0" ] && tput civis
 57    elif [ "$_nlist_has_terminfo" = "1" ]; then
 58        [ "$1" = "1" ] && { [ -n "$terminfo[cvvis]" ] && echo -n "$terminfo[cvvis]";
 59                           [ -n "$terminfo[cnorm]" ] && echo -n "$terminfo[cnorm]" }
 60        [ "$1" = "0" ] && [ -n "$terminfo[civis]" ] && echo -n "$terminfo[civis]"
 61    fi 
 62}
 63
 64# Reason for this function is that on some systems
 65# smcup and rmcup are not knowing why left empty
 66_nlist_alternate_screen() {
 67    [ "$_nlist_has_terminfo" -ne "1" ] && return
 68    [[ "$1" = "1" && -n "$terminfo[smcup]" ]] && return
 69    [[ "$1" = "0" && -n "$terminfo[rmcup]" ]] && return
 70
 71    case "$TERM" in
 72        *rxvt*)
 73            [ "$1" = "1" ] && echo -n $'\x1b7\x1b[?47h'
 74            [ "$1" = "0" ] && echo -n $'\x1b[2J\x1b[?47l\x1b8'
 75            ;;
 76        *)
 77            [ "$1" = "1" ] && echo -n $'\x1b[?1049h'
 78            [ "$1" = "0" ] && echo -n $'\x1b[?1049l'
 79            # just to remember two other that work: $'\x1b7\x1b[r\x1b[?47h', $'\x1b[?47l\x1b8'
 80            ;;
 81    esac
 82}
 83
 84_nlist_compute_user_vars_difference() {
 85        if [[ "${(t)NLIST_NONSELECTABLE_ELEMENTS}" != "array" &&
 86                "${(t)NLIST_NONSELECTABLE_ELEMENTS}" != "array-local" ]]
 87        then
 88            last_element_difference=0
 89            current_difference=0
 90        else
 91            last_element_difference=$#NLIST_NONSELECTABLE_ELEMENTS
 92            current_difference=0
 93            local idx
 94            for idx in "${(n)NLIST_NONSELECTABLE_ELEMENTS[@]}"; do
 95                [ "$idx" -le "$NLIST_CURRENT_IDX" ] && current_difference+=1 || break
 96            done
 97        fi
 98}
 99
100# List was processed, check if variables aren't off range
101_nlist_verify_vars() {
102    [ "$NLIST_CURRENT_IDX" -gt "$last_element" ] && NLIST_CURRENT_IDX="$last_element"
103    [[ "$NLIST_CURRENT_IDX" -eq 0 && "$last_element" -ne 0 ]] && NLIST_CURRENT_IDX=1
104    (( NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN=0+((NLIST_CURRENT_IDX-1)/page_height)*page_height+1 ))
105}
106
107# Compute the variables which are shown to the user
108_nlist_setup_user_vars() {
109    if [ "$1" = "1" ]; then
110        # Basic values when there are no non-selectables
111        NLIST_USER_CURRENT_IDX="$NLIST_CURRENT_IDX"
112        NLIST_USER_LAST_ELEMENT="$last_element"
113    else
114        _nlist_compute_user_vars_difference
115        NLIST_USER_CURRENT_IDX=$(( NLIST_CURRENT_IDX - current_difference ))
116        NLIST_USER_LAST_ELEMENT=$(( last_element - last_element_difference ))
117    fi
118}
119
120_nlist_colorify_disp_list() {
121    local col=$'\x1b[00;34m' reset=$'\x1b[0m'
122    [ -n "$NLIST_COLORING_COLOR" ] && col="$NLIST_COLORING_COLOR"
123    [ -n "$NLIST_COLORING_END_COLOR" ] && reset="$NLIST_COLORING_END_COLOR"
124
125    if [ "$NLIST_COLORING_MATCH_MULTIPLE" -eq 1 ]; then
126        disp_list=( "${(@)disp_list//(#mi)$~NLIST_COLORING_PATTERN/$col${MATCH}$reset}" )
127    else
128        disp_list=( "${(@)disp_list/(#mi)$~NLIST_COLORING_PATTERN/$col${MATCH}$reset}" )
129    fi
130}
131
132#
133# Main code
134#
135
136# Check if there is proper input
137if [ "$#" -lt 1 ]; then
138    echo "Usage: n-list element_1 ..."
139    return 1
140fi
141
142REPLY="-1"
143typeset -ga reply
144reply=()
145
146integer term_height="$LINES"
147integer term_width="$COLUMNS"
148if [[ "$term_height" -lt 1 || "$term_width" -lt 1 ]]; then
149    local stty_out=$( stty size )
150    term_height="${stty_out% *}"
151    term_width="${stty_out#* }"
152fi
153integer inner_height=term_height-3
154integer inner_width=term_width-3
155integer page_height=inner_height
156integer page_width=inner_width
157
158typeset -a list disp_list
159integer last_element=$#
160local action
161local final_key
162integer selection
163integer last_element_difference=0
164integer current_difference=0
165local prev_search_buffer=""
166integer prev_uniq_mode=0
167integer prev_start_idx=-1
168local MBEGIN MEND MATCH mbegin mend match
169
170# Iteration over predefined keywords
171integer curkeyword nkeywords
172local keywordisfresh="0"
173if [[ "${(t)keywords}" != *array* ]]; then
174    local -a keywords
175    keywords=()
176fi
177curkeyword=0
178nkeywords=${#keywords}
179
180# Iteration over themes
181integer curtheme nthemes
182local themeisfresh="0"
183if [[ "${(t)themes}" != *array* ]]; then
184    local -a themes
185    themes=()
186fi
187curtheme=0
188nthemes=${#themes}
189
190# Ability to remember the list between calls
191if [[ -z "$NLIST_REMEMBER_STATE" || "$NLIST_REMEMBER_STATE" -eq 0 || "$NLIST_REMEMBER_STATE" -eq 2 ]]; then
192    NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN=1
193    NLIST_CURRENT_IDX=1
194    NLIST_IS_SEARCH_MODE=0
195    NLIST_SEARCH_BUFFER=""
196    NLIST_TEXT_OFFSET=0
197    NLIST_IS_UNIQ_MODE=0
198    NLIST_IS_F_MODE=0
199
200    # Zero - because it isn't known, unless we
201    # confirm that first element is selectable
202    NLIST_USER_CURRENT_IDX=0
203    [[ ${NLIST_NONSELECTABLE_ELEMENTS[(r)1]} != 1 ]] && NLIST_USER_CURRENT_IDX=1
204    NLIST_USER_LAST_ELEMENT=$(( last_element - $#NLIST_NONSELECTABLE_ELEMENTS ))
205
206    # 2 is init once, then remember
207    [ "$NLIST_REMEMBER_STATE" -eq 2 ] && NLIST_REMEMBER_STATE=1
208fi
209
210if [ "$NLIST_START_IN_SEARCH_MODE" -eq 1 ]; then
211    NLIST_START_IN_SEARCH_MODE=0
212    NLIST_IS_SEARCH_MODE=1
213fi
214
215if [ -n "$NLIST_SET_SEARCH_TO" ]; then
216    NLIST_SEARCH_BUFFER="$NLIST_SET_SEARCH_TO"
217    NLIST_SET_SEARCH_TO=""
218fi
219
220if [ "$NLIST_START_IN_UNIQ_MODE" -eq 1 ]; then
221    NLIST_START_IN_UNIQ_MODE=0
222    NLIST_IS_UNIQ_MODE=1
223fi
224
225_nlist_alternate_screen 1
226zcurses init
227zcurses delwin main 2>/dev/null
228zcurses delwin inner 2>/dev/null
229zcurses addwin main "$term_height" "$term_width" 0 0
230zcurses addwin inner "$inner_height" "$inner_width" 1 2
231# From n-list.conf
232[ "$colorpair" = "" ] && colorpair="white/black"
233[ "$border" = "0" ] || border="1"
234local background="${colorpair#*/}"
235local backuptheme="$colorpair/$bold"
236zcurses bg main "$colorpair"
237zcurses bg inner "$colorpair"
238if [ "$NLIST_IS_SEARCH_MODE" -ne 1 ]; then
239    _nlist_cursor_visibility 0
240fi
241
242zcurses refresh
243
244#
245# Listening for input
246#
247
248local key keypad
249
250# Clear input buffer
251zcurses timeout main 0
252zcurses input main key keypad
253zcurses timeout main -1
254key=""
255keypad=""
256
257# This loop makes script faster on some Zsh's (e.g. 5.0.8)
258repeat 1; do
259    list=( "$@" )
260done
261
262last_element="$#list"
263
264zcurses clear main redraw
265zcurses clear inner redraw
266while (( 1 )); do
267    # Do searching (filtering with string)
268    if [ -n "$NLIST_SEARCH_BUFFER" ]; then
269        # Compute new list?
270        if [[ "$NLIST_SEARCH_BUFFER" != "$prev_search_buffer" || "$NLIST_IS_UNIQ_MODE" -ne "$prev_uniq_mode"
271                || "$NLIST_IS_F_MODE" -ne "$prev_f_mode" ]]
272        then
273            prev_search_buffer="$NLIST_SEARCH_BUFFER"
274            prev_uniq_mode="$NLIST_IS_UNIQ_MODE"
275            prev_f_mode="$NLIST_IS_F_MODE"
276            # regenerating list -> regenerating disp_list
277            prev_start_idx=-1
278
279            # Take all elements, including duplicates and non-selectables
280            typeset +U list
281            repeat 1; do
282                list=( "$@" )
283            done
284
285            # Remove non-selectable elements
286            [ "$#NLIST_NONSELECTABLE_ELEMENTS" -gt 0 ] && for i in "${(nO)NLIST_NONSELECTABLE_ELEMENTS[@]}"; do
287                if [[ "$i" = <-> ]]; then
288                    list[$i]=()
289                fi
290            done
291
292            # Remove duplicates
293            [ "$NLIST_IS_UNIQ_MODE" -eq 1 ] && typeset -U list
294
295            last_element="$#list"
296
297            # Next do the filtering
298            local search_buffer="${NLIST_SEARCH_BUFFER%% ##}"
299            search_buffer="${search_buffer## ##}"
300            search_buffer="${search_buffer//(#m)[][*?|#~^()><\\]/\\$MATCH}"
301            local search_pattern=""
302            local colsearch_pattern=""
303            if [ -n "$search_buffer" ]; then
304                # The repeat will make the matching work on a fresh heap
305                repeat 1; do
306                    if [ "$NLIST_IS_F_MODE" -eq "1" ]; then
307                        search_pattern="${search_buffer// ##/*~^(#a1)*}"
308                        colsearch_pattern="${search_buffer// ##/|(#a1)}"
309                        list=( "${(@M)list:#(#ia1)*$~search_pattern*}" )
310                    elif [ "$NLIST_IS_F_MODE" -eq "2" ]; then
311                        search_pattern="${search_buffer// ##/*~^(#a2)*}"
312                        colsearch_pattern="${search_buffer// ##/|(#a2)}"
313                        list=( "${(@M)list:#(#ia2)*$~search_pattern*}" )
314                    else
315                        # Pattern will be *foo*~^*bar* (inventor: Mikael Magnusson)
316                        search_pattern="${search_buffer// ##/*~^*}"
317                        # Pattern will be (foo|bar)
318                        colsearch_pattern="${search_buffer// ##/|}"
319                        list=( "${(@M)list:#(#i)*$~search_pattern*}" )
320                    fi
321                done
322
323                last_element="$#list"
324            fi
325
326            # Called after processing list
327            _nlist_verify_vars
328        fi
329
330        _nlist_setup_user_vars 1
331
332        integer end_idx=$(( NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN + page_height - 1 ))
333        [ "$end_idx" -gt "$last_element" ] && end_idx=last_element
334
335        if [ "$prev_start_idx" -ne "$NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN" ]; then
336            prev_start_idx="$NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN"
337            disp_list=( "${(@)list[NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN, end_idx]}" )
338
339            if [ -n "$colsearch_pattern" ]; then
340                local red=$'\x1b[00;31m' reset=$'\x1b[00;00m'
341                # The repeat will make the matching work on a fresh heap
342                repeat 1; do
343                    if [ "$NLIST_IS_F_MODE" -eq "1" ]; then
344                        disp_list=( "${(@)disp_list//(#mia1)($~colsearch_pattern)/$red${MATCH}$reset}" )
345                    elif [ "$NLIST_IS_F_MODE" -eq "2" ]; then
346                        disp_list=( "${(@)disp_list//(#mia2)($~colsearch_pattern)/$red${MATCH}$reset}" )
347                    else
348                        disp_list=( "${(@)disp_list//(#mi)($~colsearch_pattern)/$red${MATCH}$reset}" )
349                    fi
350                done
351            fi
352
353            # We have display list, lets replace newlines with "\n" when needed (1/2)
354            [ "$NLIST_REPLACE_NEWLINES" -eq 1 ] && disp_list=( "${(@)disp_list//$'\n'/\\n}" )
355        fi
356
357        # Output colored list
358        zcurses clear inner
359        n-list-draw "$(( (NLIST_CURRENT_IDX-1) % page_height + 1 ))" \
360            "$page_height" "$page_width" 0 0 "$NLIST_TEXT_OFFSET" inner \
361            "$disp_list[@]"
362    else
363        # There is no search, but there was in previous loop
364        # OR
365        # Uniq mode was entered or left out
366        # -> compute new list
367        if [[ -n "$prev_search_buffer" || "$NLIST_IS_UNIQ_MODE" -ne "$prev_uniq_mode" ]]; then
368            prev_search_buffer=""
369            prev_uniq_mode="$NLIST_IS_UNIQ_MODE"
370            # regenerating list -> regenerating disp_list
371            prev_start_idx=-1
372
373            # Take all elements, including duplicates and non-selectables
374            typeset +U list
375            repeat 1; do
376                list=( "$@" )
377            done
378
379            # Remove non-selectable elements only when in uniq mode
380            [ "$NLIST_IS_UNIQ_MODE" -eq 1 ] && [ "$#NLIST_NONSELECTABLE_ELEMENTS" -gt 0 ] &&
381            for i in "${(nO)NLIST_NONSELECTABLE_ELEMENTS[@]}"; do
382                if [[ "$i" = <-> ]]; then
383                    list[$i]=()
384                fi
385            done
386
387            # Remove duplicates when in uniq mode
388            [ "$NLIST_IS_UNIQ_MODE" -eq 1 ] && typeset -U list
389
390            last_element="$#list"
391            # Called after processing list
392            _nlist_verify_vars
393        fi
394
395        # "1" - shouldn't bother with non-selectables
396        _nlist_setup_user_vars "$NLIST_IS_UNIQ_MODE"
397
398        integer end_idx=$(( NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN + page_height - 1 ))
399        [ "$end_idx" -gt "$last_element" ] && end_idx=last_element
400
401        if [ "$prev_start_idx" -ne "$NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN" ]; then
402            prev_start_idx="$NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN"
403            disp_list=( "${(@)list[NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN, end_idx]}" )
404
405            [ -n "$NLIST_COLORING_PATTERN" ] && _nlist_colorify_disp_list
406
407            # We have display list, lets replace newlines with "\n" when needed (2/2)
408            [ "$NLIST_REPLACE_NEWLINES" -eq 1 ] && disp_list=( "${(@)disp_list//$'\n'/\\n}" )
409        fi
410
411        # Output the list
412        zcurses clear inner
413        n-list-draw "$(( (NLIST_CURRENT_IDX-1) % page_height + 1 ))" \
414            "$page_height" "$page_width" 0 0 "$NLIST_TEXT_OFFSET" inner \
415            "$disp_list[@]"
416    fi
417
418    local status_msg_strlen
419    local keywordmsg=""
420    if [ "$keywordisfresh" = "1" ]; then
421        keywordmsg="($curkeyword/$nkeywords) "
422        keywordisfresh="0"
423    fi
424
425    local thememsg=""
426    if [ "$themeisfresh" = "1" ]; then
427        local theme="$backuptheme"
428        [ "$curtheme" -gt 0 ] && theme="${themes[curtheme]}"
429        thememsg="($curtheme/$nthemes $theme) "
430        themeisfresh="0"
431    fi
432
433    local _txt2="" _txt3=""
434    [ "$NLIST_IS_UNIQ_MODE" -eq 1 ] && _txt2="[-UNIQ-] "
435    [ "$NLIST_IS_F_MODE" -eq 1 ] && _txt3="[-FIX-] "
436    [ "$NLIST_IS_F_MODE" -eq 2 ] && _txt3="[-FIX2-] "
437
438    if [ "$NLIST_IS_SEARCH_MODE" = "1" ]; then
439        _nlist_status_msg "${_txt2}${_txt3}${keywordmsg}${thememsg}Filtering with: ${NLIST_SEARCH_BUFFER// /+}"
440    elif [[ ${NLIST_NONSELECTABLE_ELEMENTS[(r)$NLIST_CURRENT_IDX]} != $NLIST_CURRENT_IDX ||
441            -n "$NLIST_SEARCH_BUFFER" || "$NLIST_IS_UNIQ_MODE" -eq 1 ]]; then
442        local _txt=""
443        [ -n "$NLIST_GREP_STRING" ] && _txt=" [$NLIST_GREP_STRING]"
444        _nlist_status_msg "${_txt2}${_txt3}${keywordmsg}${thememsg}Current #$NLIST_USER_CURRENT_IDX (of #$NLIST_USER_LAST_ELEMENT entries)$_txt"
445    else
446        _nlist_status_msg "${keywordmsg}${thememsg}"
447    fi
448
449    [ "$border" = "1" ] && zcurses border main
450
451    local top_msg=" ${(C)ZSH_NAME} $ZSH_VERSION, shell level $SHLVL "
452    if [[ "${NLIST_ENABLED_EVENTS[(r)F1]}" = "F1" ]]; then
453        top_msg=" F1-change view,$top_msg"
454    fi
455    zcurses move main 0 $(( term_width / 2 - $#top_msg / 2 ))
456    zcurses string main $top_msg
457
458    zcurses refresh main inner
459    zcurses move main $(( term_height - 1 - 1 )) $(( status_msg_strlen + 2 ))
460
461    # Wait for input
462    zcurses input main key keypad
463
464    # Get the special (i.e. "keypad") key or regular key
465    if [ -n "$key" ]; then
466        final_key="$key"
467    elif [ -n "$keypad" ]; then
468        final_key="$keypad"
469    else
470        _nlist_status_msg "Improper input detected"
471        zcurses refresh main inner
472    fi
473
474    n-list-input "$NLIST_CURRENT_IDX" "$NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN" \
475                    "$page_height" "$page_width" "$last_element" "$NLIST_TEXT_OFFSET" \
476                    "$final_key" "$NLIST_IS_SEARCH_MODE" "$NLIST_SEARCH_BUFFER" \
477                    "$NLIST_IS_UNIQ_MODE" "$NLIST_IS_F_MODE"
478
479    selection="$reply[1]"
480    action="$reply[2]"
481    NLIST_CURRENT_IDX="$reply[3]"
482    NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN="$reply[4]"
483    NLIST_TEXT_OFFSET="$reply[5]"
484    NLIST_IS_SEARCH_MODE="$reply[6]"
485    NLIST_SEARCH_BUFFER="$reply[7]"
486    NLIST_IS_UNIQ_MODE="$reply[8]"
487    NLIST_IS_F_MODE="$reply[9]"
488
489    if [ -z "$action" ]; then
490        continue
491    elif [ "$action" = "SELECT" ]; then
492        REPLY="$selection"
493        reply=( "$list[@]" )
494        break
495    elif [ "$action" = "QUIT" ]; then
496        REPLY=-1
497        reply=( "$list[@]" )
498        break
499    elif [ "$action" = "REDRAW" ]; then
500        zcurses clear main redraw
501        zcurses clear inner redraw
502    elif [[ "$action" = F<-> ]]; then
503        REPLY="$action"
504        reply=( "$list[@]" )
505        break
506    elif [[ "$action" = "EDIT" ]]; then
507        REPLY="EDIT"
508        reply=( "$list[@]" )
509        break
510    elif [[ "$action" = "HELP" ]]; then
511        REPLY="HELP"
512        reply=( "$list[@]" )
513        break
514    fi
515done
516
517# vim: set filetype=zsh: