From 1d68cda473d51cc75d0b880f1fa3f3d88ec3732f Mon Sep 17 00:00:00 2001 From: Benjamin Lupton Date: Thu, 26 Oct 2023 15:01:03 +0800 Subject: [PATCH] choose-menu: support paging (close #97) --- commands/choose-menu | 197 +++++++++++++++++++++++++++-------------- commands/choose-option | 2 +- commands/is-tty | 5 +- 3 files changed, 135 insertions(+), 69 deletions(-) diff --git a/commands/choose-menu b/commands/choose-menu index 88ecdeae5..0d9ef575a 100755 --- a/commands/choose-menu +++ b/commands/choose-menu @@ -171,13 +171,13 @@ function choose_menu() ( fi # prepare - local count last selections=() + local count last_index selections=() count="${#items[@]}" - last="$((count - 1))" + last_index="$((count - 1))" mapfile -t selections < <(get-array "$count") # default selection - local cursor=0 + local cursor=0 page_start_index=0 page_last_index=0 selected_count=0 local index default if test "${#defaults[@]}" -ne 0; then # bash v3 compat if test "${#defaults[@]}" -gt 1 -a "$option_multi" = 'no'; then @@ -190,6 +190,7 @@ function choose_menu() ( selections[index]='yes' if test "$option_multi" = 'no'; then cursor="$index" + page_start_index="$index" fi break fi @@ -199,9 +200,9 @@ function choose_menu() ( # commence tty_start - local read_status menu_status=0 menu='' action='' tty_target + local read_status menu_status=0 menu_title='' menu_header='' menu_header_shrunk='' menu_hint='' menu_hint_shrunk='' menu_items='' menu_item='' action='' using_tty_stderr_fallback='no' tty_target local magenta=$'\e[35m' bold=$'\e[1m' dim=$'\e[2m' reset=$'\e[0m' - local help_begin="$dim" help_end="$reset" key_begin key_end=" $reset" indent=' ' + local help_begin="$dim" help_end="$reset" key_begin key_end=" $reset" checked='●' unchecked='○' selected='☉' indent=' ' if test "$use_colors" = 'no'; then magenta='' bold='' @@ -216,80 +217,126 @@ function choose_menu() ( else key_begin=$'\e[107m ' # foreground default black, background intense white fi - tty_target="$(is-tty --fallback)" had_selected='no' - while test "$action" != 'done'; do - menu='' - - # question - if test -n "$option_question"; then - menu+="${bold}${option_question}${reset}"$'\n' + if test "$option_multi" = 'yes'; then + checked='▣' + unchecked='□' + selected='⊡' + fi + if test "$option_hints" = 'yes'; then + if test "$option_multi" = 'no'; then + menu_hint+="${help_begin}SELECT${help_end} ${key_begin}ENTER${key_end} ${key_begin}SPACE${key_end}" + else + menu_hint+="${help_begin}SELECT${help_end} ${key_begin}SPACE${key_end}" fi + if test "$count" -ne 1; then + # [⬆⬇⇧] have alignment issues, use [↑↓] + menu_hint+="${indent}${help_begin}UP${help_end} ${key_begin}↑${key_end} ${key_begin}W${key_end} ${key_begin}K${key_end}" + menu_hint+="${indent}${help_begin}DOWN${help_end} ${key_begin}↓${key_end} ${key_begin}S${key_end} ${key_begin}J${key_end}" + if test "$option_multi" = 'yes'; then + menu_hint+="${indent}${help_begin}ALL${help_end} ${key_begin}A${key_end}" + menu_hint+="${indent}${help_begin}NONE${help_end} ${key_begin}D${key_end}" + fi + fi + if test "$option_required" = 'no'; then + menu_hint+="${indent}${help_begin}CANCEL${help_end} ${key_begin}ESC${key_end}" + fi + fi + if test -n "$option_question"; then + menu_header="${bold}${option_question}${reset}"$'\n' + fi + tty_target="$(is-tty --fallback)" + if test "$tty_target" = '/dev/stderr'; then + # fix [tput: No value for $TERM and no -T specified] errors when fetching columns and rows on CI + using_tty_stderr_fallback=yes + fi + # this is to slow to recalculate on each interaction + local columns rows content_columns skip_remainder item_renders item_render menu_size needed_paging last_rows=0 last_columns=0 + mapfile -t item_renders < <(get-array "$count") + function refresh_terminal_size { + if test "$using_tty_stderr_fallback" = 'yes'; then + needed_paging='no' + else + rows="$(tput lines)" + columns="$(tput cols)" + if test "$rows" -ne "$last_rows" -o "$columns" -ne "$last_columns"; then + content_columns="$((columns - 5))" + menu_header_shrunk="$(echo-trim-colors "$menu_header" | gfold -w "$columns")"$'\n' + menu_hint_shrunk="$(echo-trim-colors "$menu_hint" | gfold -w "$columns")" + last_rows="$(tput lines)" + last_columns="$(tput cols)" + needed_paging='maybe' + page_start_index="$cursor" + fi + fi + } + while test "$action" != 'done'; do # show the menu - had_selected='no' + # one hollow circle: ⚬ ○ ◯ ❍ + # two hollow circles: ◎ ⦾ ⊚ + # one hollow, one full: ☉ ⦿ ◉ + # one full: ● + # ▣ ▢ □ ⊡ + # ☑ ☒ ⌧ + # ✓ ✔ ✖ ✗ ✘ + refresh_terminal_size + skip_remainder='no' + menu_items='' + selected_count=0 for index in "${!items[@]}"; do - if test "$index" -eq "$cursor"; then - menu+="$magenta>" + if test "${selections[index]-}" = 'yes'; then + selected_count=$((selected_count + 1)) + fi + if test "$index" -lt "$page_start_index" -o "$skip_remainder" = 'yes'; then + continue + fi + menu_item='' + if test "$index" -eq "$cursor" -a "${selections[index]-}" = 'yes'; then + menu_item+="$magenta> $checked " + elif test "${selections[index]-}" = 'yes'; then + menu_item+="$magenta $checked " + elif test "$index" -eq "$cursor"; then + menu_item+="$magenta> $selected " else - if test "${selections[index]-}" = 'yes'; then - menu+=" $magenta" - else - menu+=' ' - fi + menu_item+=" $unchecked " fi - if test "$option_multi" = 'yes'; then - # one hollow circle: ⚬ ○ ◯ ❍ - # two hollow circles: ◎ ⦾ ⊚ - # one hollow, one full: ☉ ⦿ ◉ - # one full: ● - # ▣ ▢ □ ⊡ - # ☑ ☒ - # ✓ ✔ ✖ ✗ ✘ - if test "${selections[index]-}" = 'yes'; then - had_selected='yes' - menu+=" ⦿ " + if test "$needed_paging" = 'no'; then + menu_items+="${menu_item}${items[index]}${reset}"$'\n' + else + if test -n "${item_renders[index]-}"; then + menu_item+="${item_renders[index]}" else - menu+=' ○ ' + item_render="$(gfmt -t -w "$content_columns" <<<"${items[index]}")${reset}"$'\n' + menu_item+="$item_render" + item_renders[index]="$item_render" fi - else - if test "${selections[index]-}" = 'yes'; then - had_selected='yes' - menu+=" $bold" + menu_size="$(wc -l <<<"${menu_header_shrunk}${menu_items}${menu_item}${menu_hint_shrunk}")" + menu_size="$((menu_size))" # it needs trimming for some reason, and this trims it + if test "$menu_size" -gt "$rows"; then + skip_remainder='yes' else - menu+=' ' + menu_items+="$menu_item" + page_last_index="$index" fi fi - menu+="${items[index]}${reset}"$'\n' done - # hints - if test "$option_hints" = 'yes'; then - if test "$option_multi" = 'no'; then - menu+="${help_begin}SELECT${help_end} ${key_begin}ENTER${key_end} ${key_begin}SPACE${key_end}" + # output menu + if test "$using_tty_stderr_fallback" = 'no'; then + if test "$page_start_index" -ne 0 -o "$page_last_index" -ne "$last_index"; then + needed_paging='yes' + menu_title=$'\e]0;'"👋 $count items 🙌 showing $page_start_index to $page_last_index 💁‍♀️ hiding $((count - (page_last_index - page_start_index))) 🫣 selected $selected_count ✅"$'\a' else - menu+="${help_begin}SELECT${help_end} ${key_begin}SPACE${key_end}" - fi - if test "$count" -ne 1; then - # [⬆⬇⇧] have alignment issues, use [↑↓] - menu+="${indent}${help_begin}UP${help_end} ${key_begin}↑${key_end} ${key_begin}W${key_end} ${key_begin}K${key_end}" - menu+="${indent}${help_begin}DOWN${help_end} ${key_begin}↓${key_end} ${key_begin}S${key_end} ${key_begin}J${key_end}" - if test "$option_multi" = 'yes'; then - menu+="${indent}${help_begin}ALL${help_end} ${key_begin}A${key_end}" - menu+="${indent}${help_begin}NONE${help_end} ${key_begin}D${key_end}" - fi - fi - if test "$option_required" = 'no'; then - menu+="${indent}${help_begin}CANCEL${help_end} ${key_begin}ESC${key_end}" + menu_title=$'\e]0;'"👋 $count items 🙌 selected $selected_count ✅"$'\a' + needed_paging='no' fi fi - - # output menu - printf '%s' "$menu" >"$tty_target" + printf '%s' "${menu_title}${menu_header}${menu_items}${menu_hint}" >"$tty_target" # handle the response eval_capture --statusvar=read_status --stdoutvar=action -- read-key --timeout="$option_timeout" if test "$read_status" -eq 60; then - if test "$had_selected" = 'yes'; then + if test "$selected_count" -ne 0; then tty_clear echo-style --colors="$use_colors" --notice="Read timed out [$read_status], using selection." >/dev/stderr sleep 3 @@ -311,7 +358,7 @@ function choose_menu() ( fi # reset selection if not multi - if test "$had_selected" = 'yes' -a "$option_multi" = 'no'; then + if test "$selected_count" -ne 0 -a "$option_multi" = 'no'; then for index in "${!selections[@]}"; do selections[index]='' done @@ -326,7 +373,7 @@ function choose_menu() ( elif test "$action" -le "$count"; then cursor="$((action - 1))" else - cursor="$last" + cursor="$last_index" fi action='space' elif test "$action" = 'left' -o "$action" = 'h' -o "$action" = 'k' -o "$action" = 'w'; then @@ -335,6 +382,10 @@ function choose_menu() ( action='down' elif test "$action" = 'd' -o "$action" = 'backspace'; then action='none' + elif test "$action" = 'q'; then + action='home' + elif test "$action" = 'e'; then + action='end' elif test "$action" = 'a'; then action='all' fi @@ -342,20 +393,34 @@ function choose_menu() ( # control key if test "$action" = 'up'; then if test "$cursor" -eq 0; then - cursor="$last" - elif test "$cursor" -ne 0; then + if test "$needed_paging" = 'yes'; then + page_start_index="$last_index" + fi + cursor="$last_index" + else + if test "$cursor" -eq "$page_start_index"; then + page_start_index="$((page_start_index - 1))" + fi cursor="$((cursor - 1))" fi elif test "$action" = 'down'; then - if test "$cursor" -eq "$last"; then + if test "$cursor" -eq "$last_index"; then cursor=0 - elif test "$cursor" -ne "$last"; then + page_start_index=0 + else + if test "$needed_paging" = 'yes' -a "$cursor" -eq "$page_last_index"; then + page_start_index="$((page_last_index + 1))" # ="$next_page_start_index" + fi cursor="$((cursor + 1))" fi elif test "$action" = 'home'; then cursor=0 + page_start_index=0 elif test "$action" = 'end'; then - cursor="$last" + if test "$needed_paging" = 'yes'; then + page_start_index="$last_index" + fi + cursor="$last_index" elif test "$action" = 'none'; then # if multi then unselect everything, other no-op (as any keypress by now would have cleared the non-multi selection) if test "$option_multi" = 'yes'; then diff --git a/commands/choose-option b/commands/choose-option index cb677b883..f52e39a0a 100755 --- a/commands/choose-option +++ b/commands/choose-option @@ -442,7 +442,7 @@ function choose_option() ( if test "$action" = 'select'; then if test "${#filtered_visuals[@]}" -ne "${#visuals[@]}"; then unfiltered_index="${#filtered_visuals[@]}" - filtered_visuals+=('Select from the unfiltered options.') + filtered_visuals+=("Select this to see the $(("${#visuals[@]}" - "${#filtered_visuals[@]}")) unfiltered options.") fi # trigger the menu, and add each default individually, supporting multi-line visuals diff --git a/commands/is-tty b/commands/is-tty index ff5a79995..3b7870141 100755 --- a/commands/is-tty +++ b/commands/is-tty @@ -159,9 +159,10 @@ function is_tty() ( print_line '/dev/stderr' fi return 0 + elif (: /dev/tty) &>/dev/null; then + return 0 else - (: /dev/tty) &>/dev/null - return + return 1 fi )