This commit is contained in:
ge 2023-03-21 02:48:44 +03:00
commit ed521e2e3c
9 changed files with 567 additions and 0 deletions

46
README.md Normal file
View File

@ -0,0 +1,46 @@
# tui.sh
Text-based User Interface library for POSIX compliant shells.
```shell
#!/bin/sh
. ./tui.sh
# Code...
```
See examples.
# Known issues
Currently I've no idea how to force `tput` correctly clear screen on sclroll.
- [ ] Cant use `tui_select` in subshell e.g. `selected=$(tui_select 1 2 3)`
- [ ] `tui_select` doesn't work properly on terminal scroll.
- [ ] `tui_spin -r` doesn't work properly on terminal scroll.
# Roadmap
Functions list may be changed later.
- [ ] `tui_msg` message box with OK button
- [x] `tui_select` menu
- [ ] `tui_checklist` menu with multiple choice
- [ ] `tui_confirm` confirmation menu (yes/no)
- [ ] `tui_input` text input
- [ ] `tui_password` password input
- [x] `tui_spin` spinner
- [ ] `tui_progress` progress bar
- [ ] `tui_print` print formatted text
- [ ] `tui_splitscr` split screen
- [x] `tui_termsize` get actual terminal size, set COLUMNS and LINES
- [x] `tui_curpos` get current terminal cursor position
- [x] `tui_readchar` read characters from keyboard
- [x] `tui_readkey` read keyboard codes
- [x] `tui_optval` cli argument parser
- [x] `tui_fallback` fallback to default terminal settings
# License
Licensed under The Unlicense, see UNLICENSE.

24
UNLICENSE Normal file
View File

@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>

6
examples/curpos Normal file
View File

@ -0,0 +1,6 @@
#!/bin/sh
. ../tui.sh
pos="$(tui_curpos)"
printf 'Cursor position: COLUMN=%s LINE=%s\n' "${pos% *}" "${pos#* }"

9
examples/readchar Normal file
View File

@ -0,0 +1,9 @@
#!/bin/sh
. ../tui.sh
num="${1:-1}"
printf 'Usage: readchar NUMBER\n'
printf 'Input characters (number=%s):\n' "$num"
tui_readchar -n "$num" VAR
echo Characters: $VAR

7
examples/readkey Normal file
View File

@ -0,0 +1,7 @@
#!/bin/sh
. ../tui.sh
printf 'Press any key. Key will printed below.\n'
tui_readkey KEY
printf 'Key pressed: %s\n' "$KEY"

17
examples/readkey_2 Normal file
View File

@ -0,0 +1,17 @@
#!/bin/sh
. ../tui.sh
printf 'Press arrow keys, q to quit.\n'
while true; do
tui_readkey KEY
case "$KEY" in
Up) printf 'Move UP\n';;
Down) printf 'Move DOWN\n';;
Left) printf 'Move LEFT\n';;
Right) printf 'Move RIGHT\n';;
q|Q) break;;
*) :
esac
done

5
examples/select Normal file
View File

@ -0,0 +1,5 @@
#!/bin/sh
. ../tui.sh
tui_select First Second 'Another one'

18
examples/spin Normal file
View File

@ -0,0 +1,18 @@
#!/bin/sh
. ../tui.sh
tui_spin \
-t 'Sleeping...' \
-m '\033[32;1m✓ Waked up!\033[0m' \
-- sleep 3
tui_spin -r \
-t 'Ping Google...' \
-m '\033[32;1m✓ Ping Google OK!\033[0m' \
-- ping -c 3 google.com
tui_spin \
-t 'Search *bash* files...' \
-m '\033[32;1m✓ Search *bash* files OK!\033[0m' \
-- find ~ -name "*bash*"

435
tui.sh Normal file
View File

@ -0,0 +1,435 @@
#!/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
}