tui.sh/tui.sh

436 lines
13 KiB
Bash

#!/bin/sh
# _____ _ _ ___ ____ _ _
# |_ _| | | |_ _| / ___|| | | |
# | | | | | || | \___ \| |_| |
# | | | |_| || | _ ___) | _ |
# |_| \___/|___(_)____/|_| |_|
#
# tui.sh - Text-based User Interface library for POSIX compliant shells.
#
# Depends on: libcurses (tput), GNU coreutils.
# Tested on: bash, dash.
#
# FUNCTIONS
#
# tui_msg message box with OK button
# tui_select menu
# tui_checklist menu with multiple choice
# tui_confirm confirmation menu (yes/no)
# tui_input text input
# tui_password password input
# tui_spin spinner
# tui_progress progress bar
# tui_print print formatted text
# tui_splitscr split screen
# tui_termsize get actual terminal size, set COLUMNS and LINES
# tui_curpos get current terminal cursor position
# tui_readchar read characters from keyboard
# tui_readkey read keyboard codes
# tui_optval cli argument parser
# tui_fallback fallback to default terminal settings
# ------------------------------------------------------------------------- #
# Helper functions #
# ------------------------------------------------------------------------- #
tui_termsize()
{
# Return actual terminal size "COLUMNS LINES"
# Set COLUMNS and LINES variables.
COLUMNS="${COLUMNS:-$(tput cols)}"
LINES="${LINES:-$(tput lines)}"
printf '%s %s\n' "$COLUMNS" "$LINES"
}
tui_curpos()
{
# Return current cursor position "COLUMN LINE" e.g. "0 27"
# Ref: https://stackoverflow.com/a/12341833
exec < /dev/tty
oldstty="$(stty -g)"
stty raw -echo min 0
tput u7 > /dev/tty
sleep .02
IFS=';' read -r _row _col
stty "$oldstty"
_row="$(expr $(expr substr $_row 3 99) - 1)" # Strip leading escape off
_col="$(expr ${_col%R} - 1)" # Strip trailing 'R' off
printf '%s %s\n' "$_col" "$_row"
}
tui_fallback()
{
# Handle script interruption
# Usage: trap tui_fallback <signal>
# See trap(1), signal.h(0P)
tput rmcup # restore screen content
tput rc # restore cursor position
tput ed # clear screen after cursor position
tput cnorm # show cursor
}
tui_optval()
{
# GNU-style CLI options parser.
# Parses `--opt VAL` and `--opt=VAL` options..
# Usage: tui_optval "$1" "$2"
# Sets variables:
# _opt - option name
# _val - option's value
# _sft - value for shift
_opt="${1%%=*}"; _val="${1#*=}"; _shift=1
if [ "$_opt" = "$_val" ]; then
if [ -n "$2" ] && [ "${2#${2%%?}}" != "-" ]; then
_val="$2"; _shift=2
else
unset _val
fi
fi
if [ -z "$_val" ]; then
echo "Missing argument for option $_opt" >&2; exit 1
fi
}
tui_readchar()
{
# Read number of chars into variable
# Usage: tui_readchar [-n <num>] <var>
# Options:
# -n <num> number of characters [default: 1]
# Arguments:
# <var> variable where the character will be stored
unset _num
while [ "$#" -ne 0 ]; do
case "$1" in
-n)
tui_optval "$1" "$2"
_num="$_val"
shift "$_shift"
;;
-*)
printf 'tui_readchar: illegal option %s\n' "$OPT" >&2
exit 1
;;
*)
_var="$1"
shift
;;
esac
done
_num="${_num:-1}"
if [ -z "$_var" ]; then
echo 'tui_readchar: missing argument <var>' >&2
exit 1
fi
stty -icanon -echo
eval "$_var=\$(dd bs=1 count="$_num" 2>/dev/null)"
stty icanon echo
}
tui_readkey()
{
# Read keys from keyboard and return key name or octodecimal keycode
# Usage: tui_readkey <var>
# Arguments:
# <var> variable where the key code will be stored
# Ref: https://www.unix.com/shell-programming-and-scripting/\
# 110380-using-arrow-keys-shell-scripts.html
tty_save="$(stty -g)"
_var="$1"
if [ -z "$_var" ]; then
echo 'tui_readkey: missing argument <var>' >&2
exit 1
fi
get_odx() {
od -t o1 | awk '{ for (i=2; i<=NF; i++)
printf("%s%s", i==2 ? "" : " ", $i)
exit }'
}
get_ascii() {
# Return ASCII character by octodecimal code
if [ "$1" = '000' ]; then
_oct=0
else
_oct="$(echo $1 | sed 's/^00//g;s/^0//g')"
fi
_i=0 # iterator
# "_F" below is filler for decimal numbering
for ascii in NUL SOH STX ETX EOT ENQ ACK BEL \
_F _F BS HT LF VT FF CR SO SI \
_F _F DLE DC1 DC2 DC3 DC4 NAK SYN ETB \
_F _F CAN EM SUB ESC FS GS RS US \
_F _F Space '!' '"' '#' '$' '%' '&' "'" \
_F _F '(' ')' '*' '+' ',' '-' '.' '/' \
_F _F 0 1 2 3 4 5 6 7 \
_F _F 8 9 ':' ';' '<' = '>' '?' \
_F _F _F _F _F _F _F _F _F _F _F _F _F _F \
_F _F _F _F _F _F _F _F '@' \
A B C D E F G _F _F \
H I J K L M N O _F _F \
P Q R S T U V W _F _F X Y Z \
'[' '\' ']' '^' '_' _F _F '`' \
a b c d e f g _F _F h i j k l m n o _F _F \
p q r s t u v w _F _F x y z \
'{' '|' '}' '~' DEL; do
if [ "$_i" -eq "$_oct" ]; then
echo "$ascii"
fi
_i=$(( _i + 1 ))
done
}
# Grab terminal capabilities
# Docs: "https://www.gnu.org/software/termutils/manual/termutils-2.0/\
# html_chapter/tput_1.html"
tty_cuu1=$(tput cuu1 2>&1 | get_odx) # Up arrow
tty_kcuu1=$(tput kcuu1 2>&1 | get_odx)
tty_cud1=$(tput cud1 2>&1 | get_odx) # Down arrow
tty_kcud1=$(tput kcud1 2>&1 | get_odx)
tty_cub1=$(tput cub1 2>&1 | get_odx) # Left arrow
tty_kcub1=$(tput kcud1 2>&1 | get_odx)
tty_cuf1=$(tput cuf1 2>&1 | get_odx) # Right arrow
tty_kcuf1=$(tput kcud1 2>&1 | get_odx)
tty_ent=$(echo | get_odx) # Enter
tty_kent=$(tput kent 2>&1 | get_odx)
tty_bs=$(echo -n "\b" | get_odx) # BackSpace
tty_kbs=$(tput kbs 2>&1 | get_odx)
tty_khome=$(tput khome 2>&1 | get_odx) # Home
tty_kend=$(tput kend 2>&1 | get_odx) # End
tty_kpp=$(tput kpp 2>&1 | get_odx) # Page Up
tty_knp=$(tput knp 2>&1 | get_odx) # Page Down
tty_kf1=$(tput kf1 2>&1 | get_odx) # Functional keys (1-12)
tty_kf2=$(tput kf2 2>&1 | get_odx)
tty_kf3=$(tput kf3 2>&1 | get_odx)
tty_kf4=$(tput kf4 2>&1 | get_odx)
tty_kf5=$(tput kf5 2>&1 | get_odx)
tty_kf6=$(tput kf6 2>&1 | get_odx)
tty_kf7=$(tput kf7 2>&1 | get_odx)
tty_kf8=$(tput kf8 2>&1 | get_odx)
tty_kf9=$(tput kf9 2>&1 | get_odx)
tty_kf10=$(tput kf10 2>&1 | get_odx)
tty_kf11=$(tput kf11 2>&1 | get_odx)
tty_kf12=$(tput kf12 2>&1 | get_odx)
# Some terminals (e.g. PuTTY) send the wrong code for certain arrow keys
if [ "$tty_cuu1" = "033 133 101" -o "$tty_kcuu1" = "033 133 101" ]; then
tty_cudx="033 133 102"
tty_cufx="033 133 103"
tty_cubx="033 133 104"
fi
stty cs8 -icanon -echo min 10 time 1
stty intr '' susp ''
trap "stty "$tty_save"; exit" INT HUP TERM
keypress=$(dd bs=10 count=1 2> /dev/null | get_odx)
stty "$tty_save"
unset _key
case "$keypress" in
"$tty_ent"|"$tty_kent") _key=Enter;;
"$tty_bs"|"$tty_kbs") _key=BackSpace;;
"$tty_cuu1"|"$tty_kcuu1") _key=Up;;
"$tty_cud1"|"$tty_kcud1"|"$tty_cudx") _key=Down;;
"$tty_cub1"|"$tty_kcub1"|"$tty_cubx") _key=Left;;
"$tty_cuf1"|"$tty_kcuf1"|"$tty_cufx") _key=Right;;
"$tty_khome") _key=Home;;
"$tty_kend") _key=End;;
"$tty_kpp") _key=Page_Up;;
"$tty_knp") _key=Page_Down;;
"$tty_kf1") _key=F1;;
"$tty_kf2") _key=F2;;
"$tty_kf3") _key=F3;;
"$tty_kf4") _key=F4;;
"$tty_kf5") _key=F5;;
"$tty_kf6") _key=F6;;
"$tty_kf7") _key=F7;;
"$tty_kf8") _key=F8;;
"$tty_kf9") _key=F9;;
"$tty_kf10") _key=F10;;
"$tty_kf11") _key=F11;;
"$tty_kf12") _key=F12;;
*)
# Display other keys and codes
if echo "$keypress" | grep '^[0-9][0-9][0-9]$' >/dev/null 2>&1; then
_key="$(get_ascii "$keypress")"
else
_key="$keypress"
fi
;;
esac
stty -icanon -echo
eval "$_var=\$_key"
stty icanon echo
}
# ------------------------------------------------------------------------- #
# General functions #
# ------------------------------------------------------------------------- #
tui_spin()
{
# Show spinner until command runs
# Usage: tui_spin [options] -- <command>
# Options:
# -c <char> character set for spinner
# -t <title> title to display
# -m <text> the message that will be shown after the process ends
# -o (1|2|12|21) send command output to /dev/null (1=stdout, 2=stderr).
# -r restore cursor after process ends. Removes screen
# content created after startup cursor position.
# -- end of options.
# Arguments:
# <command> command to execute
#
# To prevent data loss with '-o' option use this syntax:
# tui_spin 'command > file.tmp'
#
# Defaul spinner character set is '4/8' (see below). You can set
# ASCII safe character set e.g. \-/|
# Some Unicode character sets (Braille Pattern Dots):
# 3/6: ⠇⠋⠙⠸⠴⠦
# 5/6: ⠟⠻⠽⠾⠷⠯
# 4/8: ⡇⠏⠛⠹⢸⣰⣤⣆
# 7/8: ⣷⣯⣟⡿⢿⣻⣽⣾
unset chars title fin_message devnull restore_cursor
while [ "$#" -ne 0 ]; do
case "$1" in
-c) tui_optval "$1" "$2"; chars="$_val"; shift "$_shift";;
-t) tui_optval "$1" "$2"; title="$_val"; shift "$_shift";;
-m) tui_optval "$1" "$2"; fin_message="$_val"; shift "$_shift";;
-o) tui_optval "$1" "$2"; devnull="$_val"; shift "$_shift";;
-r) restore_cursor=1; shift;;
--) shift; set -- "$@"; break;; # end of options
-*) printf 'tui_spin: illegal option %s\n' "$1" >&2
exit 1;;
*) shift;;
esac
done
tput sc # save cursor position
tput civis # hide cursor
chars="${chars:-⡇⠏⠛⠹⢸⣰⣤⣆}"
# Run command in background and save PID
case "$devnull" in
1)
"$@" 1>/dev/null &
pid="$!"
;;
2)
"$@" 2>/dev/null &
pid="$!"
;;
12|21)
"$@" >/dev/null 2>&1 &
pid="$!"
;;
*)
"$@" &
pid="$!"
;;
esac
while [ -d /proc/"$pid" ]; do # spin while command is running
index=0
for char in $(echo "$chars" | grep -o .); do
sleep .06
printf '%s %b\r' "$char" "$title"
done
done
if [ -n "$restore_cursor" ]; then
tput rc # restore cursor position
tput ed # clear screen after cursor position
fi
if [ -n "$fin_message" ]; then
tput el # erase line
printf '%b\n' "$fin_message"
fi
tput cnorm # show cursor
}
tui_select()
{
# Interactive menu.
# Usage: tui_select <items>...
tput civis # hide cursor
pos=1 # `cursor` position
while true; do
tput ed
tput sc
# Print menu items
index=1 # $@ array index
while [ "$((index - 1))" -ne "$#" ]; do
if [ "$index" -eq "$pos" ]; then
eval "printf '> \033[7m%s\033[27m\n' \"\${${index}}\""
else
eval "printf ' %s\n' \"\${${index}}\""
fi
index="$(( index + 1 ))"
done
# Read input
tui_readkey _input
case "$_input" in
'['|[hHjJ]|Left|Up|'033 133 132')
# move up
pos="$(( pos - 1 ))"
;;
']'|[kKlL]|Down|Right|HT)
# move down
pos="$(( pos + 1 ))"
;;
Enter)
# select
selected="$(eval "echo \${${pos}}")"
break
;;
ETX)
# ctrl+c
break
;;
*)
:
esac
# Jump to last item if user press "up" when pos=1 and vice versa
if [ "$pos" -lt 1 ]; then pos="$#"; fi
if [ "$pos" -gt "$#" ]; then pos=1; fi
tput rc
done
tput cnorm # show cursor
echo "$selected" # print output
}