summaryrefslogtreecommitdiff
path: root/src/lib/util/term.sh
blob: 853dccf400297558e4b92733042797aad17a5c3b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
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
}