Code
multi_select_option() {
# little helpers for terminal print control and key input
ESC=$( printf "\033")
cursor_blink_on() { printf "%s[?25h" "$ESC"; }
cursor_blink_off() { printf "%s[?25l" "$ESC"; }
cursor_to() { printf "%s[%s;%sH" "$ESC" "$1" "${2:-1}"; }
print_inactive() { printf "%s %s " "$2" "$1"; }
print_active() { printf "%s %s[7m %s %s[27m" "$2" "$ESC" "$1" "$ESC"; }
get_cursor_row() { IFS=';' read -sdR -p $'\E[6n' ROW COL; echo "${ROW#*[}"; }
get_cursor_col() { IFS=';' read -sdR -p $'\E[6n' ROW COL; echo "${COL#*[}"; }
# local lines=$(tput lines)
local cols=$(tput cols)
local return_value=$1
local colmax=$2
local offset=$(( cols / colmax ))
local -n options=$3
# local -n defaults=$4
local defaults=()
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 lastcol=$(get_cursor_col)
local startrow=$(( lastrow - ${#options[@]} + 1 ))
# local startcol=1
# 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 = "h" ]]; then echo left; fi;
if [[ $key = "l" ]]; then echo right; fi;
if [[ $key = "a" ]]; then echo all; fi;
if [[ $key = "n" ]]; then echo none; 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;
if [[ $key = [C || $key = l ]]; then echo right; fi;
if [[ $key = [D || $key = h ]]; then echo left; fi;
fi
}
toggle_option() {
local option=$1
if [[ ${selected[option]} == true ]]; then
selected[option]=false
else
selected[option]=true
fi
}
toggle_option_multicol() {
local option_row=$1
local option_col=$2
if [[ $option_row -eq -10 ]] && [[ $option_row -eq -10 ]]; then
for ((option=0;option<${#selected[@]};option++)); do
selected[option]=true
done
else
if [[ $option_row -eq -100 ]] && [[ $option_row -eq -100 ]]; then
for ((option=0;option<${#selected[@]};option++)); do
selected[option]=false
done
else
option=$(( option_col + option_row * colmax ))
if [[ ${selected[option]} == true ]]; then
selected[option]=false
else
selected[option]=true
fi
fi
fi
}
print_options_multicol() {
# print options by overwriting the last lines
local curr_col=$1
local curr_row=$2
local curr_idx=0
local idx=0
local row=0
local col=0
curr_idx=$(( curr_col + curr_row * colmax ))
for option in "${options[@]}"; do
local prefix="[ ]"
if [[ ${selected[idx]} == "true" ]]; then
prefix="[✓]"
fi
row=$(( idx / colmax ))
col=$(( idx - row * colmax ))
cursor_to $(( startrow + row + 1)) $(( offset * col + 1))
if [ $idx -eq $curr_idx ]; then
print_active "$option" "$prefix"
else
print_inactive "$option" "$prefix"
fi
((idx++))
done
}
local active_row=0
local active_col=0
while true; do
print_options_multicol $active_col $active_row
# user key control
case $(key_input) in
space) toggle_option_multicol $active_row $active_col;;
enter) print_options_multicol -1 -1; break;;
up) (( active_row-- ));
if [[ $active_row -lt 0 ]]; then active_row=0; fi;;
down) (( active_row++ ));
if [[ $(( ${#options[@]} % colmax )) -ne 0 ]]; then
if [[ "$active_row" -ge $(( ${#options[@]} / colmax )) ]]; then
active_row=$(( ${#options[@]} / colmax ));
fi
else
if [[ "$active_row" -ge $(( ${#options[@]} / colmax -1 )) ]]; then
active_row=$(( ${#options[@]} / colmax -1 ));
fi
fi;;
left) (( active_col = active_col - 1 ));
if [[ $active_col -lt 0 ]]; then active_col=0; fi;;
right) (( active_col = active_col + 1 ));
if [[ $active_col -ge $colmax ]]; then active_col=$(( colmax - 1 )) ; fi;;
all) toggle_option_multicol -10 -10 ;;
none) toggle_option_multicol -100 -100 ;;
esac
done
# cursor position back to normal
cursor_to $(( startrow + ( ${#options[@]} / colmax ) + 2 ))
printf "\n"
cursor_blink_on
eval "$return_value"='("${selected[@]}")'
}
Usage
#!/bin/bash
multi_select_option() {
.
.
.
}
choices=("One" "Two" "Three" "Four" "Five" "Six")
multi_select_option results NUM_OF_COLUMN choices
if ! echo ${results[@]} | grep true &>/dev/null; then
echo "Nothing is selected. So exited."
exit
fi
idx=0
for choice in "${choices[@]}"; do
if [[ "${results[idx]}" == "true" ]]; then
echo $choice is selected.
fi
((idx++))
done
exit 0
where NUM_OF_COLUMN $\leqslant$ the number of elements in choices array.
Examples
One column

Two column

Three column
