719 lines
15 KiB
Bash
Executable File
719 lines
15 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
|
||
# Bash helper utilities for prompting users.
|
||
# This is a modified version of the excellent Bash TUI toolkit
|
||
# (https://github.com/timo-reymann/bash-tui-toolkit)
|
||
#
|
||
# It includes the following functions for you to use in your
|
||
# bash tool commands:
|
||
#
|
||
# - password Password prompt
|
||
# - checked Checkbox
|
||
# - text Text input with validation
|
||
# - list Select an option from a given list
|
||
# - range Prompt the user for a value within a range
|
||
# - confirm Confirmation prompt
|
||
# - editor Open the user's preferred editor for input
|
||
# - detect_os Detect the current OS
|
||
# - get_opener Get the file opener for the current OS
|
||
# - open_link Open the given link in the default browser
|
||
# See https://github.com/timo-reymann/bash-tui-toolkit/blob/main/test.sh
|
||
# for examples on how to use these commands
|
||
#
|
||
# - guard_operation Prompt for permission to run an operation
|
||
# - guard_path Prompt for permission to perform path operations
|
||
# - patch_file Patch a file
|
||
# - error Log an error
|
||
# - warn Log a warning
|
||
# - info Log info
|
||
# - debug Log a debug message
|
||
# - trace Log a trace message
|
||
# - red Output given text in red
|
||
# - green Output given text in green
|
||
# - gold Output given text in gold
|
||
# - blue Output given text in blue
|
||
# - magenta Output given text in magenta
|
||
# - cyan Output given text in cyan
|
||
# - white Output given text in white
|
||
|
||
# shellcheck disable=SC2034
|
||
red=$(tput setaf 1)
|
||
green=$(tput setaf 2)
|
||
gold=$(tput setaf 3)
|
||
blue=$(tput setaf 4)
|
||
magenta=$(tput setaf 5)
|
||
cyan=$(tput setaf 6)
|
||
white=$(tput setaf 7)
|
||
|
||
default=$(tput sgr0)
|
||
gray=$(tput setaf 243)
|
||
|
||
bold=$(tput bold)
|
||
underlined=$(tput smul)
|
||
|
||
error() {
|
||
echo -e "${red}${bold}ERROR:${default}${red} $1${default}"
|
||
}
|
||
|
||
warn() {
|
||
echo -e "${gold}${bold}WARN:${default}${gold} $1${default}"
|
||
}
|
||
|
||
info() {
|
||
echo -e "${cyan}${bold}INFO:${default}${cyan} $1${default}"
|
||
}
|
||
|
||
debug() {
|
||
echo -e "${blue}${bold}DEBUG:${default}${blue} $1${default}"
|
||
}
|
||
|
||
trace() {
|
||
echo -e "${gray}${bold}TRACE:${default}${gray} $1${default}"
|
||
}
|
||
|
||
success() {
|
||
echo -e "${green}${bold}SUCCESS:${default}${green} $1${default}"
|
||
}
|
||
|
||
red() {
|
||
echo -e "${red}$1${default}"
|
||
}
|
||
|
||
green() {
|
||
echo -e "${green}$1${default}"
|
||
}
|
||
|
||
gold() {
|
||
echo -e "${gold}$1${default}"
|
||
}
|
||
|
||
blue() {
|
||
echo -e "${blue}$1${default}"
|
||
}
|
||
|
||
magenta() {
|
||
echo -e "${magenta}$1${default}"
|
||
}
|
||
|
||
cyan() {
|
||
echo -e "${cyan}$1${default}"
|
||
}
|
||
|
||
white() {
|
||
echo -e "${white}$1${default}"
|
||
}
|
||
|
||
_read_stdin() {
|
||
read -r "$@" </dev/tty
|
||
}
|
||
|
||
_get_cursor_row() {
|
||
declare IFS=';'
|
||
_read_stdin -sdR -p $'\E[6n' ROW COL
|
||
echo "${ROW#*[}"
|
||
}
|
||
|
||
_cursor_blink_on() {
|
||
echo -en "\033[?25h" >&2
|
||
}
|
||
_cursor_blink_off() {
|
||
echo -en "\033[?25l" >&2
|
||
}
|
||
|
||
_cursor_to() {
|
||
echo -en "\033[$1;$2H" >&2
|
||
}
|
||
|
||
# shellcheck disable=SC2154
|
||
_key_input() {
|
||
declare ESC=$'\033'
|
||
declare IFS=''
|
||
_read_stdin -rsn1 a
|
||
if [[ "$ESC" == "$a" ]]; then
|
||
_read_stdin -rsn2 b
|
||
fi
|
||
|
||
declare input="${a}${b}"
|
||
case "$input" in
|
||
"${ESC}[A" | "k") echo up ;;
|
||
"${ESC}[B" | "j") echo down ;;
|
||
"${ESC}[C" | "l") echo right ;;
|
||
"${ESC}[D" | "h") echo left ;;
|
||
'') echo enter ;;
|
||
' ') echo space ;;
|
||
esac
|
||
}
|
||
|
||
_new_line_foreach_item() {
|
||
count=0
|
||
while [[ $count -lt $1 ]]; do
|
||
echo "" >&2
|
||
((count++))
|
||
done
|
||
}
|
||
|
||
_prompt_text() {
|
||
echo -en "\033[32m?\033[0m\033[1m ${1}\033[0m " >&2
|
||
}
|
||
|
||
_decrement_selected() {
|
||
declare selected=$1
|
||
((selected--))
|
||
if [[ "${selected}" -lt 0 ]]; then
|
||
selected=$(($2 - 1))
|
||
fi
|
||
|
||
echo -n $selected
|
||
}
|
||
|
||
_increment_selected() {
|
||
declare selected=$1
|
||
((selected++))
|
||
|
||
if [[ "${selected}" -ge "${opts_count}" ]]; then
|
||
selected=0
|
||
fi
|
||
|
||
echo -n $selected
|
||
}
|
||
|
||
# shellcheck disable=SC2154
|
||
input() {
|
||
_prompt_text "$1"
|
||
echo -en "\033[36m\c" >&2
|
||
_read_stdin -r text
|
||
echo -n "${text}"
|
||
}
|
||
|
||
confirm() {
|
||
trap "stty echo; exit" EXIT
|
||
_prompt_text "$1 (y/N)"
|
||
echo -en "\033[36m\c " >&2
|
||
|
||
declare first_row
|
||
first_row=$(_get_cursor_row)
|
||
declare current_row
|
||
current_row=$((first_row - 1))
|
||
declare result=""
|
||
echo -n " " >&2
|
||
|
||
while true; do
|
||
echo -e "\033[1D\c " >&2
|
||
_read_stdin -n1 result
|
||
|
||
case "$result" in
|
||
y | Y)
|
||
echo -n 1
|
||
break
|
||
;;
|
||
n | N | *)
|
||
echo -n 0
|
||
break
|
||
;;
|
||
esac
|
||
done
|
||
|
||
echo -en "\033[0m" >&2
|
||
echo "" >&2
|
||
}
|
||
|
||
list() {
|
||
_prompt_text "$1 "
|
||
declare opts=("${@:2}")
|
||
declare opts_count=$(($# - 1))
|
||
|
||
_new_line_foreach_item "${#opts[@]}"
|
||
|
||
declare last_row
|
||
last_row=$(_get_cursor_row)
|
||
declare first_row
|
||
first_row=$((last_row - opts_count + 1))
|
||
|
||
trap "_cursor_blink_on; stty echo; exit" 2
|
||
|
||
_cursor_blink_off
|
||
|
||
declare selected=0
|
||
while true; do
|
||
declare idx=0
|
||
for opt in "${opts[@]}"; do
|
||
_cursor_to $((first_row + idx))
|
||
|
||
if [[ $idx -eq "$selected" ]]; then
|
||
printf "\033[0m\033[36m❯\033[0m \033[36m%s\033[0m" "$opt" >&2
|
||
else
|
||
printf " %s" "$opt" >&2
|
||
fi
|
||
|
||
((idx++))
|
||
done
|
||
|
||
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_to "${last_row}"
|
||
_cursor_blink_on
|
||
echo -n "${selected}"
|
||
}
|
||
|
||
checkbox() {
|
||
_prompt_text "$1"
|
||
declare opts
|
||
opts=("${@:2}")
|
||
declare opts_count
|
||
opts_count=$(($# - 1))
|
||
|
||
_new_line_foreach_item "${#opts[@]}"
|
||
|
||
declare last_row
|
||
last_row=$(_get_cursor_row)
|
||
declare first_row
|
||
first_row=$((last_row - opts_count + 1))
|
||
|
||
trap "_cursor_blink_on; stty echo; exit" 2
|
||
|
||
_cursor_blink_off
|
||
|
||
declare selected=0
|
||
declare checked=()
|
||
while true; do
|
||
declare idx=0
|
||
for opt in "${opts[@]}"; do
|
||
_cursor_to $((first_row + idx))
|
||
declare icon="◯"
|
||
|
||
for item in "${checked[@]}"; do
|
||
if [[ "$item" == "$idx" ]]; then
|
||
icon="◉"
|
||
break
|
||
fi
|
||
done
|
||
|
||
if [[ $idx -eq "$selected" ]]; then
|
||
printf "%s \e[0m\e[36m❯\e[0m \e[36m%-50s\e[0m" "$icon" "$opt" >&2
|
||
else
|
||
printf "%s %-50s" "$icon" "$opt" >&2
|
||
fi
|
||
|
||
((idx++))
|
||
done
|
||
|
||
case $(_key_input) in
|
||
enter) break ;;
|
||
space)
|
||
declare found=0
|
||
for item in "${checked[@]}"; do
|
||
if [[ "$item" == "$selected" ]]; then
|
||
found=1
|
||
break
|
||
fi
|
||
done
|
||
|
||
if [ $found -eq 1 ]; 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_to "${last_row}"
|
||
_cursor_blink_on
|
||
IFS="" echo -n "${checked[@]}"
|
||
}
|
||
|
||
password() {
|
||
_prompt_text "$1"
|
||
echo -en "\033[36m" >&2
|
||
declare password=''
|
||
declare IFS=
|
||
|
||
while _read_stdin -r -s -n1 char; do
|
||
[[ -z "${char}" ]] && {
|
||
printf '\n' >&2
|
||
break
|
||
}
|
||
|
||
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}"
|
||
}
|
||
|
||
editor() {
|
||
tmpfile=$(mktemp)
|
||
_prompt_text "$1"
|
||
echo "" >&2
|
||
"${EDITOR:-vi}" "${tmpfile}" >/dev/tty
|
||
echo -en "\033[36m" >&2
|
||
sed -e 's/^/ /' "${tmpfile}" >&2
|
||
echo -en "\033[0m" >&2
|
||
cat "${tmpfile}"
|
||
}
|
||
|
||
with_validate() {
|
||
while true; do
|
||
declare val
|
||
val="$(eval "$1")"
|
||
|
||
if ($2 "$val" >/dev/null); then
|
||
echo "$val"
|
||
break
|
||
else
|
||
show_error "$($2 "$val")"
|
||
fi
|
||
done
|
||
}
|
||
|
||
range() {
|
||
declare min="$2"
|
||
declare current="$3"
|
||
declare max="$4"
|
||
declare selected="${current}"
|
||
declare max_len_current
|
||
max_len_current=0
|
||
|
||
if [[ "${#min}" -gt "${#max}" ]]; then
|
||
max_len_current="${#min}"
|
||
else
|
||
max_len_current="${#max}"
|
||
fi
|
||
|
||
declare padding
|
||
padding="$(printf "%-${max_len_current}s" "")"
|
||
declare first_row
|
||
first_row=$(_get_cursor_row)
|
||
declare current_row
|
||
current_row=$((first_row - 1))
|
||
|
||
trap "_cursor_blink_on; stty echo; exit" 2
|
||
|
||
_cursor_blink_off
|
||
|
||
_check_range() {
|
||
val=$1
|
||
|
||
if [[ "$val" -gt "$max" ]]; then
|
||
val=$min
|
||
elif [[ "$val" -lt "$min" ]]; then
|
||
val=$max
|
||
fi
|
||
|
||
echo "$val"
|
||
}
|
||
|
||
while true; do
|
||
_prompt_text "$1"
|
||
printf "\033[37m%s\033[0m \033[1;90m❮\033[0m \033[36m%s%s\033[0m \033[1;90m❯\033[0m \033[37m%s\033[0m\n" "$min" "${padding:${#selected}}" "$selected" "$max" >&2
|
||
|
||
case $(_key_input) in
|
||
enter)
|
||
break
|
||
;;
|
||
left)
|
||
selected="$(_check_range $((selected - 1)))"
|
||
;;
|
||
right)
|
||
selected="$(_check_range $((selected + 1)))"
|
||
;;
|
||
esac
|
||
|
||
_cursor_to "$current_row"
|
||
done
|
||
|
||
echo "$selected"
|
||
}
|
||
|
||
validate_present() {
|
||
if [ "$1" != "" ]; then
|
||
return 0
|
||
else
|
||
error "Please specify the value"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
show_error() {
|
||
echo -e "\033[91;1m✘ $1\033[0m" >&2
|
||
}
|
||
|
||
show_success() {
|
||
echo -e "\033[92;1m✔ $1\033[0m" >&2
|
||
}
|
||
|
||
detect_os() {
|
||
case "$OSTYPE" in
|
||
solaris*) echo "solaris" ;;
|
||
darwin*) echo "macos" ;;
|
||
linux*) echo "linux" ;;
|
||
bsd*) echo "bsd" ;;
|
||
msys*) echo "windows" ;;
|
||
cygwin*) echo "windows" ;;
|
||
*) echo "unknown" ;;
|
||
esac
|
||
}
|
||
|
||
get_opener() {
|
||
declare cmd
|
||
|
||
case "$(detect_os)" in
|
||
macos) cmd="open" ;;
|
||
linux) cmd="xdg-open" ;;
|
||
windows) cmd="start" ;;
|
||
*) cmd="" ;;
|
||
esac
|
||
|
||
echo "$cmd"
|
||
}
|
||
|
||
open_link() {
|
||
cmd="$(get_opener)"
|
||
|
||
if [[ "$cmd" == "" ]]; then
|
||
error "Your platform is not supported for opening links."
|
||
red "Please open the following URL in your preferred browser:"
|
||
red " ${1}"
|
||
return 1
|
||
fi
|
||
|
||
$cmd "$1"
|
||
|
||
if [[ $? -eq 1 ]]; then
|
||
error "Failed to open your browser."
|
||
red "Please open the following URL in your browser:"
|
||
red "${1}"
|
||
return 1
|
||
fi
|
||
|
||
return 0
|
||
}
|
||
|
||
guard_operation() {
|
||
if [[ -t 1 ]]; then
|
||
if [[ -z "$AUTO_CONFIRM" && -z "$LLM_AGENT_VAR_AUTO_CONFIRM" ]]; then
|
||
ans="$(confirm "${1:-Are you sure you want to continue?}")"
|
||
|
||
if [[ "$ans" == 0 ]]; then
|
||
error "Operation aborted!" 2>&1
|
||
exit 1
|
||
fi
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# Here is an example of a patch block that can be applied to modify the file to request the user's name:
|
||
# --- a/hello.py
|
||
# +++ b/hello.py
|
||
# \@@ ... @@
|
||
# def hello():
|
||
# - print("Hello World")
|
||
# + name = input("What is your name? ")
|
||
# + print(f"Hello {name}")
|
||
patch_file() {
|
||
awk '
|
||
FNR == NR {
|
||
lines[FNR] = $0
|
||
next;
|
||
}
|
||
|
||
{
|
||
patchLines[FNR] = $0
|
||
}
|
||
|
||
END {
|
||
totalPatchLines=length(patchLines)
|
||
totalLines = length(lines)
|
||
patchLineIndex = 1
|
||
|
||
mode = "none"
|
||
|
||
while (patchLineIndex <= totalPatchLines) {
|
||
line = patchLines[patchLineIndex]
|
||
|
||
if (line ~ /^--- / || line ~ /^\+\+\+ /) {
|
||
patchLineIndex++
|
||
continue
|
||
}
|
||
|
||
if (line ~ /^@@ /) {
|
||
mode = "hunk"
|
||
hunkIndex++
|
||
patchLineIndex++
|
||
continue
|
||
}
|
||
|
||
if (mode == "hunk") {
|
||
while (patchLineIndex <= totalPatchLines && line ~ /^[-+ ]|^\s*$/ && line !~ /^--- /) {
|
||
sanitizedLine = substr(line, 2)
|
||
|
||
if (line !~ /^\+/) {
|
||
hunkTotalOriginalLines[hunkIndex]++;
|
||
hunkOriginalLines[hunkIndex,hunkTotalOriginalLines[hunkIndex]] = sanitizedLine
|
||
}
|
||
|
||
if (line !~ /^-/) {
|
||
hunkTotalUpdatedLines[hunkIndex]++;
|
||
hunkUpdatedLines[hunkIndex,hunkTotalUpdatedLines[hunkIndex]] = sanitizedLine
|
||
}
|
||
|
||
patchLineIndex++
|
||
line = patchLines[patchLineIndex]
|
||
}
|
||
|
||
mode = "none"
|
||
} else {
|
||
patchLineIndex++
|
||
}
|
||
}
|
||
|
||
if (hunkIndex == 0) {
|
||
print "error: no patch" > "/dev/stderr"
|
||
exit 1
|
||
}
|
||
|
||
totalHunks = hunkIndex
|
||
hunkIndex = 1
|
||
|
||
for (lineIndex = 1; lineIndex <= totalLines; lineIndex++) {
|
||
line = lines[lineIndex]
|
||
nextLineIndex = 0
|
||
|
||
if (hunkIndex <= totalHunks && line == hunkOriginalLines[hunkIndex,1]) {
|
||
nextLineIndex = lineIndex + 1
|
||
|
||
for (i = 2; i <= hunkTotalOriginalLines[hunkIndex]; i++) {
|
||
if (lines[nextLineIndex] != hunkOriginalLines[hunkIndex,i]) {
|
||
nextLineIndex = 0
|
||
break
|
||
}
|
||
|
||
nextLineIndex++
|
||
}
|
||
}
|
||
|
||
if (nextLineIndex > 0) {
|
||
for (i = 1; i <= hunkTotalUpdatedLines[hunkIndex]; i++) {
|
||
print hunkUpdatedLines[hunkIndex,i]
|
||
}
|
||
|
||
hunkIndex++
|
||
lineIndex = nextLineIndex - 1;
|
||
} else {
|
||
print line
|
||
}
|
||
}
|
||
|
||
if (hunkIndex != totalHunks + 1) {
|
||
print "error: unable to apply patch" > "/dev/stderr"
|
||
exit 1
|
||
}
|
||
}
|
||
|
||
function inspectHunks() {
|
||
print "/* Begin inspecting hunks"
|
||
|
||
for (i = 1; i <= totalHunks; i++) {
|
||
print ">>>>>> Original"
|
||
|
||
for (j = 1; j <= hunkTotalOriginalLines[i]; j++) {
|
||
print hunkOriginalLines[i,j]
|
||
}
|
||
|
||
print "======"
|
||
|
||
for (j = 1; j <= hunkTotalUpdatedLines[i]; j++) {
|
||
print hunkUpdatedLines[i,j]
|
||
}
|
||
|
||
print "<<<<<< Updated"
|
||
}
|
||
|
||
print "End inspecting hunks */\n"
|
||
}' "$1" "$2"
|
||
}
|
||
|
||
guard_path() {
|
||
if [[ "$#" -ne 2 ]]; then
|
||
echo "Usage: guard_path <path> <confirmation_prompt>" >&2
|
||
exit 1
|
||
fi
|
||
|
||
if [[ -t 1 ]]; then
|
||
path="$(_to_real_path "$1")"
|
||
confirmation_prompt="$2"
|
||
|
||
if [[ ! "$path" == "$(pwd)"* && -z "$AUTO_CONFIRM" && -z "$LLM_AGENT_VAR_AUTO_CONFIRM" ]]; then
|
||
ans="$(confirm "$confirmation_prompt")"
|
||
|
||
if [[ "$ans" == 0 ]]; then
|
||
error "Operation aborted!" >&2
|
||
exit 1
|
||
fi
|
||
fi
|
||
fi
|
||
}
|
||
|
||
_to_real_path() {
|
||
path="$1"
|
||
|
||
if [[ $OS == "Windows_NT" ]]; then
|
||
path="$(cygpath -u "$path")"
|
||
fi
|
||
|
||
awk -v path="$path" -v pwd="$PWD" '
|
||
BEGIN {
|
||
if (path !~ /^\//) {
|
||
path = pwd "/" path
|
||
}
|
||
|
||
if (path ~ /\/\.{1,2}?$/) {
|
||
isDir = 1
|
||
}
|
||
|
||
split(path, parts, "/")
|
||
newPartsLength = 0
|
||
|
||
for (i = 1; i <= length(parts); i++) {
|
||
part = parts[i]
|
||
if (part == "..") {
|
||
if (newPartsLength > 0) {
|
||
delete newParts[newPartsLength--]
|
||
}
|
||
} else if (part != "." && part != "") {
|
||
newParts[++newPartsLength] = part
|
||
}
|
||
}
|
||
|
||
if (isDir == 1 || newPartsLength == 0) {
|
||
newParts[++newPartsLength] = ""
|
||
}
|
||
|
||
printf "/"
|
||
|
||
for (i = 1; i <= newPartsLength; i++) {
|
||
newPart = newParts[i]
|
||
printf newPart
|
||
if (i < newPartsLength) {
|
||
printf "/"
|
||
}
|
||
}
|
||
}'
|
||
}
|