From 89b386e1162ac49d6f440f4867bcfdc74b0a9334 Mon Sep 17 00:00:00 2001 From: Benjamin Lupton Date: Thu, 22 Feb 2024 11:40:27 +0800 Subject: [PATCH 01/38] ask: support `--default=`, close #99 (#213) * ask: support editing the default value /ref 6357faca25eafcc47d70bf11e9d75d1bbd12e616 * ask: as read now supports defaults, redo the confirm flow /ref ddff4672561f5266cf96901b9875fc98fb28d211 * ask: use cursor rather than alt tty, implement --linger /ref 4d3e6fb48e5e0b58fb31cbc7c3431decc9de386b --- commands/ask | 217 ++++++++++++++++++++++++++----------------- commands/choose-menu | 6 +- commands/confirm | 9 +- 3 files changed, 139 insertions(+), 93 deletions(-) diff --git a/commands/ask b/commands/ask index e8d959f82..bad01e279 100755 --- a/commands/ask +++ b/commands/ask @@ -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 @@ -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+=("$@") @@ -133,46 +139,94 @@ 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 @@ -180,89 +234,82 @@ function ask_() ( 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 diff --git a/commands/choose-menu b/commands/choose-menu index 2c7d3fc05..dfb200d6c 100755 --- a/commands/choose-menu +++ b/commands/choose-menu @@ -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 diff --git a/commands/confirm b/commands/confirm index 7f902a7a1..708162515 100755 --- a/commands/confirm +++ b/commands/confirm @@ -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 @@ -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 From ffe2193537c97430fb3ddf8a4bd5e0830909cac9 Mon Sep 17 00:00:00 2001 From: Benjamin Lupton Date: Sun, 25 Feb 2024 12:11:40 +0800 Subject: [PATCH 02/38] ask: integrate #99 (#213) - implement `--linger`, default to enabled - fix readline defaults failing when programatic stdin - fix tests - fix `echo-clear-lines` not retaining inlines which caused incorrect lines to be cleared - adapt callers for new defaults and expectations --- commands/ask | 118 ++++++++++++++++++++++++++------------ commands/dorothy | 5 +- commands/echo-trim-colors | 8 ++- commands/setup-dns | 6 +- commands/setup-git | 22 +++---- commands/setup-node | 16 +++--- commands/sparse-vault | 4 +- 7 files changed, 114 insertions(+), 65 deletions(-) diff --git a/commands/ask b/commands/ask index bad01e279..78a777fea 100755 --- a/commands/ask +++ b/commands/ask @@ -5,56 +5,63 @@ function ask_test() ( echo-segment --h1="TEST: $0" eval-tester --name='default response -confirm' --stdout='a default response' \ - -- ask --question='What is your response?' --default='a default response' + -- ask --question='What is your response?' --default='a default response' --no-confirm-default { - # confirm to enter a value + # confirm the default value sleep 3 echo - } | eval-tester --name='default response +confirm' --stdout='a default response' \ - -- ask --question='What is your response?' --default='a default response' --confirm + } | eval-tester --name='default response' --stdout='a default response' \ + -- ask --question='What is your response?' --default='a default response' { - # confirm to enter a value - sleep 3 - echo - - # enter the custom response + # provide the custom response sleep 3 print_line 'a custom response' - } | eval-tester --name='custom response -default -confirm' --stdout='a custom response' \ + } | eval-tester --name='custom response' --stdout='a custom response' \ -- ask --question='What is your response?' { - # confirm to enter a value - sleep 3 - echo - - # enter the custom response + # provide the custom response sleep 3 print_line 'a custom response' # confirm the custom response sleep 3 echo - } | eval-tester --name='custom response -default +confirm' --stdout='a custom response' \ + } | eval-tester --name='custom response +confirm' --stdout='a custom response' \ -- ask --question='What is your response?' --confirm { - # move down and select custom response + # confirm the default value + sleep 3 + echo + + # move down and change to custom response sleep 3 printf $'\eOB' sleep 3 echo - # enter the custom response + # provide the custom response sleep 3 print_line 'a custom response' # confirm the custom response sleep 3 echo - } | eval-tester --name='custom response +default +confirm' --stdout='a custom response' \ + } | eval-tester --name='custom response manual +default +confirm' --stdout='a custom response' \ + -- ask --question='What is your response?' --default='a default response' --confirm + + { + # override the default response with the custom response + sleep 3 + print_line 'a custom response' + + # confirm the provided value + sleep 3 + echo + } | eval-tester --name='custom response auto +default +confirm' --stdout='a custom response' \ -- ask --question='What is your response?' --default='a default response' --confirm echo-segment --g1="TEST: $0" @@ -101,7 +108,7 @@ function ask_() ( } # process - 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' + local item args=() option_question='' option_default='' option_timeout='' option_password='no' option_required='no' option_linger='yes' option_confirm_default='yes' option_confirm_input='no' while test "$#" -ne 0; do item="$1" shift @@ -125,6 +132,10 @@ function ask_() ( '--no-confirm-input'* | '--confirm-input'*) option_confirm_input="$(get-flag-value --affirmative --fallback="$option_confirm_input" -- "$item")" ;; + '--no-confirm'* | '--confirm'*) + option_confirm_default="$(get-flag-value --affirmative --fallback="$option_confirm_default" -- "$item")" + option_confirm_input="$option_confirm_default" + ;; '--') args+=("$@") shift $# @@ -175,10 +186,22 @@ function ask_() ( # helpers local ASKED='no' function send_result { + local human_result="$RESULT" + if test -z "$human_result"; then + human_result="$(echo-style --dim='[ nothing provided ]')" + fi if test "$option_linger" = 'yes'; then - print_line "$question_render"$'\n'"$RESULT" >"$tty_target" + print_line "$question_render" >"$tty_target" + if test ! -t 1 -o -z "$RESULT"; then + # only output result to tty, if stdout isn't going to tty + print_line "$human_result" >"$tty_target" + fi + fi + if test -z "$RESULT"; then + print_string "$RESULT" # empty string + else + print_line "$RESULT" fi - print_line "$RESULT" } function on_timeout { if is-value -- "$RESULT"; then @@ -197,24 +220,47 @@ function ask_() ( fi } function do_prompt { # has sideffects: RESULT, ASKED - local __read_status render render_rows - - # tty_auto + local __read_status=0 input render render_rows ASKED='yes' # not local - if test -n "$question_render"; then - print_line "$question_render" >"$tty_target" - fi while true; do - # -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")" + # reset render + if test -n "$question_render"; then + render="$question_render"$'\n' + fi + render+="$prompt" + + # test for stdin technique, as [read -i] does not support programatic stdin + if test ! -t 0; then + # we have a programatic stdin, so manually read the line without a tty prompt + # don't read multiple lines, as other lines are for subequent prompts, such as confirmations or the next program + print_string "$render" >"$tty_target" + IFS='' read -r -t 300 input || __read_status=$? + if test -z "$input" -o "$input" = $'\n'; then + # treat empty string and newline as default + : + elif [[ $input =~ ^[\s]*$ ]]; then + # treat only whitespace as empty value + RESULT='' + else + # treat everything else as manual input + RESULT="$input" + fi + else + # we have tty stdin, can do a prompt + # -i requires -e + IFS= read -r -t 300 -ei "$RESULT" -p "$render" RESULT || __read_status=$? + render+="$RESULT" + if test "$__read_status" -eq 0; then + render+=$'\n ' # add the enter key, and a space so that it doesn't get trimmed + fi + fi + # use <<< to ensure no inline on any of these + render="$(echo-trim-colors < <(print_string "$render"))" + render="$("$bin_gfold" -w "$size_columns" < <(print_string "$render"))" + render_rows="$("$bin_gwc" -l < <(print_string "$render"))" # move these lines up and erase - printf '\e[%sF\e[G\e[J' "$render_rows" >"$tty_target" + 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 diff --git a/commands/dorothy b/commands/dorothy index 60bb1383d..f459b59f2 100755 --- a/commands/dorothy +++ b/commands/dorothy @@ -977,7 +977,7 @@ function dorothy_() ( user='' fi user="$( - ask --required --confirm \ + ask --required \ --question="Enter your $where username." \ --default="$user" )" @@ -1045,8 +1045,7 @@ function dorothy_() ( EOF )" repo_url="$( - ask --confirm \ - --question="$question" + ask --question="$question" )" fi diff --git a/commands/echo-trim-colors b/commands/echo-trim-colors index 96f104606..1d4739b5c 100755 --- a/commands/echo-trim-colors +++ b/commands/echo-trim-colors @@ -35,11 +35,15 @@ function echo_trim_colors() ( # ===================================== # Action - function on_input { - # https://superuser.com/a/380778 + # https://superuser.com/a/380778 + function on_line { # trunk-ignore(shellcheck/SC2001) sed 's/\x1b\[[0-9;]*m//g' <<<"$1" } + function on_inline { + sed 's/\x1b\[[0-9;]*m//g' < <(print_string "$1") + } + # ^ note that on_input does not preserve inline stdinargs "$@" ) diff --git a/commands/setup-dns b/commands/setup-dns index 1a6879096..7d57acbd4 100755 --- a/commands/setup-dns +++ b/commands/setup-dns @@ -1591,14 +1591,14 @@ EOF # ask if required args missing if test -z "$tunnel"; then tunnel="$( - ask --required --confirm \ + ask --required \ --question="What will be name identifier of the tunnel?" )" fi if test "${#hostnames[@]}" -eq 0; then hostnames+=( "$( - ask --required --confirm \ + ask --required \ --question="What will be the hostname to access the tunnel? E.g. ${tunnel}.domain.com" )" ) @@ -1606,7 +1606,7 @@ EOF if test -z "$ingress"; then if test -z "$url"; then url="$( - ask --required --confirm \ + ask --required \ --question="What will be local URL the tunnel will expose?" )" fi diff --git a/commands/setup-git b/commands/setup-git index eb53cc090..99ddd786c 100755 --- a/commands/setup-git +++ b/commands/setup-git @@ -135,39 +135,39 @@ function setup_git() ( if test "$option_configure" = 'yes'; then # required GIT_NAME="$( - ask --required --confirm \ - --question="What is the name that you want to configure git with?" \ + ask --required \ + --question='What is the name that you want to configure git with?' \ --default="${GIT_NAME:-"$(get-profile name -- git ... || :)"}" )" GIT_EMAIL="$( - ask --required --confirm \ - --question="What is the email that you want to configure git with?" \ + ask --required \ + --question='What is the email that you want to configure git with?' \ --default="${GIT_EMAIL:-"$(get-profile email -- git ... || :)"}" )" # optional GITHUB_USERNAME="$( - ask --confirm \ - --question="What is the GitHub username that you want to configure git with?" \ + ask \ + --question='What is the GitHub username that you want to configure git with?' \ --default="${GITHUB_USERNAME:-"$(get-profile username -- git ... || :)"}" )" GITLAB_USERNAME="$( - ask --confirm \ - --question="What is the GitLab username that you want to configure git with?" \ + ask \ + --question='What is the GitLab username that you want to configure git with?' \ --default="${GITLAB_USERNAME:-"$(get-profile username -- git ... || :)"}" )" # required GIT_PROTOCOL="$( - choose-option --required --confirm \ + choose-option --required \ --question='Which git protocol to prefer?' \ --filter="$GIT_PROTOCOL" -- "${protocol_options[@]}" )" MERGE_TOOL="$( - choose-option --required --confirm \ + choose-option --required \ --question='Which merge/diff tool to prefer?' \ --filter="$MERGE_TOOL" -- "${tool_options[@]}" )" GIT_DEFAULT_BRANCH="$( - ask --required --confirm \ + ask --required \ --question='Which branch to use as the default for new repositories?' \ --default="$GIT_DEFAULT_BRANCH" )" diff --git a/commands/setup-node b/commands/setup-node index 119f29891..54b51f307 100755 --- a/commands/setup-node +++ b/commands/setup-node @@ -96,23 +96,23 @@ function setup_node() ( function configure_system_npm { echo-segment --h2='Configure npm' nvm-env -- npm config set init-author-name "$( - ask --required --confirm \ - --question="What is the profile name that you want to configure npm with?" \ + ask --required \ + --question='What is the profile name that you want to configure npm with?' \ --default="$(get-profile name -- npm ... || :)" )" nvm-env -- npm config set init-author-email "$( - ask --required --confirm \ - --question="What is the profile email that you want to configure npm with?" \ + ask --required \ + --question='What is the profile email that you want to configure npm with?' \ --default="$(get-profile email -- npm ... || :)" )" nvm-env -- npm config set init-author-url "$( - ask --required --confirm \ - --question="What is the profile homepage that you want to configure npm with?" \ + ask --required \ + --question='What is the profile homepage that you want to configure npm with?' \ --default="$(get-profile url -- npm ... || :)" )" nvm-env -- npm config set init-license "$( - ask --required --confirm \ - --question="What license do you want to configure npm to default to?" \ + ask --required \ + --question='What license do you want to configure npm to default to?' \ --default="$(npm config get init-license)" )" echo-segment --g2='Configure npm' diff --git a/commands/sparse-vault b/commands/sparse-vault index bf47aef82..7bdcd1484 100755 --- a/commands/sparse-vault +++ b/commands/sparse-vault @@ -87,12 +87,12 @@ function sparse_vault() ( # adjustments: create if test "$action" = 'create'; then option_name="$( - ask --required --confirm \ + ask --required \ --question='Enter the volume name.' \ --default="$option_name" )" option_size="$( - ask --required --confirm \ + ask --required \ --question='Enter its maximum size. E.g. MAXSIZE|100g|1t' \ --default="$option_size" )" From 69c540815190464cf88481c12678367052cfd5a6 Mon Sep 17 00:00:00 2001 From: Benjamin Lupton Date: Sat, 11 Nov 2023 19:30:08 +0800 Subject: [PATCH 03/38] ask: support editing the default value --- commands/ask | 47 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/commands/ask b/commands/ask index e8d959f82..a0fd10e16 100755 --- a/commands/ask +++ b/commands/ask @@ -83,7 +83,13 @@ function ask_() ( Specifies the default value if no user specified value is entered. --confirm - Specifies that the prompt should confirm the value before continuing. + Specifies that the prompt should confirm the (default/entered) value before continuing. + + --confirm-default + Specifies that the prompt should confirm the default value (if provided) before continuing. Defaults to enabled. + + --confirm-input + Specifies that the prompt should confirm the entered value before continuing. Defaults to disabled. --password Specifies that the prompt should hide the value when entering by using password mode. @@ -101,7 +107,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_password='no' option_required='no' option_confirm_default='yes' option_confirm_input='no' option_timeout='' while test "$#" -ne 0; do item="$1" shift @@ -116,8 +122,15 @@ function ask_() ( '--no-required'* | '--required'*) option_required="$(get-flag-value --affirmative --fallback="$option_required" -- "$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")" + ;; '--no-confirm'* | '--confirm'*) - option_confirm="$(get-flag-value --affirmative --fallback="$option_confirm" -- "$item")" + option_confirm_default="$(get-flag-value --affirmative --fallback="$option_confirm_default" -- "$item")" + option_confirm_input="$(get-flag-value --affirmative --fallback="$option_confirm_input" -- "$item")" ;; '--') args+=("$@") @@ -144,16 +157,16 @@ function ask_() ( # helpers 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" 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 @@ -163,10 +176,11 @@ function ask_() ( tty_auto ASKED='yes' # not local if test -n "${1-}"; then - print_line "$1" >"$tty_target" + echo-style --bold="$1" >"$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 '> ' RESULT || __read_status=$? if test "$__read_status" -eq 142; then return 60 # ETIMEDOUT 60 Operation timed out fi @@ -182,19 +196,24 @@ function ask_() ( 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" - return 0 - fi - # proceed with confirm + # we have an input if test "$ASKED" = 'yes'; then + # do we want to confirm the input value + if test "$option_confirm_input" = 'no'; then + print_line "$RESULT" + return 0 + fi if test "$option_password" = 'yes'; then choices+=('existing' 'use the entered password') else choices+=('existing' "use the entered value: [$RESULT]") fi else + # do we want to confirm the default value + if test "$option_confirm_default" = 'no'; then + print_line "$RESULT" + return 0 + fi if test "$option_password" = 'yes'; then choices+=('existing' 'use the preconfigured password') else From 83192662e0c87e6553bb3ccb4b8821f7b23f9a4f Mon Sep 17 00:00:00 2001 From: Benjamin Lupton Date: Sat, 11 Nov 2023 19:43:11 +0800 Subject: [PATCH 04/38] ask: as read now supports defaults, redo the confirm flow --- commands/ask | 163 ++++++++++++++++++++++++++------------------------- 1 file changed, 82 insertions(+), 81 deletions(-) diff --git a/commands/ask b/commands/ask index a0fd10e16..9c3f3608d 100755 --- a/commands/ask +++ b/commands/ask @@ -146,15 +146,23 @@ function ask_() ( # Action # prepare - local RESULT ASKED='no' tty_target + 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 # helpers + local ASKED='no' function on_timeout { if is-value -- "$RESULT"; then echo-style --notice="Ask timed out. Using the fallback value: " --code="$RESULT" >/dev/stderr @@ -171,7 +179,7 @@ function ask_() ( return 60 # ETIMEDOUT 60 Operation timed out fi } - function do_ask { # has sideffects: RESULT, ASKED + function do_prompt { # has sideffects: RESULT, ASKED local __read_status tty_auto ASKED='yes' # not local @@ -180,13 +188,18 @@ function ask_() ( fi while true; do # -i requires -e - __read_status=0 && read -r -t 300 -ei "$RESULT" -p '> ' RESULT || __read_status=$? + ____read_status=0 && read -r -t 300 -ei "$RESULT" -p '> ' RESULT || __read_status=$? 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 @@ -194,94 +207,82 @@ function ask_() ( do_validate } function do_validate { - local choose_status ask_status choice choices=() - if is-value -- "$RESULT"; then - # we have an input - if test "$ASKED" = 'yes'; then - # do we want to confirm the input value - if test "$option_confirm_input" = 'no'; then - print_line "$RESULT" - return 0 - fi - if test "$option_password" = 'yes'; then - choices+=('existing' 'use the entered password') - else - choices+=('existing' "use the entered value: [$RESULT]") - fi + 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 + print_line "$RESULT" + return 0 + fi + else + # we have asked, do we want to confirm the input value + if test "$option_confirm_input" = 'no'; then + print_line "$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 - # do we want to confirm the default value - if test "$option_confirm_default" = 'no'; then - print_line "$RESULT" - return 0 - fi - 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 + + # we want to confirm + eval_capture --statusvar=choose_status --stdoutvar=choice -- \ + choose-option \ + --timeout="$option_timeout" \ + --question="$option_question" \ + --label -- "${choices[@]}" - # 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 + # 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 + print_line "$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 "$option_question" - # 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 From 2cea4c4c61522fce7467405cfa1cf67ca3e0c4a4 Mon Sep 17 00:00:00 2001 From: Benjamin Lupton Date: Sat, 11 Nov 2023 20:25:23 +0800 Subject: [PATCH 05/38] ask: use cursor rather than alt tty, implement --linger --- commands/ask | 59 +++++++++++++++++++++++++++++++++++--------- commands/choose-menu | 6 ++--- commands/confirm | 9 +++---- 3 files changed, 55 insertions(+), 19 deletions(-) diff --git a/commands/ask b/commands/ask index 9c3f3608d..59c8965f3 100755 --- a/commands/ask +++ b/commands/ask @@ -107,7 +107,7 @@ function ask_() ( } # process - local item args=() option_question='' option_default='' option_password='no' option_required='no' option_confirm_default='yes' option_confirm_input='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 @@ -122,6 +122,9 @@ function ask_() ( '--no-required'* | '--required'*) option_required="$(get-flag-value --affirmative --fallback="$option_required" -- "$item")" ;; + '--no-linger'* | '--linger'*) + option_linger="$(get-flag-value linger --missing="$option_linger" -- "$item" | echo-affirmative --stdin)" + ;; '--no-confirm-default'* | '--confirm-default'*) option_confirm_default="$(get-flag-value --affirmative --fallback="$option_confirm_default" -- "$item")" ;; @@ -146,6 +149,7 @@ function ask_() ( # Action # prepare + # @todo implement cursor move fallback local tty_target tty_target="$(is-tty --fallback)" @@ -156,18 +160,41 @@ function ask_() ( 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 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. Had no fallback value, this is fine as the field was optional.' >/dev/stderr @@ -180,15 +207,25 @@ function ask_() ( fi } function do_prompt { # has sideffects: RESULT, ASKED - local __read_status - tty_auto + local __read_status render render_rows + + # tty_auto ASKED='yes' # not local - if test -n "${1-}"; then - echo-style --bold="$1" >"$tty_target" + if test -n "$question_render"; then + print_line "$question_render" >"$tty_target" fi while true; do # -i requires -e - ____read_status=0 && read -r -t 300 -ei "$RESULT" -p '> ' RESULT || __read_status=$? + __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 @@ -213,13 +250,13 @@ function ask_() ( if test "$ASKED" = 'no'; then # do we want to confirm the default value if is-value -- "$RESULT" && test "$option_confirm_default" = 'no'; then - print_line "$RESULT" + send_result return 0 fi else # we have asked, do we want to confirm the input value if test "$option_confirm_input" = 'no'; then - print_line "$RESULT" + send_result return 0 fi @@ -255,7 +292,7 @@ function ask_() ( # proceess the confirmation if test "$choice" = 'existing'; then # done, sucess - print_line "$RESULT" + send_result return 0 elif test "$choice" = 'custom'; then : # proceed with prompt @@ -272,7 +309,7 @@ function ask_() ( fi # prompt - eval_capture --statusvar=prompt_status -- do_prompt "$option_question" + eval_capture --statusvar=prompt_status -- do_prompt # check for failure if test "$prompt_status" -ne 0; then diff --git a/commands/choose-menu b/commands/choose-menu index 2c7d3fc05..dfb200d6c 100755 --- a/commands/choose-menu +++ b/commands/choose-menu @@ -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 diff --git a/commands/confirm b/commands/confirm index 7f902a7a1..708162515 100755 --- a/commands/confirm +++ b/commands/confirm @@ -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 @@ -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 From 9dd6c88a1e409b2b8cc59e143fef27ed127d2c2f Mon Sep 17 00:00:00 2001 From: Benjamin Lupton Date: Thu, 25 Apr 2024 17:49:20 +0800 Subject: [PATCH 06/38] choose-option/choose-menu merged into choose Still need to imlpement: - `--linger` - `--confirm-default=no` - `--confirm-input` ...and to make them consistent between ask, confirm, choose. This removes `--filter` from choose, as it is better facilitated by the new `--default(s)-fuzzy=` /close #188 --- commands.beta/choose-menu | 11 + commands.beta/choose-option | 11 + commands.beta/macos-settings | 4 +- commands.beta/macos-state | 12 +- commands.beta/macos-theme | 4 +- commands.beta/mail-sync | 8 +- commands.beta/svg-export | 4 +- commands.beta/wallhaven-helper | 2 +- commands/ask | 64 ++-- commands/btrfs-helper | 2 +- commands/checksum | 4 +- commands/{choose-menu => choose} | 379 +++++++++++++++++----- commands/choose-option | 537 ------------------------------- commands/choose-path | 4 +- commands/confirm | 35 +- commands/cpr | 10 +- commands/dorothy | 10 +- commands/dorothy-config | 2 +- commands/down | 2 +- commands/echo-checksum | 4 +- commands/get-devices | 2 +- commands/get-profile | 2 +- commands/git-helper | 10 +- commands/gocryptfs-helper | 14 +- commands/gpg-helper | 12 +- commands/macos-drive | 2 +- commands/openssl-helper | 12 +- commands/secret | 16 +- commands/setup-dns | 10 +- commands/setup-git | 8 +- commands/setup-hosts | 2 +- commands/setup-mac-appstore | 2 +- commands/setup-mac-brew | 6 +- commands/setup-util | 8 +- commands/setup-util-nerd-fonts | 4 +- commands/setup-util-ruby | 2 +- commands/setup-utils | 2 +- commands/sparse-vault | 10 +- commands/ssh-helper | 8 +- commands/what-is-listening | 4 +- docs/scripting/prompts.md | 7 +- sources/bash.bash | 133 +++++--- sources/history.fish | 4 +- sources/history.sh | 4 +- 44 files changed, 572 insertions(+), 821 deletions(-) create mode 100755 commands.beta/choose-menu create mode 100755 commands.beta/choose-option rename commands/{choose-menu => choose} (62%) delete mode 100755 commands/choose-option diff --git a/commands.beta/choose-menu b/commands.beta/choose-menu new file mode 100755 index 000000000..08fb7fb52 --- /dev/null +++ b/commands.beta/choose-menu @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +function choose_menu() ( + # b/c alias for choose + choose --index "$@" +) + +# fire if invoked standalone +if test "$0" = "${BASH_SOURCE[0]}"; then + choose_menu "$@" +fi diff --git a/commands.beta/choose-option b/commands.beta/choose-option new file mode 100755 index 000000000..0dd235795 --- /dev/null +++ b/commands.beta/choose-option @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +function choose_option() ( + # b/c alias for choose + choose "$@" +) + +# fire if invoked standalone +if test "$0" = "${BASH_SOURCE[0]}"; then + choose_option "$@" +fi diff --git a/commands.beta/macos-settings b/commands.beta/macos-settings index 7dac85e2b..561065387 100755 --- a/commands.beta/macos-settings +++ b/commands.beta/macos-settings @@ -55,7 +55,7 @@ function macos_settings() ( # ask local fodder choices fodder="$( - choose-option --multi \ + choose --multi \ --question="Which settings would you like to enable" \ --label -- \ dockside "Select which side of the screen the dock is on" \ @@ -96,7 +96,7 @@ function macos_settings() ( if is-needle --needle=dockside -- "${choices[@]}"; then local dock_side dock_side="$( - choose-option \ + choose \ --question='Which side to show the dock?' \ -- right left top bottom )" diff --git a/commands.beta/macos-state b/commands.beta/macos-state index 11fa9849a..64af69926 100755 --- a/commands.beta/macos-state +++ b/commands.beta/macos-state @@ -61,18 +61,18 @@ function macos_state() ( # ensure action="$( - choose-option \ + choose \ --question='What do you want to do?' \ - --label --filter="$action" -- \ + --label --default="$action" -- \ backup 'Make a backup?' \ restore 'Restore a backup?' )" # What is the local volume? local_root="$( - choose-option --required \ + choose --required \ --question='What is your local volume?' \ - --filter="$local_root" -- /Volumes/* + --default="$local_root" -- /Volumes/* )" # ===================================== @@ -94,7 +94,7 @@ function macos_state() ( echo print_line 'Unable to find the Time Machine backup automatically, attempting manual resolution...' time_backup_volume="$( - choose-option --required \ + choose --required \ --question='Which volume contains the time machine backups?' \ -- /Volumes/* )" @@ -173,7 +173,7 @@ function macos_state() ( else # ask backup_root="$( - choose-option --required \ + choose --required \ --question='Which backup volume to use?' \ -- "$(fs-join -- "$backup_root" 'Volumes')/"* )" diff --git a/commands.beta/macos-theme b/commands.beta/macos-theme index 6e8300001..8005d4dc9 100755 --- a/commands.beta/macos-theme +++ b/commands.beta/macos-theme @@ -85,9 +85,9 @@ function macos_theme() ( # ensure theme="$( - choose-option --required \ + choose --required \ --question='Which theme to apply?' \ - --filter="$theme" -- dark light + --default="$theme" -- dark light )" # ===================================== diff --git a/commands.beta/mail-sync b/commands.beta/mail-sync index 7913ddbb6..ad77faf53 100755 --- a/commands.beta/mail-sync +++ b/commands.beta/mail-sync @@ -75,9 +75,9 @@ function mail_sync() ( # origin IMAP server type1="$( - choose-option --required \ + choose --required \ --question='Who is the host of the origin IMAP server?' \ - --filter="$type1" -- "${types[@]}" + --default="$type1" -- "${types[@]}" )" if test "$type1" = 'gmail'; then args+=( @@ -124,9 +124,9 @@ function mail_sync() ( # target IMAP server type2="$( - choose-option --required \ + choose --required \ --question='Who is the host of the target IMAP server?' \ - --filter="$type2" -- "${types[@]}" + --default="$type2" -- "${types[@]}" )" if test "$type2" = 'gmail'; then args+=( diff --git a/commands.beta/svg-export b/commands.beta/svg-export index e9c916434..718962f40 100755 --- a/commands.beta/svg-export +++ b/commands.beta/svg-export @@ -75,9 +75,9 @@ function svg_export() ( # ensure correct format format="$( - choose-option --required \ + choose --required \ --question='Which format to export to?' \ - --filter="$format" -- "${formats[@]}" + --default="$format" -- "${formats[@]}" )" # ===================================== diff --git a/commands.beta/wallhaven-helper b/commands.beta/wallhaven-helper index c28090f52..209101e29 100755 --- a/commands.beta/wallhaven-helper +++ b/commands.beta/wallhaven-helper @@ -62,7 +62,7 @@ function wallhaven_helper() ( local collections collection mapfile -t collections < <(fetch "https://wallhaven.cc/api/v1/collections?apikey=$apikey" | jq -r '.data[] | (.id, .label)') collection="$( - choose-option \ + choose \ --question='Which collection to download?' \ --label --visual="\$LABEL [\$VALUE]" \ -- "${collections[@]}" diff --git a/commands/ask b/commands/ask index 59c8965f3..19a7d0d07 100755 --- a/commands/ask +++ b/commands/ask @@ -76,29 +76,33 @@ function ask_() ( ask [...options] OPTIONS: - --question= - Specifies the question that the prompt will be answering. + | --question= + Display this question in the prompt. If specified multiple times, they will be joined by newline, and only the first will be lingered. --default= - Specifies the default value if no user specified value is entered. + Default value if no user specified value is entered. - --confirm - Specifies that the prompt should confirm the (default/entered) value before continuing. + --[no-]confirm=[yes|no] + Confirm the (default/entered) value before continuing. - --confirm-default - Specifies that the prompt should confirm the default value (if provided) before continuing. Defaults to enabled. + --[no-]confirm-default=[YES|no] | --[no-]skip-default=[yes|NO] + Confirm the default value (if provided) before continuing. Defaults to enabled. - --confirm-input - Specifies that the prompt should confirm the entered value before continuing. Defaults to disabled. + --[no-]confirm-input=[yes|NO] + Confirm the entered value before continuing. Defaults to disabled. - --password - Specifies that the prompt should hide the value when entering by using password mode. + --[no-]password=[yes|NO] + Hide the value when entering by using password mode. - --required - Specifies that the prompt should not continue until a value is provided. + --[no-]required=[yes|NO] + Do not continue until a value is provided. Disable aborting the prompt. + + --[no-]linger=[yes|NO] + Whether the prompt should persist afterwards. --timeout= - Specifies a custom timeout value in seconds. + Custom timeout value in seconds. + EOF if test "$#" -ne 0; then echo-error "$@" @@ -107,23 +111,19 @@ function ask_() ( } # process - 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' + local item args=() option_question=() + local option_default='' option_confirm_default='yes' option_confirm_input='no' + local option_required='no' option_password='no' + local option_linger='no' option_timeout='' while test "$#" -ne 0; do item="$1" shift case "$item" in '--help' | '-h') help ;; - '--question='*) option_question="${item#*=}" ;; + '--question='*) option_question+=("${item#*=}") ;; '--default='*) option_default="${item#*=}" ;; - '--timeout='*) option_timeout="${item#*=}" ;; - '--no-password'* | '--password'*) - option_password="$(get-flag-value --affirmative --fallback="$option_password" -- "$item")" - ;; - '--no-required'* | '--required'*) - option_required="$(get-flag-value --affirmative --fallback="$option_required" -- "$item")" - ;; - '--no-linger'* | '--linger'*) - option_linger="$(get-flag-value linger --missing="$option_linger" -- "$item" | echo-affirmative --stdin)" + '--no-skip-default'* | '--skip-default'*) + option_confirm_default="$(get-flag-value --non-affirmative --fallback="$option_confirm_default" -- "$item")" ;; '--no-confirm-default'* | '--confirm-default'*) option_confirm_default="$(get-flag-value --affirmative --fallback="$option_confirm_default" -- "$item")" @@ -135,13 +135,23 @@ function ask_() ( option_confirm_default="$(get-flag-value --affirmative --fallback="$option_confirm_default" -- "$item")" option_confirm_input="$(get-flag-value --affirmative --fallback="$option_confirm_input" -- "$item")" ;; + '--no-required'* | '--required'*) + option_required="$(get-flag-value --affirmative --fallback="$option_required" -- "$item")" + ;; + '--no-password'* | '--password'*) + option_password="$(get-flag-value --affirmative --fallback="$option_password" -- "$item")" + ;; + '--no-linger'* | '--linger'*) + option_linger="$(get-flag-value linger --missing="$option_linger" -- "$item" | echo-affirmative --stdin)" + ;; + '--timeout='*) option_timeout="${item#*=}" ;; '--') args+=("$@") shift $# break ;; '--'*) help "An unrecognised flag was provided: $item" ;; - *) help "An unrecognised argument was provided: $item" ;; + *) option_question+=("$@") ;; esac done @@ -273,7 +283,7 @@ function ask_() ( # we want to confirm eval_capture --statusvar=choose_status --stdoutvar=choice -- \ - choose-option \ + choose \ --timeout="$option_timeout" \ --question="$option_question" \ --label -- "${choices[@]}" diff --git a/commands/btrfs-helper b/commands/btrfs-helper index 550e5ec33..2d87347d7 100755 --- a/commands/btrfs-helper +++ b/commands/btrfs-helper @@ -327,7 +327,7 @@ function btrfs_helper() ( -- btrfs balance status --verbose "$existing_mount"; then # balance is running, ask what to do - if test "$(choose-option --required --label --question="Do you wish to?" -- 'resume' "Resume the existing balance, and abort a new $strategy balance." 'start' "Cancel the existing balance and resume, and start a new $strategy balance.")" = 'resume'; then + if test "$(choose --required --label --question="Do you wish to?" -- 'resume' "Resume the existing balance, and abort a new $strategy balance." 'start' "Cancel the existing balance and resume, and start a new $strategy balance.")" = 'resume'; then # usage: btrfs balance resume # Resume interrupted balance sudo-helper --wrap --confirm \ diff --git a/commands/checksum b/commands/checksum index 936866932..971cbb9d1 100755 --- a/commands/checksum +++ b/commands/checksum @@ -81,9 +81,9 @@ function checksum_() ( # ensure algorithm option_algorithm="$( - choose-option --required \ + choose --required \ --question='Which checksum algorithm do you wish to use?' \ - --filter="$option_algorithm" -- "${algorithms[@]}" + --default-fuzzy="$option_algorithm" -- "${algorithms[@]}" )" # ensure paths diff --git a/commands/choose-menu b/commands/choose similarity index 62% rename from commands/choose-menu rename to commands/choose index dfb200d6c..f257ffa5a 100755 --- a/commands/choose-menu +++ b/commands/choose @@ -7,24 +7,28 @@ # - [ ] limit the options output to [$LINES - header] # - [ ] if one gets to $LINES, and there are truncated values, then scroll downwards # - [ ] support $COLUMNS - if a menu item is larger than the column, then it will show all of it when active +# - [ ] ctrl n/p for navigating up/down. +# - [ ] `hjkl` vim arrow keys. -function choose_menu_test() ( +function choose_test() ( source "$DOROTHY/sources/bash.bash" echo-segment --h1="TEST: $0" + ## choose-menu ## + # timeout response not required eval-tester --name='timeout response not required' --status='60' --stderr='Read timed out [60], without selection.' \ - -- env NO_COLOR=yes choose-menu --question='timeout response not required' --timeout=5 -- a b c + -- env NO_COLOR=yes choose --index --question='timeout response not required' --timeout=5 -- a b c # timeout response is required eval-tester --name='timeout response is required' --status='60' --stderr='Read timed out [60], without selection.' \ - -- env NO_COLOR=yes choose-menu --question='timeout response is required' --timeout=5 --required -- a b c + -- env NO_COLOR=yes choose --index --question='timeout response is required' --timeout=5 --required -- a b c # default response { sleep 3 } | eval-tester --name='default response' --stdout='1' --ignore-stderr \ - -- choose-menu --question='default response' --timeout=2 --default=b -- a b c + -- choose --index --question='default response' --timeout=2 --default=b -- a b c # default response should clear on movement { @@ -33,13 +37,13 @@ function choose_menu_test() ( printf $'\eOB' sleep 3 } | eval-tester --name='default response should clear on movement' --status='60' --stdout='' --ignore-stderr \ - -- choose-menu --question='default response should clear on movement' --timeout=10 --default=b -- a b c + -- choose --index --question='default response should clear on movement' --timeout=10 --default=b -- a b c # default multi response { sleep 3 } | eval-tester --name='default multi response' --stdout=$'1\n2' --ignore-stderr \ - -- choose-menu --question='default multi response' --timeout=2 --multi --default=b --default=c -- a b c + -- choose --index --question='default multi response' --timeout=2 --multi --default=b --default=c -- a b c # default multi response should not clear on movement { @@ -47,20 +51,20 @@ function choose_menu_test() ( printf $'\eOB' sleep 3 } | eval-tester --name='default multi response should not clear on movement' --stdout=$'1\n2' --ignore-stderr \ - -- choose-menu --question='default multi response should not clear on movement' --timeout=10 --multi --default=b --default=c -- a b c + -- choose --index --question='default multi response should not clear on movement' --timeout=10 --multi --default=b --default=c -- a b c # multiline defaults { sleep 3 } | eval-tester --name='default multiline response' --stdout=$'1\n2\n3' --ignore-stderr \ - -- choose-menu --question='default multiline response' --timeout=2 --multi --default=$'b\nB' --defaults=$'c\nd' -- a $'b\nB' c d + -- choose --index --question='default multiline response' --timeout=2 --multi --default=$'b\nB' --defaults=$'c\nd' -- a $'b\nB' c d # first selection { sleep 3 echo } | eval-tester --name='first response' --stdout='0' \ - -- choose-menu --question='first selection' -- a b c + -- choose --index --question='first selection' -- a b c # second selection { @@ -70,7 +74,7 @@ function choose_menu_test() ( sleep 3 echo } | eval-tester --name='second response' --stdout='1' \ - -- choose-menu --question='second selection' -- a b c + -- choose --index --question='second selection' -- a b c # abort response via escape { @@ -78,12 +82,59 @@ function choose_menu_test() ( sleep 3 printf $'\x1b' } | eval-tester --name='abort response via escape' \ - -- choose-menu --question='abort response via escape' -- a b c + -- choose --index --question='abort response via escape' -- a b c + + ## choose ## + + # timeout response not required + eval-tester --name='timeout response not required' --status='0' --stderr=$'Read timed out [60], without selection.\nMenu timed out [60], no result, not required.' \ + -- env NO_COLOR=yes choose --question='timeout response not required' --timeout=5 -- a b c + + # timeout response is required + eval-tester --name='timeout response is required' --status='60' --stderr=$'Read timed out [60], without selection.\nMenu timed out [60], no result, is required.' \ + -- env NO_COLOR=yes choose --question='timeout response is required' --timeout=5 --required -- a b c + + # default response + { + sleep 3 + } | eval-tester --name='default response' --stdout='b' --ignore-stderr \ + -- choose --question='default response' --timeout=2 --default=b -- a b c + + # default multi response + { + sleep 3 + } | eval-tester --name='default multi response' --stdout=$'b\nc' --ignore-stderr \ + -- choose --question='default multi response' --timeout=2 --multi --default=b --default=c -- a b c + + # first selection + { + sleep 3 + echo + } | eval-tester --name='first selection' --stdout='a' \ + -- choose --question='first selection' -- a b c + + # second selection + { + # move down and select second response + sleep 3 + printf $'\eOB' + sleep 3 + echo + } | eval-tester --name='second selection' --stdout='b' \ + -- choose --question='second selection' -- a b c + + # abort response via escape + { + # press escape key + sleep 3 + printf $'\x1b' + } | eval-tester --name='abort response via escape' \ + -- choose --question='abort response via escape' -- a b c echo-segment --g1="TEST: $0" return 0 ) -function choose_menu() ( +function choose_() ( source "$DOROTHY/sources/bash.bash" source "$DOROTHY/sources/tty.bash" require_array 'mapfile' @@ -94,24 +145,50 @@ function choose_menu() ( function help { cat <<-EOF >/dev/stderr ABOUT: - Display a menu that the user can navigate using the keyboard. + Prompt the user to select an item from the menu, in a clean and robust way. USAGE: - choose-menu [...options] -- ... - - RETURNS: - The index of the result + choose [...options] -- ... OPTIONS: - --default= - --defaults= - Pre-selected s. + | --question= + Display this question in the prompt. If specified multiple times, they will be joined by newline, and only the first will be lingered. - --question= - Question to display as the prompt. + --label -- ...[