#!/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 "$@" &2 } _cursor_blink_off() { echo -en "\033[?25l" >&2 } _cursor_to() { echo -en "\033[$1;${2:-1}H" >&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 " >&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 "/" } } }' }