n-list 17 KB

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