Skip to content

Commit

Permalink
choose-menu: support paging (close #97)
Browse files Browse the repository at this point in the history
  • Loading branch information
balupton committed Oct 26, 2023
1 parent 733b1a1 commit 1d68cda
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 69 deletions.
197 changes: 131 additions & 66 deletions commands/choose-menu
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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=''
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -335,27 +382,45 @@ 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

# 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
Expand Down
2 changes: 1 addition & 1 deletion commands/choose-option
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions commands/is-tty
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,10 @@ function is_tty() (
print_line '/dev/stderr'
fi
return 0
elif (: </dev/tty >/dev/tty) &>/dev/null; then
return 0
else
(: </dev/tty >/dev/tty) &>/dev/null
return
return 1
fi
)

Expand Down

0 comments on commit 1d68cda

Please sign in to comment.