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>
This commit is contained in:
Levente Polyak 2024-01-18 02:39:34 +01:00
parent 66e83c950c
commit fedfc80ca1
No known key found for this signature in database
GPG Key ID: FC1B547C8D8172C8
2 changed files with 184 additions and 1 deletions

View File

@ -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}"
}

182
src/lib/util/term.sh Normal file
View File

@ -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
}