330 lines
9.9 KiB
Bash
330 lines
9.9 KiB
Bash
#!/bin/bash
|
|
# @name Prompts
|
|
# @brief Inquirer.js inspired prompts
|
|
|
|
_get_cursor_row() {
|
|
local IFS=';'
|
|
# shellcheck disable=SC2162,SC2034
|
|
read -sdR -p $'\E[6n' ROW COL;
|
|
echo "${ROW#*[}";
|
|
}
|
|
_cursor_blink_on() { echo -en "\e[?25h" >&2; }
|
|
_cursor_blink_off() { echo -en "\e[?25l" >&2; }
|
|
_cursor_to() { echo -en "\e[$1;${2:-1}H" >&2; }
|
|
|
|
# key input helper
|
|
_key_input() {
|
|
read -s -r -N1 key 2>/dev/null >&2
|
|
case $key in
|
|
"A") echo "up"; ;;
|
|
"B") echo "down"; ;;
|
|
" ") echo "space"; ;;
|
|
$'\n') echo "enter" ;;
|
|
esac
|
|
}
|
|
|
|
# print new line for empty element in array
|
|
# shellcheck disable=SC2231
|
|
_new_line_foreach_item() { for _ in $1[@]; do echo -en "\n" >&2; done }
|
|
|
|
# display prompt text without linebreak
|
|
_prompt_text() {
|
|
echo -en "\e[32m?\e[0m\e[1m ${1}\e[0m " >&2
|
|
}
|
|
|
|
# decrement counter $1, considering out of range for $2
|
|
_decrement_selected() {
|
|
local selected=$1;
|
|
((selected--))
|
|
if [ "${selected}" -lt 0 ]; then
|
|
selected=$(($2 - 1));
|
|
fi
|
|
echo -n $selected
|
|
}
|
|
|
|
# increment counter $1, considering out of range for $2
|
|
_increment_selected() {
|
|
local selected=$1;
|
|
((selected++));
|
|
if [ "${selected}" -ge "${opts_count}" ]; then
|
|
selected=0;
|
|
fi
|
|
echo -n $selected
|
|
}
|
|
|
|
# checks if $1 contains element $2
|
|
_contains() {
|
|
items=$1
|
|
search=$2
|
|
for item in "${items[@]}"; do
|
|
if [ "$item" == "$search" ]; then return 0; fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# @description Prompt for text
|
|
# @arg $1 string Phrase for prompting to text
|
|
# @stderr Instructions for user
|
|
# @stdout Text as provided by user
|
|
# @example
|
|
# # Raw input without validation
|
|
# text=$(input "Please enter something and confirm with enter")
|
|
# @example
|
|
# # Input with validation
|
|
# text=$(with_validate 'input "Please enter at least one character and confirm with enter"' validate_present)
|
|
input() {
|
|
_prompt_text "$1"; echo -en "\e[36m\c" >&2
|
|
read -r text
|
|
echo -n "${text}"
|
|
}
|
|
|
|
# @description Show confirm dialog for yes/no
|
|
# @arg $1 string Phrase for promptint to text
|
|
# @stdout 0 for no, 1 for yes
|
|
# @stderr Instructions for user
|
|
# @example
|
|
# confirmed=$(confirm "Should it be?")
|
|
# if [ "$confirmed" = "0" ]; then echo "No?"; else echo "Yes!"; fi
|
|
confirm() {
|
|
_prompt_text "$1 (y/N)"
|
|
echo -en "\e[36m\c " >&2
|
|
local result=""
|
|
echo -n " " >&2
|
|
until [[ "$result" == "y" ]] || [[ "$result" == "N" ]]
|
|
do
|
|
echo -e "\e[1D\c " >&2
|
|
# shellcheck disable=SC2162
|
|
read -n1 result
|
|
done
|
|
echo -en "\e[0m" >&2
|
|
|
|
case $result in
|
|
y) echo -n 1; ;;
|
|
N) echo -n 0 ;;
|
|
esac
|
|
|
|
echo "" >&2
|
|
}
|
|
|
|
# @description Renders a text based list of options that can be selected by the
|
|
# user using up, down and enter keys and returns the chosen option.
|
|
# Inspired by https://unix.stackexchange.com/questions/146570/arrow-key-enter-menu/415155#415155
|
|
# @arg $1 string Phrase for promptint to text
|
|
# @arg $2 array List of options (max 256)
|
|
# @stdout selected index (0 for opt1, 1 for opt2 ...)
|
|
# @stderr Instructions for user
|
|
# @example
|
|
# options=("one" "two" "three" "four")
|
|
# option=$(list "Select one item" "${options[@]}")
|
|
# echo "Your choice: ${options[$option]}"
|
|
list() {
|
|
_prompt_text "$1 "
|
|
echo "" >&2
|
|
|
|
local opts=("${@:2}")
|
|
local opts_count=$(($# -1))
|
|
_new_line_foreach_item "${opts[@]}"
|
|
|
|
# determine current screen position for overwriting the options
|
|
local lastrow; lastrow=$(_get_cursor_row)
|
|
local startrow; startrow=$((lastrow - opts_count + 1))
|
|
|
|
# ensure cursor and input echoing back on upon a ctrl+c during read -s
|
|
trap "_cursor_blink_on; stty echo; exit" 2
|
|
_cursor_blink_off
|
|
|
|
local selected=0
|
|
while true; do
|
|
# print options by overwriting the last lines
|
|
local idx=0
|
|
for opt in "${opts[@]}"; do
|
|
_cursor_to $((startrow + idx))
|
|
if [ $idx -eq $selected ]; then
|
|
printf "\e[0m\e[36m\u276F\e[0m \e[36m%s\e[0m" "$opt" >&2
|
|
else
|
|
printf " %s" "$opt" >&2
|
|
fi
|
|
((idx++))
|
|
done
|
|
|
|
# user key control
|
|
case $(_key_input) in
|
|
enter) break; ;;
|
|
up) selected=$(_decrement_selected "${selected}" "${opts_count}"); ;;
|
|
down) selected=$(_increment_selected "${selected}" "${opts_count}"); ;;
|
|
esac
|
|
done
|
|
|
|
echo -en "\n" >&2
|
|
|
|
# cursor position back to normal
|
|
_cursor_to "${lastrow}"
|
|
_cursor_blink_on
|
|
|
|
echo -n "${selected}"
|
|
}
|
|
|
|
# @description Render a text based list of options, where multiple can be selected by the
|
|
# user using up, down and enter keys and returns the chosen option.
|
|
# Inspired by https://unix.stackexchange.com/questions/146570/arrow-key-enter-menu/415155#415155
|
|
# @arg $1 string Phrase for promptint to text
|
|
# @arg $2 array List of options (max 256)
|
|
# @stdout selected index (0 for opt1, 1 for opt2 ...)
|
|
# @stderr Instructions for user
|
|
# @example
|
|
# options=("one" "two" "three" "four")
|
|
# checked=$(checkbox "Select one or more items" "${options[@]}")
|
|
# echo "Your choices: ${checked}"
|
|
checkbox() {
|
|
_prompt_text "$1"
|
|
echo "" >&2
|
|
|
|
local opts; opts=("${@:2}")
|
|
local opts_count; opts_count=$(($# -1))
|
|
_new_line_foreach_item "${opts[@]}"
|
|
|
|
# determine current screen position for overwriting the options
|
|
local lastrow; lastrow=$(_get_cursor_row)
|
|
local startrow; startrow=$((lastrow - opts_count + 1))
|
|
|
|
# ensure cursor and input echoing back on upon a ctrl+c during read -s
|
|
trap "_cursor_blink_on; stty echo; exit" 2
|
|
_cursor_blink_off
|
|
|
|
local selected=0
|
|
local checked=()
|
|
while true; do
|
|
# print options by overwriting the last lines
|
|
local idx=0
|
|
for opt in "${opts[@]}"; do
|
|
_cursor_to $((startrow + idx))
|
|
local icon
|
|
if _contains "${checked[*]}" $idx; then
|
|
icon=$(echo -en "\u25C9")
|
|
else
|
|
icon=$(echo -en "\u25EF")
|
|
fi
|
|
if [ $idx -eq $selected ]; then
|
|
printf "%s \e[0m\e[36m\u276F\e[0m \e[36m%-50s\e[0m" "$icon" "$opt" >&2
|
|
else
|
|
printf "%s %-50s " "$icon" "$opt" >&2
|
|
fi
|
|
((idx++))
|
|
done
|
|
|
|
# user key control
|
|
case $(_key_input) in
|
|
enter) break;;
|
|
space)
|
|
if _contains "${checked[*]}" $selected; then
|
|
checked=( "${checked[@]/$selected}" )
|
|
else
|
|
checked+=("${selected}")
|
|
fi
|
|
;;
|
|
up) selected=$(_decrement_selected "${selected}" "${opts_count}"); ;;
|
|
down) selected=$(_increment_selected "${selected}" "${opts_count}"); ;;
|
|
esac
|
|
done
|
|
|
|
# cursor position back to normal
|
|
_cursor_to "${lastrow}"
|
|
_cursor_blink_on
|
|
|
|
IFS=" " echo -n "${checked[@]}"
|
|
}
|
|
|
|
# @description Show password prompt displaying stars for each password character letter typed
|
|
# it also allows deleting input
|
|
# @arg $1 string Phrase for promptint to text
|
|
# @stdout password as written by user
|
|
# @stderr Instructions for user
|
|
# @example
|
|
# # Password prompt with custom validation
|
|
# validate_password() { if [ ${#1} -lt 10 ];then echo "Password needs to be at least 10 characters"; exit 1; fi }
|
|
# pass=$(with_validate 'password "Enter random password"' validate_password)
|
|
# @example
|
|
# # Password ith no validation
|
|
# pass=$(password "Enter password to use")
|
|
password() {
|
|
_prompt_text "$1"
|
|
echo -en "\e[36m" >&2
|
|
local password=''
|
|
local IFS=
|
|
while read -r -s -n1 char; do
|
|
# ENTER pressed; output \n and break.
|
|
[[ -z "${char}" ]] && { printf '\n' >&2; break; }
|
|
# BACKSPACE pressed; remove last character
|
|
if [ "${char}" == $'\x7f' ]; then
|
|
if [ "${#password}" -gt 0 ]; then
|
|
password="${password%?}"
|
|
echo -en '\b \b' >&2
|
|
fi
|
|
else
|
|
password+=$char
|
|
echo -en '*' >&2
|
|
fi
|
|
done
|
|
echo -en "\e[0m" >&2
|
|
echo -n "${password}"
|
|
}
|
|
|
|
# @description Open default editor ($EDITOR) if none is set falls back to vi
|
|
# @arg $1 string Phrase for promptint to text
|
|
# @stdout Text as input by user in input
|
|
# @stderr Instructions for user
|
|
# @example
|
|
# # Open default editor
|
|
# text=$(editor "Please enter something in the editor")
|
|
# echo -e "You wrote:\n${text}"
|
|
editor() {
|
|
tmpfile=$(mktemp)
|
|
_prompt_text "$1"
|
|
echo "" >&2
|
|
|
|
"${EDITOR:-vi}" "${tmpfile}" >/dev/tty
|
|
echo -en "\e[36m" >&2
|
|
# shellcheck disable=SC2002
|
|
cat "${tmpfile}" | sed -e 's/^/ /' >&2
|
|
echo -en "\e[0m" >&2
|
|
|
|
cat "${tmpfile}"
|
|
}
|
|
|
|
# @description Evaluate prompt command with validation, this prompts the user for input till the validation function
|
|
# returns with 0
|
|
# @arg $1 string Prompt command to evaluate until validation is successful
|
|
# @arg #2 function validation callback (this is called once for exit code and once for status code)
|
|
# @stdout Value collected by evaluating prompt
|
|
# @stderr Instructions for user
|
|
# @example
|
|
# # Using builtin is present validator
|
|
# text=$(with_validate 'input "Please enter something and confirm with enter"' validate_present)
|
|
# @example
|
|
# # Using custom validator e.g. for password
|
|
# validate_password() { if [ ${#1} -lt 10 ];then echo "Password needs to be at least 10 characters"; exit 1; fi }
|
|
# pass=$(with_validate 'password "Enter random password"' validate_password)
|
|
with_validate() {
|
|
while true; do
|
|
local val; val="$(eval "$1")"
|
|
if ($2 "$val" >/dev/null); then
|
|
echo "$val";
|
|
break;
|
|
else
|
|
show_error "$($2 "$val")";
|
|
fi
|
|
done
|
|
}
|
|
|
|
# @description Validate a prompt returned any value
|
|
# @arg $1 value to validate
|
|
# @stdout error message for user
|
|
# @exitcode 0 String is at least 1 character long
|
|
# @exitcode 1 There was no input given
|
|
# @example
|
|
# # text input with validation
|
|
# text=$(with_validate 'input "Please enter something and confirm with enter"' validate_present)
|
|
validate_present() {
|
|
if [ "$1" != "" ]; then return 0; else echo "Please specify the value"; return 1; fi
|
|
}
|