Skip to content

Instantly share code, notes, and snippets.

@am-kantox
Created January 27, 2023 16:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save am-kantox/c604803c4a507ba742111554197c6e98 to your computer and use it in GitHub Desktop.
Save am-kantox/c604803c4a507ba742111554197c6e98 to your computer and use it in GitHub Desktop.
Implementation for single and multiple selects in bash
function multiselect {
# credits: https://unix.stackexchange.com/a/673436/55106 (altered with single-choice by me)
# little helpers for terminal print control and key input
ESC=$( printf "\033")
cursor_blink_on() { printf "$ESC[?25h"; }
cursor_blink_off() { printf "$ESC[?25l"; }
cursor_to() { printf "$ESC[$1;${2:-1}H"; }
print_inactive() { printf "$2 $1 "; }
print_active() { printf "$2 $ESC[7m $1 $ESC[27m"; }
get_cursor_row() { IFS=';' read -sdR -p $'\E[6n' ROW COL; echo ${ROW#*[}; }
local return_value=$1
local -n options=$2
local -n defaults=$3
local single_choice=$4
local selected=()
for ((i=0; i<${#options[@]}; i++)); do
if [[ ${defaults[i]} = "true" ]]; then
selected+=("true")
else
selected+=("false")
fi
printf "\n"
done
# determine current screen position for overwriting the options
local lastrow=`get_cursor_row`
local startrow=$(($lastrow - ${#options[@]}))
# ensure cursor and input echoing back on upon a ctrl+c during read -s
trap "cursor_blink_on; stty echo; printf '\n'; exit" 2
cursor_blink_off
key_input() {
local key
IFS= read -rsn1 key 2>/dev/null >&2
if [[ $key = "" ]]; then echo enter; fi;
if [[ $key = $'\x20' ]]; then echo space; fi;
if [[ $key = "k" ]]; then echo up; fi;
if [[ $key = "j" ]]; then echo down; fi;
if [[ $key = $'\x1b' ]]; then
read -rsn2 key
if [[ $key = [A || $key = k ]]; then echo up; fi;
if [[ $key = [B || $key = j ]]; then echo down; fi;
fi
}
toggle_option() {
local option=$1
if [[ ${single_choice} == true ]]; then
for ((idx=0; idx<${#options[@]}; idx++)); do
selected[idx]=false
done
selected[option]=true
else
if [[ ${selected[option]} == true ]]; then
selected[option]=false
else
selected[option]=true
fi
fi
}
print_options() {
# print options by overwriting the last lines
local idx=0
local lb='['
local rb=']'
if [[ ${single_choice} == true ]]; then lb='('; rb=')'; fi
for option in "${options[@]}"; do
local prefix="$lb\e[1;31m✗\e[0m$rb"
if [[ ${selected[idx]} == true ]]; then
prefix="$lb\e[38;5;46m✔\e[0m$rb"
fi
cursor_to $(($startrow + $idx))
if [ $idx -eq $1 ]; then
print_active "$option" "$prefix"
else
print_inactive "$option" "$prefix"
fi
((idx++))
done
}
local active
if [[ ${single_choice} == true ]]; then
for ((idx=0; idx<${#options[@]}; idx++)); do
if [[ ${selected[$idx]} == true ]]; then active=$idx; fi
done
else
active=0
fi
while true; do
print_options $active
# user key control
case `key_input` in
space) toggle_option $active;;
enter) if [[ ${single_choice} == true ]]; then toggle_option $active; fi;
print_options -1; break;;
up) ((active--));
if [ $active -lt 0 ]; then active=$((${#options[@]} - 1)); fi;;
down) ((active++));
if [ $active -ge ${#options[@]} ]; then active=0; fi;;
esac
done
# cursor position back to normal
cursor_to $lastrow
printf "\n"
cursor_blink_on
local option
if [[ ${single_choice} == true ]]; then
for ((idx=0; idx<${#options[@]}; idx++)); do
if [[ ${selected[$idx]} == true ]]; then option=${options[$idx]}; fi
done
eval $return_value='("${option}")'
else
eval $return_value='("${selected[@]}")'
fi
}
# Usage: multiselect
# my_options=( "Option 1" "Option 2" "Option 3" )
# preselection=( "true" "true" "false" )
# multiselect result my_options preselection false
# Usage: soloselect
# my_options=( "Option 1" "Option 2" "Option 3" )
# preselection=( "false" "true" "false" )
# multiselect result my_options preselection true
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment