summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLevente Polyak <anthraxx@archlinux.org>2024-01-18 02:39:34 +0100
committerLevente Polyak <anthraxx@archlinux.org>2024-01-22 19:44:51 +0100
commitfedfc80ca15a196d565b9f5dc5159be594f74da3 (patch)
tree67a030a88785518fca9844e9823feebbf984a880 /src
parent66e83c950cfa1c51820f04130abfacaf7c6b4c4c (diff)
downloaddevtools-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>
Diffstat (limited to 'src')
-rw-r--r--src/lib/common.sh3
-rw-r--r--src/lib/util/term.sh182
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
+}