n-list 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  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. #
  5. # Copy this file into /usr/share/zsh/site-functions/
  6. # and add 'autoload n-list` to .zshrc
  7. #
  8. # This function outputs a list of elements that can be
  9. # navigated with keyboard. Uses curses library
  10. emulate -LR zsh
  11. setopt typesetsilent extendedglob noshortloops
  12. _nlist_has_terminfo=0
  13. zmodload zsh/curses
  14. zmodload zsh/terminfo 2>/dev/null && _nlist_has_terminfo=1
  15. trap "REPLY=-2; reply=(); return" TERM INT QUIT
  16. trap "_nlist_exit" EXIT
  17. # Drawing and input
  18. autoload n-list-draw n-list-input
  19. # Cleanup before any exit
  20. _nlist_exit() {
  21. setopt localoptions
  22. setopt extendedglob
  23. [[ "$REPLY" = -(#c0,1)[0-9]## ]] || REPLY="-1"
  24. zcurses 2>/dev/null delwin inner
  25. zcurses 2>/dev/null delwin main
  26. zcurses 2>/dev/null refresh
  27. zcurses end
  28. _nlist_alternate_screen 0
  29. _nlist_cursor_visibility 1
  30. unset _nlist_has_terminfo
  31. }
  32. # Outputs a message in the bottom of the screen
  33. _nlist_status_msg() {
  34. # -1 for border, -1 for 0-based indexing
  35. zcurses move main $(( term_height - 1 - 1 )) 2
  36. zcurses clear main eol
  37. zcurses string main "$1"
  38. #status_msg_strlen is localized in caller
  39. status_msg_strlen=$#1
  40. }
  41. # Prefer tput, then module terminfo
  42. _nlist_cursor_visibility() {
  43. if type tput 2>/dev/null 1>&2; then
  44. [ "$1" = "1" ] && { tput cvvis; tput cnorm }
  45. [ "$1" = "0" ] && tput civis
  46. elif [ "$_nlist_has_terminfo" = "1" ]; then
  47. [ "$1" = "1" ] && { [ -n $terminfo[cvvis] ] && echo -n $terminfo[cvvis];
  48. [ -n $terminfo[cnorm] ] && echo -n $terminfo[cnorm] }
  49. [ "$1" = "0" ] && [ -n $terminfo[civis] ] && echo -n $terminfo[civis]
  50. fi
  51. }
  52. # Reason for this function is that on some systems
  53. # smcup and rmcup are not knowing why left empty
  54. _nlist_alternate_screen() {
  55. [ "$_nlist_has_terminfo" -ne "1" ] && return
  56. [[ "$1" = "1" && -n "$terminfo[smcup]" ]] && return
  57. [[ "$1" = "0" && -n "$terminfo[rmcup]" ]] && return
  58. case "$TERM" in
  59. *rxvt*)
  60. [ "$1" = "1" ] && echo -n $'\x1b7\x1b[?47h'
  61. [ "$1" = "0" ] && echo -n $'\x1b[2J\x1b[?47l\x1b8'
  62. ;;
  63. *)
  64. [ "$1" = "1" ] && echo -n $'\x1b[?1049h'
  65. [ "$1" = "0" ] && echo -n $'\x1b[?1049l'
  66. # just to remember two other that work: $'\x1b7\x1b[r\x1b[?47h', $'\x1b[?47l\x1b8'
  67. ;;
  68. esac
  69. }
  70. _nlist_compute_user_vars_difference() {
  71. if [[ "${(t)NLIST_NONSELECTABLE_ELEMENTS}" != "array" &&
  72. "${(t)NLIST_NONSELECTABLE_ELEMENTS}" != "array-local" ]]
  73. then
  74. last_element_difference=0
  75. current_difference=0
  76. else
  77. last_element_difference=$#NLIST_NONSELECTABLE_ELEMENTS
  78. current_difference=0
  79. local idx
  80. for idx in "${(n)NLIST_NONSELECTABLE_ELEMENTS[@]}"; do
  81. [ "$idx" -le "$NLIST_CURRENT_IDX" ] && current_difference+=1 || break
  82. done
  83. fi
  84. }
  85. # List was processed, check if variables aren't off range
  86. _nlist_verify_vars() {
  87. [ "$NLIST_CURRENT_IDX" -gt "$last_element" ] && NLIST_CURRENT_IDX="$last_element"
  88. [[ "$NLIST_CURRENT_IDX" -eq 0 && "$last_element" -ne 0 ]] && NLIST_CURRENT_IDX=1
  89. (( NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN=0+((NLIST_CURRENT_IDX-1)/page_height)*page_height+1 ))
  90. }
  91. # Compute the variables which are shown to the user
  92. _nlist_setup_user_vars() {
  93. if [ "$1" = "1" ]; then
  94. # Basic values when there are no non-selectables
  95. NLIST_USER_CURRENT_IDX="$NLIST_CURRENT_IDX"
  96. NLIST_USER_LAST_ELEMENT="$last_element"
  97. else
  98. _nlist_compute_user_vars_difference
  99. NLIST_USER_CURRENT_IDX=$(( NLIST_CURRENT_IDX - current_difference ))
  100. NLIST_USER_LAST_ELEMENT=$(( last_element - last_element_difference ))
  101. fi
  102. }
  103. _nlist_coloring_list_into_col_list() {
  104. local col=$'\x1b[00;34m' reset=$'\x1b[0m'
  105. [ -n "$NLIST_COLORING_COLOR" ] && col="$NLIST_COLORING_COLOR"
  106. [ -n "$NLIST_COLORING_END_COLOR" ] && reset="$NLIST_COLORING_END_COLOR"
  107. if [ "$NLIST_COLORING_MATCH_MULTIPLE" -eq 1 ]; then
  108. col_list=( "${(@)list//(#mi)$~NLIST_COLORING_PATTERN/$col${MATCH}$reset}" )
  109. else
  110. col_list=( "${(@)list/(#mi)$~NLIST_COLORING_PATTERN/$col${MATCH}$reset}" )
  111. fi
  112. }
  113. #
  114. # Main code
  115. #
  116. # Check if there is proper input
  117. if [ "$#" -lt 1 ]; then
  118. echo "Usage: n-list element_1 ..."
  119. return 1
  120. fi
  121. REPLY="-1"
  122. reply=()
  123. integer term_height="$LINES"
  124. integer term_width="$COLUMNS"
  125. if [[ "$term_height" -lt 1 || "$term_width" -lt 1 ]]; then
  126. local stty_out=$( stty size )
  127. term_height="${stty_out% *}"
  128. term_width="${stty_out#* }"
  129. fi
  130. integer inner_height=term_height-3
  131. integer inner_width=term_width-3
  132. integer page_height=inner_height
  133. integer page_width=inner_width
  134. typeset -a list col_list disp_list
  135. integer last_element=$#
  136. local action
  137. local final_key
  138. integer selection
  139. integer last_element_difference=0
  140. integer current_difference=0
  141. local prev_search_buffer=""
  142. integer prev_uniq_mode=0
  143. integer prev_start_idx=-1
  144. # Ability to remember the list between calls
  145. if [[ -z "$NLIST_REMEMBER_STATE" || "$NLIST_REMEMBER_STATE" -eq 0 || "$NLIST_REMEMBER_STATE" -eq 2 ]]; then
  146. NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN=1
  147. NLIST_CURRENT_IDX=1
  148. NLIST_IS_SEARCH_MODE=0
  149. NLIST_SEARCH_BUFFER=""
  150. NLIST_TEXT_OFFSET=0
  151. NLIST_IS_UNIQ_MODE=0
  152. # Zero - because it isn't known, unless we
  153. # confirm that first element is selectable
  154. NLIST_USER_CURRENT_IDX=0
  155. [[ ${NLIST_NONSELECTABLE_ELEMENTS[(r)1]} != 1 ]] && NLIST_USER_CURRENT_IDX=1
  156. NLIST_USER_LAST_ELEMENT=$(( last_element - $#NLIST_NONSELECTABLE_ELEMENTS ))
  157. # 2 is init once, then remember
  158. [ "$NLIST_REMEMBER_STATE" -eq 2 ] && NLIST_REMEMBER_STATE=1
  159. fi
  160. if [ "$NLIST_START_IN_SEARCH_MODE" -eq 1 ]; then
  161. NLIST_START_IN_SEARCH_MODE=0
  162. NLIST_IS_SEARCH_MODE=1
  163. fi
  164. if [ "$NLIST_START_IN_UNIQ_MODE" -eq 1 ]; then
  165. NLIST_START_IN_UNIQ_MODE=0
  166. NLIST_IS_UNIQ_MODE=1
  167. fi
  168. _nlist_alternate_screen 1
  169. zcurses init
  170. zcurses delwin main 2>/dev/null
  171. zcurses delwin inner 2>/dev/null
  172. zcurses addwin main "$term_height" "$term_width" 0 0
  173. zcurses addwin inner "$inner_height" "$inner_width" 1 2
  174. zcurses bg main white/black
  175. zcurses bg inner white/black
  176. if [ "$NLIST_IS_SEARCH_MODE" -ne 1 ]; then
  177. _nlist_cursor_visibility 0
  178. fi
  179. #
  180. # Listening for input
  181. #
  182. local key keypad
  183. # Clear input buffer
  184. zcurses timeout main 0
  185. zcurses input main key keypad
  186. zcurses timeout main -1
  187. key=""
  188. keypad=""
  189. list=( "$@" )
  190. last_element="$#list"
  191. integer is_colored=0
  192. if [[ -z "$NLIST_SEARCH_BUFFER" && -n "$NLIST_COLORING_PATTERN" ]]; then
  193. is_colored=1
  194. _nlist_coloring_list_into_col_list
  195. fi
  196. while (( 1 )); do
  197. # Do searching (filtering with string)
  198. if [ -n "$NLIST_SEARCH_BUFFER" ]; then
  199. # Compute new list, col_list ?
  200. if [[ "$NLIST_SEARCH_BUFFER" != "$prev_search_buffer" || "$NLIST_IS_UNIQ_MODE" -ne "$prev_uniq_mode" ]]; then
  201. prev_search_buffer="$NLIST_SEARCH_BUFFER"
  202. prev_uniq_mode="$NLIST_IS_UNIQ_MODE"
  203. # regenerating list -> regenerating disp_list
  204. prev_start_idx=-1
  205. # Take all elements, including duplicates and non-selectables
  206. typeset +U list
  207. list=( "$@" )
  208. # Remove non-selectable elements
  209. [ "$#NLIST_NONSELECTABLE_ELEMENTS" -gt 0 ] && for i in "${(nO)NLIST_NONSELECTABLE_ELEMENTS[@]}"; do
  210. list[$i]=()
  211. done
  212. # Remove duplicates
  213. [ "$NLIST_IS_UNIQ_MODE" -eq 1 ] && typeset -U list
  214. last_element="$#list"
  215. # Next do the filtering
  216. local search_buffer="${NLIST_SEARCH_BUFFER%% ##}"
  217. search_buffer="${search_buffer## ##}"
  218. search_buffer="${search_buffer//(#m)[][*?|#~^()><\\]/\\$MATCH}"
  219. if [ -n "$search_buffer" ]; then
  220. # Patterns will be *foo*~^*bar* and foo|bar)
  221. local search_pattern="${search_buffer// ##/*~^*}"
  222. local colsearch_pattern="${search_buffer// ##/|}"
  223. list=( "${(@M)list:#(#i)*$~search_pattern*}" )
  224. last_element="$#list"
  225. local red=$'\x1b[00;31m' reset=$'\x1b[00;00m'
  226. col_list=( "${(@)list//(#mi)($~colsearch_pattern)/$red${MATCH}$reset}" )
  227. else
  228. col_list=( "$list[@]" )
  229. fi
  230. # Called after processing list
  231. _nlist_verify_vars
  232. fi
  233. _nlist_setup_user_vars 1
  234. integer end_idx=$(( NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN + page_height - 1 ))
  235. [ "$end_idx" -gt "$last_element" ] && end_idx=last_element
  236. if [ "$prev_start_idx" -ne "$NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN" ]; then
  237. prev_start_idx="$NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN"
  238. disp_list=( "${(@)col_list[NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN, end_idx]}" )
  239. fi
  240. # Output colored list
  241. n-list-draw "$(( (NLIST_CURRENT_IDX-1) % page_height + 1 ))" \
  242. "$page_height" "$page_width" 0 0 "$NLIST_TEXT_OFFSET" inner \
  243. "$disp_list[@]"
  244. else
  245. # There is no search, but there was in previous loop
  246. # OR
  247. # Uniq mode was entered or left out
  248. # -> compute new list (maybe also col_list)
  249. if [[ -n "$prev_search_buffer" || "$NLIST_IS_UNIQ_MODE" -ne "$prev_uniq_mode" ]]; then
  250. prev_search_buffer=""
  251. prev_uniq_mode="$NLIST_IS_UNIQ_MODE"
  252. # regenerating list -> regenerating disp_list
  253. prev_start_idx=-1
  254. # Take all elements, including duplicates and non-selectables
  255. typeset +U list
  256. list=( "$@" )
  257. # Remove non-selectable elements only when in uniq mode
  258. [ "$NLIST_IS_UNIQ_MODE" -eq 1 ] && [ "$#NLIST_NONSELECTABLE_ELEMENTS" -gt 0 ] &&
  259. for i in "${(nO)NLIST_NONSELECTABLE_ELEMENTS[@]}"; do
  260. list[$i]=()
  261. done
  262. # Remove duplicates when in uniq mode
  263. [ "$NLIST_IS_UNIQ_MODE" -eq 1 ] && typeset -U list
  264. # Apply coloring pattern (when not with search query)
  265. is_colored=0
  266. if [ -n "$NLIST_COLORING_PATTERN" ]; then
  267. is_colored=1
  268. _nlist_coloring_list_into_col_list
  269. fi
  270. last_element="$#list"
  271. # Called after processing list
  272. _nlist_verify_vars
  273. fi
  274. # "1" - shouldn't bother with non-selectables
  275. _nlist_setup_user_vars "$NLIST_IS_UNIQ_MODE"
  276. integer end_idx=$(( NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN + page_height - 1 ))
  277. [ "$end_idx" -gt "$last_element" ] && end_idx=last_element
  278. if [ "$is_colored" -eq 0 ]; then
  279. if [ "$prev_start_idx" -ne "$NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN" ]; then
  280. prev_start_idx="$NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN"
  281. disp_list=( "${(@)list[NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN, end_idx]}" )
  282. fi
  283. else
  284. if [ "$prev_start_idx" -ne "$NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN" ]; then
  285. prev_start_idx="$NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN"
  286. disp_list=( "${(@)col_list[NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN, end_idx]}" )
  287. fi
  288. fi
  289. # Output the list
  290. n-list-draw "$(( (NLIST_CURRENT_IDX-1) % page_height + 1 ))" \
  291. "$page_height" "$page_width" 0 0 "$NLIST_TEXT_OFFSET" inner \
  292. "$disp_list[@]"
  293. fi
  294. local status_msg_strlen
  295. if [ "$NLIST_IS_SEARCH_MODE" = "1" ]; then
  296. local _txt2=""
  297. [ "$NLIST_IS_UNIQ_MODE" -eq 1 ] && _txt2="[-UNIQ-] "
  298. _nlist_status_msg "${_txt2}Filtering with: ${NLIST_SEARCH_BUFFER// /+}"
  299. elif [[ ${NLIST_NONSELECTABLE_ELEMENTS[(r)$NLIST_CURRENT_IDX]} != $NLIST_CURRENT_IDX ||
  300. -n "$NLIST_SEARCH_BUFFER" || "$NLIST_IS_UNIQ_MODE" -eq 1 ]]; then
  301. local _txt="" _txt2=""
  302. [ -n "$NLIST_GREP_STRING" ] && _txt=" [$NLIST_GREP_STRING]"
  303. [ "$NLIST_IS_UNIQ_MODE" -eq 1 ] && _txt2="[-UNIQ-] "
  304. _nlist_status_msg "${_txt2}Current #$NLIST_USER_CURRENT_IDX (of #$NLIST_USER_LAST_ELEMENT entries)$_txt"
  305. else
  306. _nlist_status_msg ""
  307. fi
  308. zcurses border main
  309. zcurses refresh main inner
  310. zcurses move main $(( term_height - 1 - 1 )) $(( status_msg_strlen + 2 ))
  311. # Wait for input
  312. zcurses input main key keypad
  313. # Get the special (i.e. "keypad") key or regular key
  314. if [ -n "$key" ]; then
  315. final_key="$key"
  316. elif [ -n "$keypad" ]; then
  317. final_key="$keypad"
  318. else
  319. _nlist_status_msg "Inproper input detected"
  320. zcurses refresh main inner
  321. fi
  322. n-list-input "$NLIST_CURRENT_IDX" "$NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN" \
  323. "$page_height" "$page_width" "$last_element" "$NLIST_TEXT_OFFSET" \
  324. "$final_key" "$NLIST_IS_SEARCH_MODE" "$NLIST_SEARCH_BUFFER" \
  325. "$NLIST_IS_UNIQ_MODE"
  326. selection="$reply[1]"
  327. action="$reply[2]"
  328. NLIST_CURRENT_IDX="$reply[3]"
  329. NLIST_FROM_WHAT_IDX_LIST_IS_SHOWN="$reply[4]"
  330. NLIST_TEXT_OFFSET="$reply[5]"
  331. NLIST_IS_SEARCH_MODE="$reply[6]"
  332. NLIST_SEARCH_BUFFER="$reply[7]"
  333. NLIST_IS_UNIQ_MODE="$reply[8]"
  334. if [ "$action" = "SELECT" ]; then
  335. REPLY="$selection"
  336. reply=( "$list[@]" )
  337. break
  338. elif [ "$action" = "QUIT" ]; then
  339. REPLY=-1
  340. reply=( "$list[@]" )
  341. break
  342. elif [ "$action" = "REDRAW" ]; then
  343. zcurses clear main redraw
  344. zcurses clear inner redraw
  345. fi
  346. done
  347. # vim: set filetype=zsh: