diff options
author | Levente Polyak <anthraxx@archlinux.org> | 2024-01-18 02:39:34 +0100 |
---|---|---|
committer | Levente Polyak <anthraxx@archlinux.org> | 2024-01-22 19:44:51 +0100 |
commit | fedfc80ca15a196d565b9f5dc5159be594f74da3 (patch) | |
tree | 67a030a88785518fca9844e9823feebbf984a880 | |
parent | 66e83c950cfa1c51820f04130abfacaf7c6b4c4c (diff) | |
download | devtools-fedfc80ca15a196d565b9f5dc5159be594f74da3.tar.xz |
feat(term): add terminal utils to handle a dynamic spinner
The spinner uses a status file that can be used to dynamically update
the message. The spinner itself buffers the output in a frame buffer
variable before flushing a frame in one go.
Signed-off-by: Levente Polyak <anthraxx@archlinux.org>
-rw-r--r-- | src/lib/common.sh | 3 | ||||
-rw-r--r-- | src/lib/util/term.sh | 182 |
2 files changed, 184 insertions, 1 deletions
diff --git a/src/lib/common.sh b/src/lib/common.sh index 9d5622e..63f43f1 100644 --- a/src/lib/common.sh +++ b/src/lib/common.sh @@ -13,7 +13,7 @@ set +u +o posix $DEVTOOLS_INCLUDE_COMMON_SH # Avoid any encoding problems -export LANG=C +export LANG=C.UTF-8 # Set buildtool properties export BUILDTOOL=devtools @@ -108,6 +108,7 @@ cleanup() { if [[ -n ${WORKDIR:-} ]] && $_setup_workdir; then rm -rf "$WORKDIR" fi + tput cnorm >&2 exit "${1:-0}" } diff --git a/src/lib/util/term.sh b/src/lib/util/term.sh new file mode 100644 index 0000000..853dccf --- /dev/null +++ b/src/lib/util/term.sh @@ -0,0 +1,182 @@ +#!/hint/bash +# +# SPDX-License-Identifier: GPL-3.0-or-later + +[[ -z ${DEVTOOLS_INCLUDE_UTIL_TERM_SH:-} ]] || return 0 +DEVTOOLS_INCLUDE_UTIL_TERM_SH=1 + +set -eo pipefail + + +readonly PKGCTL_TERM_SPINNER_DOTS=Dots +export PKGCTL_TERM_SPINNER_DOTS +readonly PKGCTL_TERM_SPINNER_DOTS12=Dots12 +export PKGCTL_TERM_SPINNER_DOTS12 +readonly PKGCTL_TERM_SPINNER_LINE=Line +export PKGCTL_TERM_SPINNER_LINE +readonly PKGCTL_TERM_SPINNER_SIMPLE_DOTS_SCROLLING=SimpleDotsScrolling +export PKGCTL_TERM_SPINNER_SIMPLE_DOTS_SCROLLING +readonly PKGCTL_TERM_SPINNER_TRIANGLE=Triangle +export PKGCTL_TERM_SPINNER_TRIANGLE +readonly PKGCTL_TERM_SPINNER_RANDOM=Random +export PKGCTL_TERM_SPINNER_RANDOM + +readonly PKGCTL_TERM_SPINNER_TYPES=( + "${PKGCTL_TERM_SPINNER_DOTS}" + "${PKGCTL_TERM_SPINNER_DOTS12}" + "${PKGCTL_TERM_SPINNER_LINE}" + "${PKGCTL_TERM_SPINNER_SIMPLE_DOTS_SCROLLING}" + "${PKGCTL_TERM_SPINNER_TRIANGLE}" +) +export PKGCTL_TERM_SPINNER_TYPES + + +term_cursor_hide() { + tput civis >&2 +} + +term_cursor_show() { + tput cnorm >&2 +} + +term_cursor_up() { + tput cuu1 +} + +term_carriage_return() { + tput cr +} + +term_erase_line() { + tput el +} + +term_erase_lines() { + local lines=$1 + + local cursor_up erase_line + cursor_up=$(term_cursor_up) + erase_line="$(term_carriage_return)$(term_erase_line)" + + local prefix='' + for _ in $(seq 1 "${lines}"); do + printf '%s' "${prefix}${erase_line}" + prefix="${cursor_up}" + done +} + +_pkgctl_spinner_type=${PKGCTL_TERM_SPINNER_RANDOM} +term_spinner_set_type() { + _pkgctl_spinner_type=$1 +} + +# takes a status directory that can be used to dynamically update the spinner +# by writing to the `status` file inside that directory atomically. +# replace the placeholder %spinner% with the currently configured spinner type +term_spinner_start() { + local status_dir=$1 + local parent_pid=$$ + ( + local spinner_type=${_pkgctl_spinner_type} + local spinner_offset=0 + local frame_buffer='' + local spinner status_message line + + local status_file="${status_dir}/status" + local next_file="${status_dir}/next" + local drawn_file="${status_dir}/drawn" + + # assign random spinner type + if [[ ${spinner_type} == "${PKGCTL_TERM_SPINNER_RANDOM}" ]]; then + spinner_type=${PKGCTL_TERM_SPINNER_TYPES[$((RANDOM % ${#PKGCTL_TERM_SPINNER_TYPES[@]}))]} + fi + + # select spinner based on the named type + case "${spinner_type}" in + "${PKGCTL_TERM_SPINNER_DOTS}") + spinner=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏") + update_interval=0.08 + ;; + "${PKGCTL_TERM_SPINNER_DOTS12}") + spinner=("⢀⠀" "⡀⠀" "⠄⠀" "⢂⠀" "⡂⠀" "⠅⠀" "⢃⠀" "⡃⠀" "⠍⠀" "⢋⠀" "⡋⠀" "⠍⠁" "⢋⠁" "⡋⠁" "⠍⠉" "⠋⠉" "⠋⠉" "⠉⠙" "⠉⠙" "⠉⠩" "⠈⢙" "⠈⡙" "⢈⠩" "⡀⢙" "⠄⡙" "⢂⠩" "⡂⢘" "⠅⡘" "⢃⠨" "⡃⢐" "⠍⡐" "⢋⠠" "⡋⢀" "⠍⡁" "⢋⠁" "⡋⠁" "⠍⠉" "⠋⠉" "⠋⠉" "⠉⠙" "⠉⠙" "⠉⠩" "⠈⢙" "⠈⡙" "⠈⠩" "⠀⢙" "⠀⡙" "⠀⠩" "⠀⢘" "⠀⡘" "⠀⠨" "⠀⢐" "⠀⡐" "⠀⠠" "⠀⢀" "⠀⡀") + update_interval=0.08 + ;; + "${PKGCTL_TERM_SPINNER_LINE}") + spinner=("⎯" "\\" "|" "/") + update_interval=0.13 + ;; + "${PKGCTL_TERM_SPINNER_SIMPLE_DOTS_SCROLLING}") + spinner=(". " ".. " "..." " .." " ." " ") + update_interval=0.2 + ;; + "${PKGCTL_TERM_SPINNER_TRIANGLE}") + spinner=("◢" "◣" "◤" "◥") + update_interval=0.05 + ;; + esac + + # hide the cursor while spinning + term_cursor_hide + + # run the spinner as long as the parent process didn't terminate + while ps -p "${parent_pid}" &>/dev/null; do + # cache the new status template if it exists + if mv "${status_file}" "${next_file}" &>/dev/null; then + status_message="$(cat "$next_file")" + elif [[ -z "${status_message}" ]]; then + # wait until we either have a new or cached status + sleep 0.05 + fi + + # fill the frame buffer with the current status + local prefix='' + while IFS= read -r line; do + # replace spinner placeholder + line=${line//%spinner%/${spinner[spinner_offset%${#spinner[@]}]}} + + # append the current line to the frame buffer + frame_buffer+="${prefix}${line}" + prefix=$'\n' + done <<< "${status_message}" + + # print current frame buffer + echo -n "${frame_buffer}" >&2 + mv "${next_file}" "${drawn_file}" &>/dev/null ||: + + # setup next frame buffer to clear current content + frame_buffer=$(term_erase_lines "$(awk 'END {print NR}' <<< "${status_message}")") + + # advance the spinner animation offset + (( ++spinner_offset )) + + # sleep for the spinner update interval + sleep "${update_interval}" + done + )& + _pkgctl_spinner_pid=$! + disown +} + +term_spinner_stop() { + local status_dir=$1 + local frame_buffer status_file + + # kill the spinner process + if ! kill "${_pkgctl_spinner_pid}" > /dev/null 2>&1; then + return 1 + fi + unset _pkgctl_spinner_pid + + # acquire last drawn status + status_file="${status_dir}/drawn" + if [[ ! -f ${status_file} ]]; then + return 0 + fi + + # clear terminal based on last status line + frame_buffer=$(term_erase_lines "$(awk 'END {print NR}' < "${status_file}")") + echo -n "${frame_buffer}" >&2 + + # show the cursor after stopping the spinner + term_cursor_show +} |