Files
q-tools/shell/select-option.sh
2023-08-17 09:11:45 +03:00

315 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
## Function start
function select_option {
local nformat
local optcount=$#
printf -v nformat "%%%dd. " ${#optcount}
local max_opt=$(( ${#SELECT_TITLE} - 4 ))
local multiselected=()
local choices=()
local shortcuts=()
local idx=0
local clean_opt
local selected=$SELECT_SELECT
for opt; do
multiselected+=(0)
if [[ "$opt" =~ ^\[.\] ]]; then # choice starts with [x]
shortcuts+=( "${opt:1:1}" )
choices+=( "${opt:3}" )
else
shortcuts+=( "${opt:0:1}" )
choices+=( "$opt" )
fi
if [[ "${#choices[$idx]}" -gt $max_opt ]]; then
max_opt="${#choices[$idx]}"
fi
((idx++))
done
if [[ $SELECT_CENTER -eq 1 ]]; then
local pad_center=$(( ( $( tput cols ) - $max_opt ) / 2 - 5 ))
if [[ $pad_center -gt 0 ]]; then
printf -v pad_center "%${pad_center}s" " "
else
pad_center=""
fi
fi
# little helpers for terminal print control and key input
ESC=$( printf "\033" )
cursor_blink_on() { printf "$ESC[?25h"; }
cursor_blink_off() { printf "$ESC[?25l"; }
clear_screen() { printf "$ESC[2J"; }
cursor_to() { printf "$ESC[$1;${2:-1}H"; }
if [[ $SELECT_MONOCHROME -eq 1 ]]; then
print_option() { printf " $ESC[30m%s $ESC[0m%s %s $ESC[30m%s$ESC[0m$ESC[K" "$1" "$2" "$3" "$4"; }
print_selected() { printf " $ESC[30m%s $ESC[0m%s{ $ESC[1m%s$ESC[0m } $ESC[30m%s$ESC[0m$ESC[K" "$1" "$2" "$3" "$4"; }
print_multiselected() { printf "$ESC[30;1m*$ESC[0m"; }
else
print_option() { printf " $ESC[30m%s $ESC[33m%s %s$ESC[0m $ESC[30m%s$ESC[0m$ESC[K" "$1" "$2" "$3" "$4"; }
print_selected() { printf " $ESC[30m%s $ESC[35;1m%s{ $ESC[37;1m%s$ESC[0;35;1m } $ESC[0;30m%s$ESC[0m$ESC[K" "$1" "$2" "$3" "$4"; }
print_multiselected() { printf "$ESC[32;1m*$ESC[0m"; }
fi
get_cursor_row() { IFS=';' read -sdR -p $'\E[6n' ROW COL; echo ${ROW#*[}; }
key_input() { read -s -n1 $SELECT_TIMEOUT key1 2>/dev/null >&2
timeoutec=$?
read -s -n2 -t 0.1 key2 2>/dev/null >&2
local idx=$selected
while true; do
idx=$(( ($idx + 1) % $optcount ))
if [[ "$key1" = "${shortcuts[$idx]}" ]]; then
echo $idx; return;
fi
if [[ $idx -eq $selected ]]; then break; fi
done
if [[ $timeoutec = 142 ]]; then echo esc; return; fi
if [[ $key1$key2 = $ESC[A ]]; then echo up; return; fi
if [[ $key1$key2 = $ESC[B ]]; then echo down; return; fi
if [[ $key1$key2 = $ESC[C ]]; then echo left; return; fi
if [[ $key1$key2 = $ESC[D ]]; then echo right; return; fi
if [[ $key1 = $'\e' ]]; then echo esc; return; fi
if [[ $key1 = x ]]; then echo multiselect; return; fi
if [[ $key1 = q ]]; then echo esc; return; fi
if [[ $key1 = "" ]]; then echo enter; return; fi
}
reset_display() {
cursor_blink_on; stty echo; printf '\n';
}
if [[ $SELECT_MIDDLE -eq 1 ]]; then
clear_screen
cursor_to 1
local pad_middle=$(( ( $( tput lines ) - $optcount ) / 2 - 2 ))
if [[ $pad_middle -gt 0 ]]; then
printf "\n%.0s" $( seq 1 $pad_middle )
fi
fi
if [ -n "$SELECT_TITLE" ]; then
printf "${pad_center} $ESC[35;1m%s$ESC[0m$ESC[K \n" "$SELECT_TITLE"
fi
# initially print empty new lines (scroll down if at bottom of screen)
for opt; do printf "\n"; done
# Extras for top and bottom
printf "\n\n"
# determine current screen position for overwriting the options
local lastrow=`get_cursor_row`
local startrow=$(( $lastrow - $# - 2 ))
#~ if [ -n "$SELECT_TITLE" ]; then
#~ ((startrow--))
#~ ((startrow--))
#~ fi
local toprow="+~~--"
local bottomrow="--~~+"
local lborderchars="|:'"
local rborderchars="|:."
# ensure cursor and input echoing back on upon a ctrl+c during read -s
trap "cursor_blink_on; stty echo; printf '\n'; exit 1" 2
cursor_blink_off
local bottomlength=$(( 10 + $max_opt ))
local multiselect_pos=6
if [[ $SELECT_NUMBERS -eq 1 ]]; then
bottomlength=$(( $bottomlength + 3 ))
multiselect_pos=$(( $multiselect_pos + 3 ))
fi
if [[ $SELECT_SHORTCUTS -eq 1 ]]; then
bottomlength=$(( $bottomlength + 3 ))
multiselect_pos=$(( $multiselect_pos + 3 ))
fi
while true; do
# print options by overwriting the last lines
local idx=0
local nidx
local shortcut
cursor_to $startrow
if [ $selected -lt 0 ]; then selected=$(($# - 1)); fi
if [ $selected -ge $# ]; then selected=0; fi
printf "${pad_center} $ESC[30m%s$ESC[0m$ESC[K " "$toprow"
for opt; do
opt="${choices[$idx]}"
local lborder="${lborderchars:$idx:1}"
if [ -z "$lborder" ]; then lborder=" "; fi
local rborder="${rborderchars:$(( $optcount - $idx - 1)):1}"
if [ -z "$rborder" ]; then rborder=" "; fi
if [[ $SELECT_UNDERSCORES -eq 1 ]]; then
opt=${opt//_/ }
fi
if [[ $SELECT_NUMBERS -eq 1 ]]; then
printf -v nidx "$nformat" $(( idx + 1 ))
fi
if [[ $SELECT_SHORTCUTS -eq 1 ]]; then
printf -v shortcut "%s) " "${shortcuts[$idx]}"
fi
printf -v padopt "%-${max_opt}s" "$opt"
cursor_to $(($startrow + $idx + 1))
printf "%s" "${pad_center}"
if [ $idx -eq $selected ]; then
print_selected "$lborder" "$nidx$shortcut" "$padopt" "$rborder"
else
print_option "$lborder" "$nidx$shortcut" "$padopt" "$rborder"
fi
if (( ${multiselected[$idx]} )); then
cursor_to $(($startrow + $idx + 1)) $multiselect_pos
print_multiselected
fi
((idx++))
done
cursor_to $(($startrow + $idx + 1))
printf "${pad_center}$ESC[K$ESC[30m%${bottomlength}s$ESC[0m " "$bottomrow"
# user key control
local user_input=`key_input`
case "$user_input" in
enter) break;;
esc) reset_display; exit 1;;
up) ((selected--));;
down) ((selected++));;
multiselect) if (( $SELECT_MULTI )); then
multiselected[$selected]=$(( 1 - ${multiselected[$selected]} ));
((selected++));
fi;;
[0-9]*) selected=$user_input;;
esac
done
# cursor position back to normal
cursor_to $lastrow
reset_display
local returnvalue=0
local idx=0
if (( $SELECT_MULTI )); then
if [[ -n "$SELECT_TOFILE" ]]; then printf "" > "$SELECT_TOFILE"; fi
for opt; do
if (( ${multiselected[$idx]} )); then
if [[ -n "$SELECT_TOFILE" ]]; then echo "${choices[$idx]}" >> "$SELECT_TOFILE"; fi
returnvalue=$(( $returnvalue + 2**$idx ))
fi
((idx++))
done
else
# Single choice
returnvalue=$selected
if [[ -n "$SELECT_TOFILE" ]]; then
echo "${choices[$selected]}" > "$SELECT_TOFILE"
fi
fi
return $(( $returnvalue + 10 ))
}
## Function end
[[ $_ == $0 ]] && {
_help() {
SELF=$( basename "$0" )
echo "Select Option [Pure BASH]
Adapted from: https://unix.stackexchange.com/questions/146570/arrow-key-enter-menu
Usage: $SELF [-nms_] [--nc] [-o File] [--title Title] [--timeout #] list of options
-n Show item number
-m Multiple choice selection. Note that only 7 options can be encoded
reliably in return value. Use File output for more options.
-s Show shortcuts
--nc No colors
--title Shows a title above selection
--center Center box on screen
--middle Clear screen and position in the middle
--timeout # Set timeout in seconds
--select # Go to a row when starting. 0 = first item
-o File Save the choice(s) in a file.
-_ Print spaces instead of underscores, workaround for
shell arguments containing spaces.
--package Special option to base64 encode this program, to be
used in your own programs
User interface:
Enter Select option
Esc / Ctrl-c Exit
x Multi-selection
other keys Shortcut, defaults to first letter of choice
Returns:
Single selection:
- (10 + index) of selection (first option = 10)
- 1 if ctrl-c or ESC pressed
- If output file is defined, choice is printed there.
Multiple selection:
- (10 + 2**index) encoded multiple values (no options = 10). See example.
- 1 if ctrl-c or ESC pressed
- All choices separately in the output File.
Example:
Single choice:
> $SELF -n one two three
> EC=\$?
> [[ \$EC -lt 10 ]] && { echo Exited..; } || { echo You selected \$(( \$EC - 9 )); }
Single choice with shortcuts, press 1 2 3 or c to move in the menu:
> $SELF -s [1]One [2]Two [3]Three [c]Cancel
Multi choice:
> $SELF -m one two three four five six seven -o choice.txt
> C=\$?; C=\$(( \$C - 10 ))
> if (( \$C == 0 )); then printf 'No selection'; else printf 'Selections: '; fi
> for b in 7 6 5 4 3 2 1 0; do
> if (( \$C >= 2**\$b )); then printf '%d, ' \$(( \$b + 1 )); C=\$(( \$C - 2**\$b )); fi
> done; printf '\\n'
"
exit
}
_package_self() {
grep ^SELECT_ "$0"
printf "source <( echo '"
awk '/^## Function start/{flag=1;next}/^## Function end/{flag=0}flag' "$0" | \
base64 -w 0
printf "' | base64 -d )\n"
printf 'select_option one two three || choice=$(( $? - 9 ))\n'
}
SELECT_MONOCHROME=0
SELECT_SHORTCUTS=0
SELECT_NUMBERS=0
SELECT_UNDERSCORES=0
SELECT_MULTI=0
SELECT_CENTER=0
SELECT_MIDDLE=0
SELECT_TOFILE=""
SELECT_TITLE=""
SELECT_TIMEOUT=""
SELECT_SELECT=0
for (( i=1; i<=$#; i++ )); do
value=${!i}
j=$(( i + 1 ))
[[ "$value" = "--help" ]] && _help
[[ "$value" = "--nc" ]] && { SELECT_MONOCHROME=1; continue; }
[[ "$value" = "--package" ]] && { _package_self; exit; }
[[ "$value" = "--center" ]] && { SELECT_CENTER=1; continue; }
[[ "$value" = "--middle" ]] && { SELECT_MIDDLE=1; continue; }
[[ "$value" = "--title" ]] && { SELECT_TITLE="${!j}"; ((i++)); continue; }
[[ "$value" = "--timeout" ]] && { SELECT_TIMEOUT="-t ${!j}"; ((i++)); continue; }
[[ "$value" = "--select" ]] && { SELECT_SELECT="${!j}"; ((i++)); continue; }
[[ "${value}" = "-"* ]] && {
[[ "$value" =~ -.*h ]] && { _help; }
[[ "$value" =~ -.*m ]] && { SELECT_MULTI=1; }
[[ "$value" =~ -.*s ]] && { SELECT_SHORTCUTS=1; }
[[ "$value" =~ -.*n ]] && { SELECT_NUMBERS=1; }
[[ "$value" =~ -.*_ ]] && { SELECT_UNDERSCORES=1; }
[[ "$value" =~ -.*o ]] && {
SELECT_TOFILE="${!j}"; ((i++));
[[ -z "$SELECT_TOFILE" ]] && { echo Output file name missing; exit 1; }
}
continue
}
opts+=( "$value" )
done
if [[ "${#opts[@]}" -eq 0 ]]; then
_help
fi
select_option "${opts[@]}"
exit $?
}