Skip to content

Instantly share code, notes, and snippets.

@marslo
Last active December 3, 2025 10:31
Show Gist options
  • Select an option

  • Save marslo/3f4b1eae28902394ad3201d1b5ea5537 to your computer and use it in GitHub Desktop.

Select an option

Save marslo/3f4b1eae28902394ad3201d1b5ea5537 to your computer and use it in GitHub Desktop.
Spinner

Tip

for ansicolor, either using \033[XXm directly, or use following c() function as below:

# credit: https://github.com/ppo/bash-colors
# author: @ppo
# shellcheck disable=SC2015,SC2059
c() { [ $# == 0 ] && printf "\e[0m" || printf "$1" | sed 's/\(.\)/\1;/g;s/\([SDIUFNHT]\)/2\1/g;s/\([KRGYBMCW]\)/3\1/g;s/\([krgybmcw]\)/4\1/g;y/SDIUFNHTsdiufnhtKRGYBMCWkrgybmcw/12345789123457890123456701234567/;s/^\(.*\);$/\\e[\1m/g'; }

Braille Patterns Spinner

1-dot

1-dot

local spinner=( '' '' '' '' '' '' '' '' )

4-dots

4-dots without color

local spinner=( '' '' '' '' '' '' '' '' '' )

4-dots with color

local spinner=(
  "$(c Rs)$(c)"
  "$(c Ys)$(c)"
  "$(c Gs)$(c)"
  "$(c Bs)$(c)"
  "$(c Ms)$(c)"
  "$(c Ys)$(c)"
  "$(c Gs)$(c)"
  "$(c Bs)$(c)"
  "$(c Ms)$(c)"
)

7-dots

7-dots without color

local spinner=( '' '' '' '' '' '' '' '' '' )

7-dots with color

local spinner=(
  "$(c Rs)$(c)"
  "$(c Ys)$(c)"
  "$(c Gs)$(c)"
  "$(c Cs)$(c)"
  "$(c Rs)$(c)"
  "$(c Ys)$(c)"
  "$(c Gs)$(c)"
  "$(c Cs)$(c)"
  )

others

spinner-3

local spinner=(
  "∙∙∙∙∙"
  "$(c Ys)$(c)∙∙∙∙"     # yellow
  "$(c Gs)$(c)∙∙∙"     # green
  "∙∙$(c Cs)$(c)∙∙"     # cyan
  "∙∙∙$(c Bs)$(c)"     # blue
  "∙∙∙∙$(c Ms)$(c)"     # magenta
)

spinner-2

local spinner=(
  "$(c Rs)∙∙∙∙∙$(c)"     # red
  "$(c Ys)●∙∙∙∙$(c)"     # yellow
  "$(c Gs)∙●∙∙∙$(c)"     # green
  "$(c Cs)∙∙●∙∙$(c)"     # cyan
  "$(c Bs)∙∙∙●∙$(c)"     # blue
  "$(c Ms)∙∙∙∙●$(c)"     # magenta
)

spinner-1

local spinner=( '∙∙∙∙∙' '●∙∙∙∙' '∙●∙∙∙' '∙∙●∙∙' '∙∙∙●∙' '∙∙∙∙●' )
#!/usr/bin/env bash
declare plfile=''
# credit: https://github.com/ppo/bash-colors
if [[ -f "${HOME}/.local/bin/bash-color.sh" ]]; then
source "${HOME}/.local/bin/bash-color.sh"
else
# or copy & paste the `c()` function from https://github.com/ppo/bash-colors/blob/master/bash-colors.sh#L3
c() { :; }
fi
function die() { echo -e "$(c Ri)ERROR$(c)$(c i): $*.$(c) $(c 0Wdi)exit ...$(c)" >&2; exit 1; }
function cleanfile() { local file="${1:-}"; test -f "${file}" && rm -f -- "${file}"; }
function restoreCursor() { printf "\033[?25h" >&2; }
function cleanupSpinner() { printf '\r\033[K' >&2; restoreCursor; }
function cleanupOnExit() { restoreCursor 2>/dev/null || true; cleanfile "${plfile:-}"; }
# ensure cursor restoration and tempfile removal on any exit
trap cleanupOnExit EXIT
function withSpinner() {
local msg="${1:-}"; shift
local __resultvar="$1"; shift
local spinner=(
"$(c Rs)⣄$(c)"
"$(c Ys)⣆$(c)"
"$(c Gs)⡇$(c)"
"$(c Bs)⠏$(c)"
"$(c Ms)⠋$(c)"
"$(c Ys)⠹$(c)"
"$(c Gs)⢸$(c)"
"$(c Bs)⣰$(c)"
"$(c Ms)⣠$(c)"
)
local frame=0
local output
local cmdPid
local pgid=''
local interrupted=0
local tmpout tmperr
local innerStatus=0 # subshell exit code
# hide cursor and print prefix
printf "\033[?25l" >&2
printf "%s" "${msg:+${msg} }" >&2
# kill subprocess group on job control + Ctrl-C
local hadMonitoring
hadMonitoring="$(set -o | awk '/monitor/ {print $2}')" # on/off
set -m
trap 'interrupted=1; [[ -n "${pgid}" ]] && kill -TERM -- -"${pgid}" 2>/dev/null' INT
tmpout="$(mktemp "/tmp/tmpout.XXXXXX")" || return 1
tmperr="$(mktemp "/tmp/tmperr.XXXXXX")" || { rm -f "${tmpout}"; return 1; }
# subshell, use the exit code of the command as the exit code of the subshell
output="$(
{
local cmdStatus=0
# execute the command in subshell, redirect stdout -> tmpout, stderr -> tmperr
"$@" >"${tmpout}" 2>"${tmperr}" &
cmdPid=$!
pgid="$(ps -o pgid= "${cmdPid}" | tr -d ' ')"
# spinner loop
while kill -0 "${cmdPid}" 2>/dev/null && (( interrupted == 0 )); do
printf "\r\033[K%s%b" "${msg:+${msg} }" "${spinner[frame]}" >&2
(( frame = (frame + 1) % ${#spinner[@]} ))
sleep 0.08
done
if (( interrupted )); then
# if ctrl-c happened, kill the process group, and returns 130
wait "${cmdPid}" 2>/dev/null || true
cmdStatus=130
else
# if command finished: get the real exit code; using close errexit to avoid exiting on non-zero
set +e
wait "${cmdPid}"
cmdStatus=$?
set -e
fi
cat "${tmpout}" # print the command output from temp file, it will be captured by $(...)
exit "${cmdStatus}" # exit code of subshell, so the $? outside is $cmdStatus, not 0
}
)"
# subshell exit code (== $cmdStatus)
innerStatus=$?
[[ "${hadMonitoring}" == "off" ]] && set +m
cleanupSpinner # cursor revert to beginning of line + clear line + show cursor
trap - INT # dismiss the INT trap, to avoid impacting the next commands
# if Ctrl-C happened, just print interrupted message, no stderr shows
if (( innerStatus == 130 )); then
printf "\r\033[K%b✗%b Interrupted!%b\033[K\n" "$(c 0Ri)" "$(c 0Ci)" "$(c)" >&2
else
# not stopped by Ctrl+C AND exit code != 0, print stderr
if (( innerStatus != 0 )) && [[ -s "${tmperr}" ]]; then
# print the stderr content
printf "%b>> exit code : %b%d%b\n" "$(c Wdi)" "$(c 0Mi)" "${innerStatus}" "$(c)" >&2
printf "%b>> stderr : %b%s%b\n" "$(c Wdi)" "$(c 0Mi)" "$(cat "${tmperr}")" "$(c)" >&2
fi
# nothing to be processed for success case
# printf "\r\033[K\033[32m✓\033[0m Done!\033[K\n" >&2
printf "\r" >&2
fi
# cleanup temp files
cleanfile "${tmpout:-}"
cleanfile "${tmperr:-}"
# put the stdout into the variable provided by caller
printf -v "${__resultvar}" '%s' "${output}"
# return the inner command's exit code
return "${innerStatus}"
}
function main() {
plfile="$(mktemp "/tmp/ccm-payload.XXXXXX.json")"
local response=''
local curlExit=128
if withSpinner '' response \
curl -sS --http1.1 https://api.openai.com/v1/chat/completions \
-H "Authorization: Bearer ${OPENAI_API_KEY}" \
-H "Content-Type: application/json" \
....
then
curlExit=0
else
curlExit=$?
fi
# if curl error
if (( curlExit != 0 )); then die "curl failed with exit code: $(c 0Mi)${curlExit}$(c)"; fi
# if error
if jq -e '.error' >/dev/null <<<"${response}"; then
echo "${response}" | jq . >&2
die "OpenAI API returned an error (see above)"
fi
# show response message
echo "${response}" | jq -r '.choices[0].message.content'
}
main "$@"
# vim:tabstop=2:softtabstop=2:shiftwidth=2:expandtab:filetype=sh:
#!/usr/bin/env bash
# credit: https://github.com/ppo/bash-colors
# shellcheck disable=SC2015,SC2059
c() { [ $# == 0 ] && printf "\e[0m" || printf "$1" | sed 's/\(.\)/\1;/g;s/\([SDIUFNHT]\)/2\1/g;s/\([KRGYBMCW]\)/3\1/g;s/\([krgybmcw]\)/4\1/g;y/SDIUFNHTsdiufnhtKRGYBMCWkrgybmcw/12345789123457890123456701234567/;s/^\(.*\);$/\\e[\1m/g'; }
# capture ctrl-c to exit the sub-process
function withSpinner() {
local msg="$1"; shift
local __resultvar="$1"; shift
local spinner=(
"$(c Rs)⣄$(c)"
"$(c Ys)⣆$(c)"
"$(c Gs)⡇$(c)"
"$(c Bs)⠏$(c)"
"$(c Ms)⠋$(c)"
"$(c Ys)⠹$(c)"
"$(c Gs)⢸$(c)"
"$(c Bs)⣰$(c)"
"$(c Ms)⣠$(c)"
)
local frame=0
local output
local cmdPid
local pgid=""
local interrupted=0
# explicit recovery cursor
function restoreCursor() { printf "\033[?25h" >&2; }
# ensure that any exit restores the cursor.
trap 'restoreCursor' EXIT
# hide cursor
printf "\033[?25l" >&2
printf "%s " "${msg}" >&2
set -m
trap 'interrupted=1; [ -n "${pgid}" ] && kill -TERM -- -${pgid} 2>/dev/null' INT
# shellcheck disable=SC2034,SC2030
output="$(
{
"$@" 2>/dev/null &
cmdPid=$!
pgid=$(ps -o pgid= ${cmdPid} | tr -d ' ')
echo "${pgid}" > "${tmpfile}"
while kill -0 "$cmdPid" 2>/dev/null && (( interrupted == 0 )); do
printf "\r\033[K%s %b" "${msg}" "${spinner[frame]}" >&2
((frame = (frame + 1) % ${#spinner[@]}))
sleep 0.1
done
wait "${cmdPid}" 2>/dev/null
}
)"
# \r : beginning of line
# \033[K : clear current position to end of line
if (( interrupted )); then
printf "\r\033[K\033[31m✗\033[0m Interrupted!\033[K\n" >&2
# shellcheck disable=SC2031
[ -n "${pgid}" ] && kill -TERM -- -"${pgid}" 2>/dev/null
else
printf "\r\033[K\033[32m✓\033[0m Done!\033[K\n" >&2
fi
# a separate recovery cursor is no longer required because the exit trap is handled
}
# main function
function main() {
tmpfile=$(mktemp)
trap 'rm -f "${tmpfile}"' EXIT
withSpinner "Loading..." result sleep 5
echo "Exit code: $?"
}
main "$@"
# vim:tabstop=2:softtabstop=2:shiftwidth=2:expandtab:filetype=sh:
#!/usr/bin/env bash
# credit: https://github.com/ppo/bash-colors
# shellcheck disable=SC2015,SC2059
c() { [ $# == 0 ] && printf "\e[0m" || printf "$1" | sed 's/\(.\)/\1;/g;s/\([SDIUFNHT]\)/2\1/g;s/\([KRGYBMCW]\)/3\1/g;s/\([krgybmcw]\)/4\1/g;y/SDIUFNHTsdiufnhtKRGYBMCWkrgybmcw/12345789123457890123456701234567/;s/^\(.*\);$/\\e[\1m/g'; }
# capture ctrl-c to exit the sub-process
# return the sub-process stdout ( to external variable )
function withSpinner() {
local msg="$1"; shift
local __resultvar="$1"; shift
local spinner=(
"$(c Rs)⣾$(c)"
"$(c Ys)⣽$(c)"
"$(c Gs)⣻$(c)"
"$(c Cs)⢿$(c)"
"$(c Rs)⡿$(c)"
"$(c Ys)⣟$(c)"
"$(c Gs)⣯$(c)"
"$(c Cs)⣷$(c)"
)
local frame=0
local output
local cmdPid
local pgid=''
local interrupted=0
# define the cursor recovery function
restoreCursor() { printf "\033[?25h" >&2; }
# make sure that any exit restores the cursor
trap 'restoreCursor' EXIT
# hide cursor
printf "\033[?25l" >&2
printf "%s " "$msg" >&2
set -m
trap 'interrupted=1; [ -n "$pgid" ] && kill -TERM -- -$pgid 2>/dev/null' INT
# use file descriptor to capture output
local tmpout
tmpout=$(mktemp)
exec 3<> "${tmpout}"
# shellcheck disable=SC2031,SC2030
output="$(
{
# execute command and redirect output to file descriptor 3
"$@" >&3 2>/dev/null &
cmdPid=$!
pgid=$(ps -o pgid= "$cmdPid" | tr -d ' ')
# update the spinner while the command is running
while kill -0 "$cmdPid" 2>/dev/null && (( interrupted == 0 )); do
printf "\r\033[K%s %b" "${msg}" "${spinner[frame]}" >&2
((frame = (frame + 1) % ${#spinner[@]}))
sleep 0.08
done
wait "$cmdPid" 2>/dev/null
# show the captured content
cat "${tmpout}"
}
)"
# clean the temporary file
exec 3>&-
rm -f "${tmpout}"
# \r : beginning of line
# \033[K : clear current position to end of line
# shellcheck disable=SC2031
if (( interrupted )); then
printf "\r\033[K\033[31m✗\033[0m Interrupted!\033[K\n" >&2
[ -n "${pgid}" ] && kill -TERM -- -"${pgid}" 2>/dev/null
else
# or using `printf "\r" >&2` directly without sub-progress status output
printf "\r\033[K\033[32m✓\033[0m Done!\033[K\n" >&2
fi
# assign the result to an external variable
printf -v "$__resultvar" "%s" "$output"
}
function main() {
# shellcheck disable=SC2155
local tmpfile=$(mktemp)
trap 'rm -f "${tmpfile}"' EXIT
local response
withSpinner "Loading..." response \
curl -s https://<API> ...
# check curl output
echo "${response}"
}
main "$@"
# vim:tabstop=2:softtabstop=2:shiftwidth=2:expandtab:filetype=sh:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment