Code
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_option() { printf "%s %s " "$2" "$1"; }
print_selected() { 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#*[}"; }
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
}
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
row=$(( idx / colmax ))
col=$(( idx - row * colmax ))
cursor_to $(( startrow + row + 1 )) $(( offset * col + 1 ))
if [[ $idx -eq $curr_idx ]]; then
print_selected "$option"
else
print_option "$option"
fi
((idx++))
done
}
# initially print empty new lines (scroll down if at bottom of screen)
for opt; do printf "\n"; done
# determine current screen position for overwriting the options
# local return_value=$1
local lastrow=$(get_cursor_row)
# local lastcol=$(get_cursor_col)
local startrow=$(( lastrow - $# ))
# local startcol=1
# local lines=$( tput lines )
local cols=$(tput cols)
local colmax=$2
local offset=$(( cols / colmax ))
# local size=$4
shift 4
# 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
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
enter) 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;;
esac
done
# cursor position back to normal
cursor_to $(( lastrow - $# + ( $# / colmax ) - 1 ))
printf "\n"
cursor_blink_on
return $(( active_col + active_row * colmax ))
}
Usage
#!/bin/bash
select_option() {
.
.
.
}
options=("Apple" "Banana" "Orange" "Exit")
select_option $? NUM_OF_COLUMN "${options[@]}"
case $? in
0) echo "You selected Apple";;
1) echo "You selected Banana";;
2) echo "You selected Orange";;
3) echo "You selected Exit";
exit;;
esac
exit 0
where NUM_OF_COLUMN $\leqslant$ the number of elements in options array.
Examples
One column

Two column

Three column

Four column
