From ed521e2e3c542e1cbb455c7439e166dfb74f3b84 Mon Sep 17 00:00:00 2001 From: ge Date: Tue, 21 Mar 2023 02:48:44 +0300 Subject: [PATCH] init --- README.md | 46 +++++ UNLICENSE | 24 +++ examples/curpos | 6 + examples/readchar | 9 + examples/readkey | 7 + examples/readkey_2 | 17 ++ examples/select | 5 + examples/spin | 18 ++ tui.sh | 435 +++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 567 insertions(+) create mode 100644 README.md create mode 100644 UNLICENSE create mode 100644 examples/curpos create mode 100644 examples/readchar create mode 100644 examples/readkey create mode 100644 examples/readkey_2 create mode 100644 examples/select create mode 100644 examples/spin create mode 100644 tui.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..7496bca --- /dev/null +++ b/README.md @@ -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. diff --git a/UNLICENSE b/UNLICENSE new file mode 100644 index 0000000..68a49da --- /dev/null +++ b/UNLICENSE @@ -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 diff --git a/examples/curpos b/examples/curpos new file mode 100644 index 0000000..3f64f74 --- /dev/null +++ b/examples/curpos @@ -0,0 +1,6 @@ +#!/bin/sh + +. ../tui.sh + +pos="$(tui_curpos)" +printf 'Cursor position: COLUMN=%s LINE=%s\n' "${pos% *}" "${pos#* }" diff --git a/examples/readchar b/examples/readchar new file mode 100644 index 0000000..883a3a2 --- /dev/null +++ b/examples/readchar @@ -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 diff --git a/examples/readkey b/examples/readkey new file mode 100644 index 0000000..baab078 --- /dev/null +++ b/examples/readkey @@ -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" diff --git a/examples/readkey_2 b/examples/readkey_2 new file mode 100644 index 0000000..2a9dd44 --- /dev/null +++ b/examples/readkey_2 @@ -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 diff --git a/examples/select b/examples/select new file mode 100644 index 0000000..75c4ceb --- /dev/null +++ b/examples/select @@ -0,0 +1,5 @@ +#!/bin/sh + +. ../tui.sh + +tui_select First Second 'Another one' diff --git a/examples/spin b/examples/spin new file mode 100644 index 0000000..0e8803b --- /dev/null +++ b/examples/spin @@ -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*" diff --git a/tui.sh b/tui.sh new file mode 100644 index 0000000..f2df4bf --- /dev/null +++ b/tui.sh @@ -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 + # 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 ] + # Options: + # -n number of characters [default: 1] + # Arguments: + # 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 ' >&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 + # Arguments: + # 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 ' >&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] -- + # Options: + # -c character set for spinner + # -t 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 +}