Skip to content

Instantly share code, notes, and snippets.

@kurahaupo
Last active February 15, 2023 23:44
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kurahaupo/032dbd97fdbc902d9990b4e1ecab815b to your computer and use it in GitHub Desktop.
Save kurahaupo/032dbd97fdbc902d9990b4e1ecab815b to your computer and use it in GitHub Desktop.
Implementing "dialog" in pure bash
#!/bin/bash
# (replace the preceding line with the path to Bash on your system)
# Takes the list of items as command-line parameters
# Interacts using stdin+stderr
# Writes the answer to stdout
# Use like:
# result=$( bash dialog.bash Foo Bar Zot Baz ) || exit
# or
# read result < <( bash dialog.bash Foo Bar Zot Baz ) || exit
#
# Or use this whole file as te basis for a shell function.
options=("$@")
cur_sel=0
num_sel=${#options[@]}
((last_sel=num_sel-1))
width=40
redraw() {
local i
printf '\e[H' # top-left of terminal
for i in "${!options[@]}" # [0..last_sel] inclusive
do
if ((i==cur_sel)) # this line is the current selection
then printf '\e[7m%-*s\e[0m' $width "${options[i]}"
# \e[7m - inverse
# \e[0m - cancel inverse
# %-*s - pad with spaces to specified width
else printf '%s' "${options[i]}"
fi
printf '\e[K\n' # erase to end of line, newline
done
}
# a single keypress is normally just one byte, but there are two exceptions:
# (1) if it's a unicode character encoded as UTF-8
# (2) if it's a function key (anything not on the core typewriter keyboard)
# then it's encoded as a Control Sequence (starting with \e [ and ending with
# a byte between \x40 and \x7f inclusive).
getkey() {
local IFS= key2=
key=
read -s -N1 key || return # get a single byte
[[ $key != [$'\e\xc0'-$'\xfb'] ]] && # single byte, neither ESC nor UTF-8 lead
return
while
read -s -N1 -t0.01 key2 # read more bytes while available
do
key+=$key2 # accumulate bytes as part of keypress
case $key in
$'\e[') ;; # bare CSI, keep going
$'\e'?) ! break ;; # ESC + byte but not CSI
$'\e['*[@-~]) break ;; # a complete Control Sequence ends with any byte in the \x40 to \x7f range
[$'\xc0'-$'\xfb']*[!$'\x80'-$'\xbf']) ! break ;; # invalid UTF-8
[$'\xc0'-$'\xdf']?) break ;; # valid UTF-8 \u000100-\u00007ff
[$'\xe0'-$'\xef']??) break ;; # valid UTF-8 \u000800-\u000ffff
[$'\xf0'-$'\xf7']???) break ;; # valid UTF-8 \u010000-\u01fffff
[$'\xf8'-$'\xfb']????) ! break ;; # fake UTF-8 \u200000-\u3ffffff
esac
done || {
# reach here from « ! break » above
read -s -N9999 -t0.1 key2 # purge any unread bytes
return 1
}
# TODO: map escape sequences to symbolic names
# case $key in
# $'\e['*A) key=UP ;;
# $'\e['*B) key=DOWN ;;
# ...etc...
# esac
}
printf '\e[?1049h' >&2 # switch to alternate screen buffer
finis=$'\e[?1049l' # switch back to normal screen buffer
finis=$'\e[H\e[2J'$finis # home & clear screen before buffer-switch, in case it doesn't work
finish() { printf %s "$finis" >&2 ; finis= ;}
trap finish EXIT
while
redraw >&2
getkey
do
case $key in
$'\e['*A) ((cur_sel > 0 && --cur_sel)) ;;
$'\e['*B) ((cur_sel < last_sel && ++cur_sel)) ;;
$'\e['*H) ((cur_sel = 0)) ;;
$'\e['*F) ((cur_sel = last_sel)) ;;
$'\n' |\
' ' |\
'')
finish # early, so that the following line will come out after we've cleared away the menu
printf '%s\n' "${options[cur_sel]}"
exit 0 ;;
$'\e') break ;;
esac
done
exit 1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment