Skip to content

Commit

Permalink
ask: support --default=<value>, close #99 (#213)
Browse files Browse the repository at this point in the history
* ask: support editing the default value /ref 6357fac
* ask: as read now supports defaults, redo the confirm flow /ref ddff467
* ask: use cursor rather than alt tty, implement --linger /ref 4d3e6fb
  • Loading branch information
balupton committed Feb 25, 2024
1 parent f9c8377 commit 89b386e
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 93 deletions.
217 changes: 132 additions & 85 deletions commands/ask
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ function ask_() (
}

# process
local item args=() option_question='' option_default='' option_password='no' option_required='no' option_confirm='no' option_timeout=''
local item args=() option_question='' option_default='' option_timeout='' option_password='no' option_required='no' option_linger='no' option_confirm_default='yes' option_confirm_input='no'
while test "$#" -ne 0; do
item="$1"
shift
Expand All @@ -116,8 +116,14 @@ function ask_() (
'--no-required'* | '--required'*)
option_required="$(get-flag-value --affirmative --fallback="$option_required" -- "$item")"
;;
'--no-confirm'* | '--confirm'*)
option_confirm="$(get-flag-value --affirmative --fallback="$option_confirm" -- "$item")"
'--no-linger'* | '--linger'*)
option_linger="$(get-flag-value --affirmative --fallback="$option_linger" -- "$item")"
;;
'--no-confirm-default'* | '--confirm-default'*)
option_confirm_default="$(get-flag-value --affirmative --fallback="$option_confirm_default" -- "$item")"
;;
'--no-confirm-input'* | '--confirm-input'*)
option_confirm_input="$(get-flag-value --affirmative --fallback="$option_confirm_input" -- "$item")"
;;
'--')
args+=("$@")
Expand All @@ -133,136 +139,177 @@ function ask_() (
# Action

# prepare
local RESULT ASKED='no' tty_target
# @todo implement cursor move fallback
local tty_target
tty_target="$(is-tty --fallback)"

# adjust result
local RESULT
if test -n "$option_default"; then
RESULT="$option_default"
else
RESULT=''
fi

# adjust timeout to one minute if we have a default value, or if optional
if test -z "$option_timeout" && (is-value -- "$RESULT" || test "$option_required" = 'no'); then
option_timeout=60
fi

# adjust question
local question_render prompt
prompt='> '
question_render="$(echo-style --bold="$option_question")"

# adjust tty
local size_columns bin_gfold bin_gwc
size_columns="$(tput cols)"
if is-mac; then
bin_gfold="$(type -P 'gfold' 2>/dev/null || :)"
bin_gwc="$(type -P 'gwc' 2>/dev/null || :)"
else
bin_gfold="$(type -P 'fold' 2>/dev/null || :)"
bin_gwc="$(type -P 'wc' 2>/dev/null || :)"
fi

# helpers
local ASKED='no'
function send_result {
if test "$option_linger" = 'yes'; then
print_line "$question_render"$'\n'"$RESULT" >"$tty_target"
fi
print_line "$RESULT"
}
function on_timeout {
if is-value -- "$RESULT"; then
echo-style --notice="Ask timed out, using fallback value: " --code="$RESULT" >/dev/stderr
echo-style --notice="Ask timed out. Using the fallback value: " --code="$RESULT" >/dev/stderr
sleep 5
print_line "$RESULT"
send_result
return 0
elif test "$option_required" = 'no'; then
echo-style --notice='Ask timed out, as the field was optional will use no value.' >/dev/stderr
echo-style --notice='Ask timed out. Had no fallback value, this is fine as the field was optional.' >/dev/stderr
sleep 5
return 0
else
echo-style --warning='Ask timed out, with no fallback.' >/dev/stderr
echo-style --warning='Ask timed out. Had no fallback value... the field was required.' >/dev/stderr
sleep 5
return 60 # ETIMEDOUT 60 Operation timed out
fi
}
function do_ask { # has sideffects: RESULT, ASKED
local __read_status
tty_auto
function do_prompt { # has sideffects: RESULT, ASKED
local __read_status render render_rows

# tty_auto
ASKED='yes' # not local
if test -n "${1-}"; then
print_line "$1" >"$tty_target"
if test -n "$question_render"; then
print_line "$question_render" >"$tty_target"
fi
while true; do
__read_status=0 && read -r -t 300 -r -p '> ' RESULT || __read_status=$?
# -i requires -e
__read_status=0 && read -r -t 300 -ei "$RESULT" -p "$prompt" RESULT || __read_status=$?

# \n at the end to factor in the enter key
render="$(echo-trim-colors -- "$question_render"$'\n'"$prompt$RESULT")"
render="$("$bin_gfold" -w "$size_columns" <<<"$render")"
render_rows="$("$bin_gwc" -l <<<"$render")"

# move these lines up and erase
printf '\e[%sF\e[G\e[J' "$render_rows" >"$tty_target"

if test "$__read_status" -eq 142; then
return 60 # ETIMEDOUT 60 Operation timed out
fi
if is-value -- "$RESULT"; then
# we have a value, proceed
break
elif test "$option_required" = 'no'; then
elif test "$option_required" = 'yes'; then
# ask again
continue
else
# no result, optional, set value to empty, and proceed
RESULT=''
break
fi
done
do_validate
}
function do_validate {
local choose_status ask_status choice choices=()
if is-value -- "$RESULT"; then
# we have a value, so go for it
if test "$option_confirm" != 'yes'; then
print_line "$RESULT"
local choose_status prompt_status choice choices=()

# have we prompted?
if test "$ASKED" = 'no'; then
# do we want to confirm the default value
if is-value -- "$RESULT" && test "$option_confirm_default" = 'no'; then
send_result
return 0
fi
# proceed with confirm
if test "$ASKED" = 'yes'; then
if test "$option_password" = 'yes'; then
choices+=('existing' 'use the entered password')
else
choices+=('existing' "use the entered value: [$RESULT]")
fi
else
# we have asked, do we want to confirm the input value
if test "$option_confirm_input" = 'no'; then
send_result
return 0
fi

# redo choices, has to be redone each time due to result
if test "$option_password" = 'yes'; then
choices+=('existing' 'use the entered password')
else
if test "$option_password" = 'yes'; then
choices+=('existing' 'use the preconfigured password')
else
choices+=('existing' "use the preconfigured value: [$RESULT]")
fi
choices+=('existing' "use the entered value: [$RESULT]")
fi
fi
if test "$ASKED" = 'yes'; then
choices+=('custom' 'redo the entered value')
else
choices+=('custom' 'enter a value')
fi
if test "$option_required" = 'no'; then
choices+=('none' 'use no value')
fi
if test "$option_required" = 'no'; then
choices+=('none' 'use no value')
fi

# as need to confirm, adjust the timeout
if test -z "$option_timeout" && (is-value -- "$RESULT" || test "$option_required" = 'no'); then
# timeout of one minute for confirms of existing values, or optional values
option_timeout=60
# we want to confirm
eval_capture --statusvar=choose_status --stdoutvar=choice -- \
choose-option \
--timeout="$option_timeout" \
--question="$option_question" \
--label -- "${choices[@]}"

# check the confirmation
if test "$choose_status" -eq 60; then
echo-style --error="Choose timed out: $choose_status" >/dev/stderr
on_timeout
return
elif test "$choose_status" -ne 0; then
echo-style --error="Choose failed: $choose_status" >/dev/stderr
sleep 3
return "$choose_status"
fi

# proceess the confirmation
if test "$choice" = 'existing'; then
# done, sucess
send_result
return 0
elif test "$choice" = 'custom'; then
: # proceed with prompt
elif test "$choice" = 'none'; then
# done, sucess
echo
return 0
else
# unknown error
echo-style --error="Invalid choice: $choice" >/dev/stderr
sleep 3
return 14 # EFAULT 14 Bad address
fi
fi

# ask
eval_capture --statusvar=choose_status --stdoutvar=choice -- \
choose-option \
--timeout="$option_timeout" \
--question="$option_question" \
--label -- "${choices[@]}"
# prompt
eval_capture --statusvar=prompt_status -- do_prompt

# check
if test "$choose_status" -eq 60; then
echo-style --error="Choose timed out: $choose_status" >/dev/stderr
# check for failure
if test "$prompt_status" -ne 0; then
# timeout probably
on_timeout
return
elif test "$choose_status" -ne 0; then
echo-style --error="Choose failed: $choose_status" >/dev/stderr
sleep 3
return "$choose_status"
fi

# handle
if test "$choice" = 'existing'; then
# done, sucess
print_line "$RESULT"
return 0
elif test "$choice" = 'custom'; then
# ask
eval_capture --statusvar=ask_status -- do_ask "$option_question"

# check for failure
if test "$ask_status" -ne 0; then
# timeout probably
on_timeout
return
fi

# done, success
return 0
elif test "$choice" = 'none'; then
# done, sucess
echo
return 0
else
# unknown error
echo-style --error="Invalid choice: $choice" >/dev/stderr
sleep 3
return 14 # EFAULT 14 Bad address
fi
# done, success
return 0
}

# act
Expand Down
6 changes: 3 additions & 3 deletions commands/choose-menu
Original file line number Diff line number Diff line change
Expand Up @@ -365,14 +365,14 @@ function choose_menu() (
if test "$size_rows" -ne "$size_rows_prior" -o "$size_columns" -ne "$size_columns_prior"; then
size_content="$((size_columns - 5))"
# recalculate for new size
menu_header_shrunk="$(echo-trim-colors "$menu_header" | "$bin_gfold" -w "$size_columns")"
menu_header_shrunk="$(echo-trim-colors -- "$menu_header" | "$bin_gfold" -w "$size_columns")"
menu_header_size="$("$bin_gwc" -l <<<"${menu_header_shrunk}")"
menu_hint="${menu_hint_standard}${menu_hint_extras}"
menu_hint_shrunk="$(echo-trim-colors "$menu_hint" | "$bin_gfold" -w "$size_columns")"
menu_hint_shrunk="$(echo-trim-colors -- "$menu_hint" | "$bin_gfold" -w "$size_columns")"
menu_hint_size="$("$bin_gwc" -l <<<"${menu_hint_shrunk}")"
if test "$menu_hint_size" -gt 1; then
menu_hint="${menu_hint_standard}"
menu_hint_shrunk="$(echo-trim-colors "$menu_hint" | "$bin_gfold" -w "$size_columns")"
menu_hint_shrunk="$(echo-trim-colors -- "$menu_hint" | "$bin_gfold" -w "$size_columns")"
menu_hint_size="$("$bin_gwc" -l <<<"${menu_hint_shrunk}")"
fi
# move start index to current item, as otherwise it could be out of range
Expand Down
9 changes: 4 additions & 5 deletions commands/confirm
Original file line number Diff line number Diff line change
Expand Up @@ -258,17 +258,16 @@ function confirm_() (
CURSOR_COLUMN=''
print_string "$prompt " >"$tty_target"
# send an ansi query to fetch the cursor row and column, returns [^[[24;80R] where 24 is row, 80 is column
# use _ to discard, the first read var is garbage, the second read var is the column, the final read var is the column
# use _ to discard, the first read var is garbage, the second read var is the row, the final read var is the column
# use a 2 second timeout, as otherwise [confirm --test] on macos sonoma will wait forever
# shorter timeouts aren't suitable as slower machines take a while for the response
# we are already in a TTY, so can usually guarantee an answer, and the read will complete immediately upon a response thanks to [-d R] which completes reading when the R is read, which is the final character of the response query
local _
IFS='[;' read -t 2 -srd R -p $'\e[6n' _ _ CURSOR_COLUMN <"$tty_target" || :

# output the body if it exists
# output the body on a newline if it exists
if test -n "$body"; then
echo >"$tty_target"
print_string "$body" >"$tty_target"
print_string $'\n'"$body" >"$tty_target"

# move these lines up
if test "$body_lines" -ne 0; then
Expand Down Expand Up @@ -308,7 +307,7 @@ function confirm_() (
fi
finished=yes

# make sure to erase question, as ctrl+c buggers everything
# erase from start of current row to end of screen, as ctrl+c buggers everything
printf '\e[G\e[J' >"$tty_target"

# output the finale
Expand Down

0 comments on commit 89b386e

Please sign in to comment.