Last active
February 15, 2023 23:44
-
-
Save kurahaupo/032dbd97fdbc902d9990b4e1ecab815b to your computer and use it in GitHub Desktop.
Implementing "dialog" in pure bash
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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