From 762ae97a36f5dc5fbc28e35a1b92fcc1914697a1 Mon Sep 17 00:00:00 2001 From: Timo Reymann Date: Wed, 15 Feb 2023 00:19:14 +0100 Subject: [PATCH] feat: Add first modules This adds the first modules blocks from the initial proof of concept, with basic docs. Currently missing is bundling and usage instructions --- .development/docs/Dockerfile | 17 ++ .gitignore | 1 + .pre-commit-config.yaml | 32 ++++ LICENSE | 201 ++++++++++++++++++++++++ Makefile | 14 ++ README.md | 15 +- docs/modules/Logging.md | 38 +++++ docs/modules/Prompts.md | 124 +++++++++++++++ docs/modules/User-Feedback.md | 29 ++++ renovate.json | 15 ++ src/logging.sh | 63 ++++++++ src/main.sh | 8 + src/prompts.sh | 283 ++++++++++++++++++++++++++++++++++ src/user_feedback.sh | 15 ++ test.sh | 67 ++++++++ 15 files changed, 920 insertions(+), 2 deletions(-) create mode 100644 .development/docs/Dockerfile create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 docs/modules/Logging.md create mode 100644 docs/modules/Prompts.md create mode 100644 docs/modules/User-Feedback.md create mode 100644 renovate.json create mode 100644 src/logging.sh create mode 100644 src/main.sh create mode 100644 src/prompts.sh create mode 100644 src/user_feedback.sh create mode 100755 test.sh diff --git a/.development/docs/Dockerfile b/.development/docs/Dockerfile new file mode 100644 index 0000000..611fd52 --- /dev/null +++ b/.development/docs/Dockerfile @@ -0,0 +1,17 @@ +FROM alpine:3 +RUN apk add git bash make gawk +RUN mkdir -p /workspace && \ + addgroup -g 1000 dev && \ + adduser -D -u 1000 -G dev dev && \ + chown -R dev:dev /workspace +WORKDIR /opt +# renovate: datasource=github-releases depName=reconquest/shdoc +ENV shdoc_version="v1.1" +RUN git clone --recursive https://github.com/reconquest/shdoc && \ + cd shdoc && \ + git checkout ${shdoc_version} && \ + make install +USER dev +WORKDIR /workspace +COPY --chown=dev:dev ./src /workspace +ENTRYPOINT ["/bin/bash", "-c"] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f11b75 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a9dc6a4 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,32 @@ +fail_fast: true +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-json + - id: check-merge-conflict + - id: check-yaml + - id: detect-private-key + - id: check-symlinks + - id: check-vcs-permalinks + - id: trailing-whitespace + - id: mixed-line-ending + args: + - --fix=lf + - id: check-case-conflict + - id: check-toml + - id: check-xml + - id: fix-byte-order-marker + - id: destroyed-symlinks + + - repo: https://github.com/syntaqx/git-hooks + rev: v0.0.17 + hooks: + - id: shellcheck + additional_dependencies: [] + + - repo: https://github.com/matthorgan/pre-commit-conventional-commits + rev: 20fb9631be1385998138432592d0b6d4dfa38fc9 + hooks: + - id: conventional-commit-check + stages: [commit-msg] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4f4639b --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +SHELL := /bin/bash +.PHONY: help + +help: ## Display this help page + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[33m%-30s\033[0m %s\n", $$1, $$2}' + +lint: ## Lint files with shellcheck + @find src/*.sh -type f -exec "shellcheck" "-x" {} \; + +generate-docs: ## Build documentation using docker container + docker build . -t bash-tui-toolkit/shdoc -f .development/docs/Dockerfile + docker run --rm bash-tui-toolkit/shdoc 'shdoc < logging.sh ' 2>&1 > docs/modules/Logging.md + docker run --rm bash-tui-toolkit/shdoc 'shdoc < prompts.sh ' 2>&1 > docs/modules/Prompts.md + docker run --rm bash-tui-toolkit/shdoc 'shdoc < user_feedback.sh ' 2>&1 > docs/modules/User-Feedback.md diff --git a/README.md b/README.md index a4de86f..8ee9b23 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,27 @@ bash-tui-toolkit === +[![pre-commit](https://img.shields.io/badge/%E2%9A%93%20%20pre--commit-enabled-success)](https://pre-commit.com/) Toolkit to create simple Terminal UIs using plain bash builtins +> :warning: Currently, WIP and not in a stable state for usage + ## Goals + - provide a simple and clear default set of elements to use creating an interactive terminal UI - be clean and minimalistic - zero dependencies to be installed -- parts can be used seperately +- parts can be used separately ## Install + > tbd ## Documentation -> tbd + +For a list of available modules and their documentation please check the [docs/modules](./docs/modules) folder + +## Alternatives + +- [kahkhang/Inquirer.sh](https://github.com/kahkhang/Inquirer.sh) - List, Checkbox and Text Input with more advanced + validation diff --git a/docs/modules/Logging.md b/docs/modules/Logging.md new file mode 100644 index 0000000..44853fe --- /dev/null +++ b/docs/modules/Logging.md @@ -0,0 +1,38 @@ +# Logging + +Provide logging helpers for structured logging + +## Overview + +Parse log level from text representation to level number + +## Index + +* [parse_log_level](#parse_log_level) +* [log](#log) + +### parse_log_level + +Parse log level from text representation to level number + +#### Arguments + +* **$1** (string): Log level to parse + +#### Output on stdout + +* numeric log level + +### log + +Log output on a given level, checks if $LOG_LEVEL, if not set defaults to INFO + +#### Arguments + +* **$1** (number): Numeric log level +* **$2** (string): Message to output + +#### Output on stdout + +* Formatted log message with ANSI color codes + diff --git a/docs/modules/Prompts.md b/docs/modules/Prompts.md new file mode 100644 index 0000000..f208b44 --- /dev/null +++ b/docs/modules/Prompts.md @@ -0,0 +1,124 @@ +# Prompts + +Inquirer.js inspired prompts + +## Overview + +Prompt for text + +## Index + +* [input](#input) +* [confirm](#confirm) +* [list](#list) +* [checkbox](#checkbox) +* [password](#password) +* [editor](#editor) +* [with_validate](#with_validate) +* [validate_present](#validate_present) + +### input + +Prompt for text + +#### Arguments + +* **$1** (string): Phrase for promptint to text + +#### Output on stdout + +* Text as provided by user + +### confirm + +Show confirm dialog for yes/no + +#### Arguments + +* **$1** (string): Phrase for promptint to text + +#### Output on stdout + +* 0 for no, 1 for yes + +### list + +Renders a text based list of options that can be selected by the +user using up, down and enter keys and returns the chosen option. +Inspired by https://unix.stackexchange.com/questions/146570/arrow-key-enter-menu/415155#415155 + +#### Arguments + +* **$1** (string): Phrase for promptint to text +* **$2** (array): List of options (max 256) + +#### Output on stdout + +* selected index (0 for opt1, 1 for opt2 ...) + +### checkbox + +Render a text based list of options, where multiple can be selected by the +user using up, down and enter keys and returns the chosen option. +Inspired by https://unix.stackexchange.com/questions/146570/arrow-key-enter-menu/415155#415155 + +#### Arguments + +* **$1** (string): Phrase for promptint to text +* **$2** (array): List of options (max 256) + +#### Output on stdout + +* selected index (0 for opt1, 1 for opt2 ...) + +### password + +Show password prompt displaying stars for each password character letter typed +it also allows deleting input + +#### Arguments + +* **$1** (string): Phrase for promptint to text + +#### Output on stdout + +* password as written by user + +### editor + +Open default editor + +#### Arguments + +* **$1** (string): Phrase for promptint to text + +#### Output on stdout + +* Text as input by user in input + +### with_validate + +Evaluate prompt command with validation, this prompts the user for input till the validation function +returns with 0 + +#### Arguments + +* **$1** (string): Prompt command to evaluate until validation is successful +* #2 function validation callback (this is called once for exit code and once for status code) + +#### Output on stdout + +* Value collected by evaluating prompt + +### validate_present + +Validate a prompt returned any value + +#### Arguments + +* **$1** (value): to validate + +#### Output on stdout + +* error message for user + diff --git a/docs/modules/User-Feedback.md b/docs/modules/User-Feedback.md new file mode 100644 index 0000000..a953179 --- /dev/null +++ b/docs/modules/User-Feedback.md @@ -0,0 +1,29 @@ +# User-Feedback + +Provides useful colored outputs for user feedback on actions + +## Overview + +Display error message in stderr, prefixed by check emoji + +## Index + +* [show_error](#show_error) +* [show_success](#show_success) + +### show_error + +Display error message in stderr, prefixed by check emoji + +#### Arguments + +* **$1** (string): Error message to display + +### show_success + +Display success message in stderr, prefixed by cross emoji + +#### Arguments + +* **$1** (string): Success message to display + diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..049f64b --- /dev/null +++ b/renovate.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "github>timo-reymann/renovate-config" + ], + "regexManagers": [ + { + "fileMatch": ["Dockerfile$"], + "matchStrings": [ + "datasource=(?.*?) depName=(?.*?)( versioning=(?.*?))?\\sARG .*?_VERSION=(?.*)\\s" + ], + "versioningTemplate": "{{#if versioning}}{{{versioning}}}{{else}}semver{{/if}}" + } + ] +} diff --git a/src/logging.sh b/src/logging.sh new file mode 100644 index 0000000..bc65b3e --- /dev/null +++ b/src/logging.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# @name Logging +# @brief Provide logging helpers for structured logging + +# Log levels +LOG_ERROR=3 +LOG_WARN=2 +LOG_INFO=1 +LOG_DEBUG=0 + +# @description Parse log level from text representation to level number +# +# @arg $1 string Log level to parse +# @stdout numeric log level +parse_log_level() { + local level="$1" + + case "${level}" in + info | INFO) echo $LOG_INFO; ;; + debug | DEBUG) echo $LOG_DEBUG; ;; + warn | WARN) echo $LOG_WARN; ;; + error | ERROR) echo $LOG_ERROR; ;; + *) echo -1; ;; + esac +} + +# @description Log output on a given level, checks if $LOG_LEVEL, if not set defaults to INFO +# @arg $1 number Numeric log level +# @arg $2 string Message to output +# @stdout Formatted log message with ANSI color codes +log() { + local level="$1" + local message="$2" + local color="" + + if [[ $level -lt ${LOG_LEVEL:-$LOG_INFO} ]]; then + return + fi + + case "${level}" in + "$LOG_INFO") + level="INFO" + color='\e[1;36m' + ;; + + "$LOG_DEBUG") + level="DEBUG" + color='\e[1;34m' + ;; + + "$LOG_WARN") + level="WARN" + color='\e[0;33m' + ;; + + "$LOG_ERROR") + level="ERROR" + color='\e[0;31m' + ;; + esac + + echo -e "[${color}$(printf '%-5s' "${level}")\e[0m] \e[1;35m$(date +'%Y-%m-%dT%H:%M:%S')\e[0m ${message}" +} diff --git a/src/main.sh b/src/main.sh new file mode 100644 index 0000000..6da5e93 --- /dev/null +++ b/src/main.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# shellcheck source=src/prompts.sh +source "${BASH_SOURCE%/*}/prompts.sh" +# shellcheck source=src/user_feedback.sh +source "${BASH_SOURCE%/*}/user_feedback.sh" +# shellcheck source=src/logging.sh +source "${BASH_SOURCE%/*}/logging.sh" diff --git a/src/prompts.sh b/src/prompts.sh new file mode 100644 index 0000000..ec9994f --- /dev/null +++ b/src/prompts.sh @@ -0,0 +1,283 @@ +#!/bin/bash +# @name Prompts +# @brief Inquirer.js inspired prompts + +_get_cursor_row() { + local IFS=';' + # shellcheck disable=SC2162,SC2034 + read -sdR -p $'\E[6n' ROW COL; + echo "${ROW#*[}"; +} +_cursor_blink_on() { echo -en "\e[?25h" >&2; } +_cursor_blink_off() { echo -en "\e[?25l" >&2; } +_cursor_to() { echo -en "\e[$1;${2:-1}H" >&2; } + +# key input helper +_key_input() { + read -s -r -N1 key 2>/dev/null >&2 + case $key in + "A") echo "up"; ;; + "B") echo "down"; ;; + " ") echo "space"; ;; + $'\n') echo "enter" ;; + esac +} + +# print new line for empty element in array +_new_line_foreach_item() { for _ in "${1[@]}"; do echo -en "\n" >&2; done } + +# display prompt text without linebreak +_prompt_text() { + echo -en "\e[32m?\e[0m\e[1m ${1}\e[0m " >&2 +} + +# decrement counter $1, considering out of range for $2 +_decrement_selected() { + local selected=$1; + ((selected--)) + if [ "${selected}" -lt 0 ]; then + selected=$(($2 - 1)); + fi + echo $selected +} + +# increment counter $1, considering out of range for $2 +_increment_selected() { + local selected=$1; + ((selected++)); + if [ "${selected}" -ge "${opts_count}" ]; then + selected=0; + fi + echo $selected +} + +# checks if $1 contains element $2 +_contains() { + items=$1 + search=$2 + for item in "${items[@]}"; do + if [ "$item" == "$search" ]; then return 0; fi + done + return 1 +} + +# @description Prompt for text +# @arg $1 string Phrase for promptint to text +# @stdout Text as provided by user +# @stderr Instructions for user +input() { + _prompt_text "$1"; echo -en "\e[36m\c" >&2 + read -r text + echo "${text}" +} + +# @description Show confirm dialog for yes/no +# @arg $1 string Phrase for promptint to text +# @stdout 0 for no, 1 for yes +# @stderr Instructions for user +confirm() { + _prompt_text "$1 (y/N)" + echo -en "\e[36m\c " >&2 + local result="" + echo -n " " >&2 + until [[ "$result" == "y" ]] || [[ "$result" == "N" ]] + do + echo -e "\e[1D\c " >&2 + # shellcheck disable=SC2162 + read -n1 result + done + echo -en "\e[0m" >&2 + + case $result in + y) echo 1; ;; + N) echo 0 ;; + esac + + echo "" >&2 +} + +# @description Renders a text based list of options that can be selected by the +# user using up, down and enter keys and returns the chosen option. +# Inspired by https://unix.stackexchange.com/questions/146570/arrow-key-enter-menu/415155#415155 +# @arg $1 string Phrase for promptint to text +# @arg $2 array List of options (max 256) +# @stdout selected index (0 for opt1, 1 for opt2 ...) +# @stderr Instructions for user +list() { + _prompt_text "$1 " + + local opts=("${@:2}") + local opts_count=$(($# -1)) + _new_line_foreach_item "${opts[@]}" + + # determine current screen position for overwriting the options + local lastrow; lastrow=$(_get_cursor_row) + local startrow; startrow=$((lastrow - opts_count + 1)) + + # ensure cursor and input echoing back on upon a ctrl+c during read -s + trap "_cursor_blink_on; stty echo; exit" 2 + _cursor_blink_off + + local selected=0 + while true; do + # print options by overwriting the last lines + local idx=0 + for opt in "${opts[@]}"; do + _cursor_to $((startrow + idx)) + if [ $idx -eq $selected ]; then + printf "\e[0m\e[36m\u276F\e[0m \e[36m%s\e[0m" "$opt" >&2 + else + printf " %s" "$opt" >&2 + fi + ((idx++)) + done + + # user key control + case $(_key_input) in + enter) break; ;; + up) selected=$(_decrement_selected "${selected}" "${opts_count}"); ;; + down) selected=$(_increment_selected "${selected}" "${opts_count}"); ;; + esac + done + + # cursor position back to normal + _cursor_to "${lastrow}" + _cursor_blink_on + + echo "${selected}" +} + +# @description Render a text based list of options, where multiple can be selected by the +# user using up, down and enter keys and returns the chosen option. +# Inspired by https://unix.stackexchange.com/questions/146570/arrow-key-enter-menu/415155#415155 +# @arg $1 string Phrase for promptint to text +# @arg $2 array List of options (max 256) +# @stdout selected index (0 for opt1, 1 for opt2 ...) +# @stderr Instructions for user +checkbox() { + _prompt_text "$1" + local opts; opts=("${@:2}") + local opts_count; opts_count=$(($# -1)) + _new_line_foreach_item "${opts[@]}" + + # determine current screen position for overwriting the options + local lastrow; lastrow=$(_get_cursor_row) + local startrow; startrow=$((lastrow - opts_count + 1)) + + # ensure cursor and input echoing back on upon a ctrl+c during read -s + trap "_cursor_blink_on; stty echo; exit" 2 + _cursor_blink_off + + local selected=0 + local checked=() + while true; do + # print options by overwriting the last lines + local idx=0 + for opt in "${opts[@]}"; do + _cursor_to $((startrow + idx)) + local icon + if _contains "${checked[*]}" $idx; then + icon=$(echo -en "\u25C9") + else + icon=$(echo -en "\u25EF") + fi + if [ $idx -eq $selected ]; then + printf "%s \e[0m\e[36m\u276F\e[0m \e[36m%-50s\e[0m" "$icon" "$opt" >&2 + else + printf "%s %-50s " "$icon" "$opt" >&2 + fi + ((idx++)) + done + + # user key control + case $(_key_input) in + enter) break;; + space) + if _contains "${checked[*]}" $selected; then + checked=( "${checked[@]/$selected}" ) + else + checked+=("${selected}") + fi + ;; + up) selected=$(_decrement_selected "${selected}" "${opts_count}"); ;; + down) selected=$(_increment_selected "${selected}" "${opts_count}"); ;; + esac + done + + # cursor position back to normal + _cursor_to "${lastrow}" + _cursor_blink_on + + IFS=" " echo "${checked[@]}" +} + +# @description Show password prompt displaying stars for each password character letter typed +# it also allows deleting input +# @arg $1 string Phrase for promptint to text +# @stdout password as written by user +# @stderr Instructions for user +password() { + _prompt_text "$1" + echo -en "\e[36m" >&2 + local password='' + local IFS= + while read -r -s -n1 char; do + # ENTER pressed; output \n and break. + [[ -z "${char}" ]] && { printf '\n' >&2; break; } + # BACKSPACE pressed; remove last character + if [ "${char}" == $'\x7f' ]; then + if [ "${#password}" -gt 0 ]; then + password="${password%?}" + echo -en '\b \b' >&2 + fi + else + password+=$char + echo -en '*' >&2 + fi + done + echo -en "\e[0m" >&2 + echo "${password}" +} + +# @description Open default editor +# @arg $1 string Phrase for promptint to text +# @stdout Text as input by user in input +# @stderr Instructions for user +editor() { + tmpfile=$(mktemp) + _prompt_text "$1" + echo "" >&2 + + "${EDITOR:-vi}" "${tmpfile}" >/dev/tty + echo -en "\e[36m" >&2 + # shellcheck disable=SC2002 + cat "${tmpfile}" | sed -e 's/^/ /' >&2 + echo -en "\e[0m" >&2 + + cat "${tmpfile}" +} + +# @description Evaluate prompt command with validation, this prompts the user for input till the validation function +# returns with 0 +# @arg $1 string Prompt command to evaluate until validation is successful +# @arg #2 function validation callback (this is called once for exit code and once for status code) +# @stdout Value collected by evaluating prompt +# @stderr Instructions for user +with_validate() { + while true; do + local val; val="$(eval "$1")" + if ($2 "$val" >/dev/null); then + echo "$val"; + break; + else + show_error "$($2 "$val")"; + fi + done +} + +# @description Validate a prompt returned any value +# @arg $1 value to validate +# @stdout error message for user +validate_present() { + if [ "$1" != "" ]; then return 0; else echo "Please specify the value"; return 1; fi +} diff --git a/src/user_feedback.sh b/src/user_feedback.sh new file mode 100644 index 0000000..e26d8f3 --- /dev/null +++ b/src/user_feedback.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# @name User-Feedback +# @brief Provides useful colored outputs for user feedback on actions + +# @description Display error message in stderr, prefixed by check emoji +# @arg $1 string Error message to display +show_error() { + echo -e "\e[91;1m\u2718 $1" >&2 +} + +# @description Display success message in stderr, prefixed by cross emoji +# @arg $1 string Success message to display +show_success() { + echo -e "\e[92;1m\u2714 $1" >&2 +} diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..dbd4f36 --- /dev/null +++ b/test.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# +# Basic demo of features +# +source src/main.sh + +# +# UTILS +# +show_error "Something went wrong" +show_success "There we go" + +# +# LOGGING +# +export LOG_LEVEL="$LOG_DEBUG" +log "$LOG_DEBUG" "Debug message" +log "$LOG_INFO" "Info message" +log "$LOG_WARN" "Warn message" +log "$LOG_ERROR" "Error message" + +# +# PROMPTS +# + +options=("one" "two" "three" "four") + +validate_password() { + if [ ${#1} -lt 10 ];then + echo "Password needs to be at least 10 characters" + exit 1 + fi +} +# Password prompt +pass=$(with_validate 'password "Enter random password"' validate_password) + +# Checkbox +checked=$(checkbox "Select one or more items" "${options[@]}") + +# text input with validation +text=$(with_validate 'input "Please enter something and confirm with enter"' validate_present) + +# Select +option=$(list "Select one item" "${options[@]}") + +# Confirm +confirmed=$(confirm "Should it be?") + +# Open editor +editor=$(editor "Please enter something in the editor") + +# Print results +echo " +--- +password: +$pass +input: +$text +select: +$option +checkbox: +$checked +confirm: +$confirmed +editor: +$editor +"