n-list 16.9 KB
Newer Older
1
2
3
# $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
4
# $reply (array) is the second part of the output - use the index (REPLY) to get selected element
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#
# 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

32
    [[ "$REPLY" = -(#c0,1)[0-9]## || "$REPLY" = F<-> || "$REPLY" = "EDIT" || "$REPLY" = "HELP" ]] || REPLY="-1"
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
    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
}

120
_nlist_colorify_disp_list() {
121
122
123
124
125
    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
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
130
131
132
133
134
135
136
137
138
139
140
141
142
    fi
}

#
# Main code
#

# Check if there is proper input
if [ "$#" -lt 1 ]; then
    echo "Usage: n-list element_1 ..."
    return 1
fi

REPLY="-1"
143
typeset -ga reply
144
145
146
147
148
149
150
151
152
153
154
155
156
157
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

158
typeset -a list disp_list
159
160
161
162
163
164
165
166
167
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
168
local MBEGIN MEND MATCH mbegin mend match
169

170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# Iteration over predefined keywords
integer curkeyword nkeywords
local keywordisfresh="0"
if [[ "${(t)keywords}" != *array* ]]; then
    local -a keywords
    keywords=()
fi
curkeyword=0
nkeywords=${#keywords}

# Iteration over themes
integer curtheme nthemes
local themeisfresh="0"
if [[ "${(t)themes}" != *array* ]]; then
    local -a themes
    themes=()
fi
curtheme=0
nthemes=${#themes}

190
191
192
193
194
195
196
197
# 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
198
    NLIST_IS_F_MODE=0
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214

    # 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

215
216
217
218
219
if [ -n "$NLIST_SET_SEARCH_TO" ]; then
    NLIST_SEARCH_BUFFER="$NLIST_SET_SEARCH_TO"
    NLIST_SET_SEARCH_TO=""
fi

220
221
222
223
224
225
226
227
228
229
230
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
231
232
233
234
235
236
237
# From n-list.conf
[ "$colorpair" = "" ] && colorpair="white/black"
[ "$border" = "0" ] || border="1"
local background="${colorpair#*/}"
local backuptheme="$colorpair/$bold"
zcurses bg main "$colorpair"
zcurses bg inner "$colorpair"
238
239
240
241
if [ "$NLIST_IS_SEARCH_MODE" -ne 1 ]; then
    _nlist_cursor_visibility 0
fi

242
243
zcurses refresh

244
245
246
247
248
249
250
251
252
253
254
255
256
#
# Listening for input
#

local key keypad

# Clear input buffer
zcurses timeout main 0
zcurses input main key keypad
zcurses timeout main -1
key=""
keypad=""

257
258
259
260
261
# This loop makes script faster on some Zsh's (e.g. 5.0.8)
repeat 1; do
    list=( "$@" )
done

262
263
264
265
266
last_element="$#list"

while (( 1 )); do
    # Do searching (filtering with string)
    if [ -n "$NLIST_SEARCH_BUFFER" ]; then
267
        # Compute new list?
268
269
270
        if [[ "$NLIST_SEARCH_BUFFER" != "$prev_search_buffer" || "$NLIST_IS_UNIQ_MODE" -ne "$prev_uniq_mode"
                || "$NLIST_IS_F_MODE" -ne "$prev_f_mode" ]]
        then
271
272
            prev_search_buffer="$NLIST_SEARCH_BUFFER"
            prev_uniq_mode="$NLIST_IS_UNIQ_MODE"
273
            prev_f_mode="$NLIST_IS_F_MODE"
274
275
276
277
278
            # regenerating list -> regenerating disp_list
            prev_start_idx=-1

            # Take all elements, including duplicates and non-selectables
            typeset +U list
279
280
281
            repeat 1; do
                list=( "$@" )
            done
282
283
284

            # Remove non-selectable elements
            [ "$#NLIST_NONSELECTABLE_ELEMENTS" -gt 0 ] && for i in "${(nO)NLIST_NONSELECTABLE_ELEMENTS[@]}"; do
285
286
287
                if [[ "$i" = <-> ]]; then
                    list[$i]=()
                fi
288
289
290
291
292
293
294
295
296
297
298
            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}"
299
300
            local search_pattern=""
            local colsearch_pattern=""
301
            if [ -n "$search_buffer" ]; then
302
303
                # The repeat will make the matching work on a fresh heap
                repeat 1; do
304
305
306
307
308
309
310
311
312
313
314
315
316
317
                    if [ "$NLIST_IS_F_MODE" -eq "1" ]; then
                        search_pattern="${search_buffer// ##/*~^(#a1)*}"
                        colsearch_pattern="${search_buffer// ##/|(#a1)}"
                        list=( "${(@M)list:#(#ia1)*$~search_pattern*}" )
                    elif [ "$NLIST_IS_F_MODE" -eq "2" ]; then
                        search_pattern="${search_buffer// ##/*~^(#a2)*}"
                        colsearch_pattern="${search_buffer// ##/|(#a2)}"
                        list=( "${(@M)list:#(#ia2)*$~search_pattern*}" )
                    else
                        # Patterns will be *foo*~^*bar* and (foo|bar)
                        search_pattern="${search_buffer// ##/*~^*}"
                        colsearch_pattern="${search_buffer// ##/|}"
                        list=( "${(@M)list:#(#i)*$~search_pattern*}" )
                    fi
318
319
                done

320
321
322
323
324
325
326
327
328
329
330
331
332
333
                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"
334
335
336
337
            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'
338
339
                # The repeat will make the matching work on a fresh heap
                repeat 1; do
340
341
342
343
344
345
346
                    if [ "$NLIST_IS_F_MODE" -eq "1" ]; then
                        disp_list=( "${(@)disp_list//(#mia1)($~colsearch_pattern)/$red${MATCH}$reset}" )
                    elif [ "$NLIST_IS_F_MODE" -eq "2" ]; then
                        disp_list=( "${(@)disp_list//(#mia2)($~colsearch_pattern)/$red${MATCH}$reset}" )
                    else
                        disp_list=( "${(@)disp_list//(#mi)($~colsearch_pattern)/$red${MATCH}$reset}" )
                    fi
347
                done
348
            fi
349

350
            # We have display list, lets replace newlines with "\n" when needed (1/2)
351
            [ "$NLIST_REPLACE_NEWLINES" -eq 1 ] && disp_list=( "${(@)disp_list//$'\n'/\\n}" )
352
353
354
        fi

        # Output colored list
355
        zcurses clear inner
356
357
358
359
360
361
362
        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
363
        # -> compute new list
364
365
366
367
368
369
370
371
        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
372
373
374
            repeat 1; do
                list=( "$@" )
            done
375
376
377
378

            # 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
379
380
381
                if [[ "$i" = <-> ]]; then
                    list[$i]=()
                fi
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
            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

398
399
400
        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]}" )
401

402
            [ -n "$NLIST_COLORING_PATTERN" ] && _nlist_colorify_disp_list
403

404
405
            # We have display list, lets replace newlines with "\n" when needed (2/2)
            [ "$NLIST_REPLACE_NEWLINES" -eq 1 ] && disp_list=( "${(@)disp_list//$'\n'/\\n}" )
406
407
408
        fi

        # Output the list
409
        zcurses clear inner
410
411
412
413
414
415
        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
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
    local keywordmsg=""
    if [ "$keywordisfresh" = "1" ]; then
        keywordmsg="($curkeyword/$nkeywords) "
        keywordisfresh="0"
    fi

    local thememsg=""
    if [ "$themeisfresh" = "1" ]; then
        local theme="$backuptheme"
        [ "$curtheme" -gt 0 ] && theme="${themes[curtheme]}"
        thememsg="($curtheme/$nthemes $theme) "
        themeisfresh="0"
    fi

    local _txt2="" _txt3=""
    [ "$NLIST_IS_UNIQ_MODE" -eq 1 ] && _txt2="[-UNIQ-] "
    [ "$NLIST_IS_F_MODE" -eq 1 ] && _txt3="[-FIX-] "
    [ "$NLIST_IS_F_MODE" -eq 2 ] && _txt3="[-FIX2-] "

435
    if [ "$NLIST_IS_SEARCH_MODE" = "1" ]; then
436
        _nlist_status_msg "${_txt2}${_txt3}${keywordmsg}${thememsg}Filtering with: ${NLIST_SEARCH_BUFFER// /+}"
437
438
    elif [[ ${NLIST_NONSELECTABLE_ELEMENTS[(r)$NLIST_CURRENT_IDX]} != $NLIST_CURRENT_IDX ||
            -n "$NLIST_SEARCH_BUFFER" || "$NLIST_IS_UNIQ_MODE" -eq 1 ]]; then
439
        local _txt=""
440
        [ -n "$NLIST_GREP_STRING" ] && _txt=" [$NLIST_GREP_STRING]"
441
        _nlist_status_msg "${_txt2}${_txt3}${keywordmsg}${thememsg}Current #$NLIST_USER_CURRENT_IDX (of #$NLIST_USER_LAST_ELEMENT entries)$_txt"
442
    else
443
        _nlist_status_msg "${keywordmsg}${thememsg}"
444
445
    fi

446
    [ "$border" = "1" ] && zcurses border main
447

448
    local top_msg=" F1-change view, ${(C)ZSH_NAME} $ZSH_VERSION, shell level $SHLVL "
449
450
451
    zcurses move main 0 $(( term_width / 2 - $#top_msg / 2 ))
    zcurses string main $top_msg

452
453
454
455
456
457
458
459
    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
460
        final_key="$key"
461
462
463
464
465
466
467
468
469
470
    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" \
471
                    "$NLIST_IS_UNIQ_MODE" "$NLIST_IS_F_MODE"
472
473
474
475
476
477
478
479
480

    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]"
481
    NLIST_IS_F_MODE="$reply[9]"
482

483
484
485
    if [ -z "$action" ]; then
        continue
    elif [ "$action" = "SELECT" ]; then
486
487
488
489
490
491
492
493
494
495
        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
496
497
498
499
500
501
502
503
504
505
506
507
    elif [[ "$action" = F<-> ]]; then
        REPLY="$action"
        reply=( "$list[@]" )
        break
    elif [[ "$action" = "EDIT" ]]; then
        REPLY="EDIT"
        reply=( "$list[@]" )
        break
    elif [[ "$action" = "HELP" ]]; then
        REPLY="HELP"
        reply=( "$list[@]" )
        break
508
509
510
511
    fi
done

# vim: set filetype=zsh: