#!/bin/bash # bash spinners - functions and demo # Set of functions to create, start and stop a spinner as background task. # Version: 2.0.2 -- Last update: Tue Jan 20 08:50:44 2026 # (C) 2023-2026 F-Hauri.ch - http://www.f-hauri.ch # This version use collection of themes from a JSOM file. The idea and the # original JSON was forked from https://github.com/sindresorhus/cli-spinners # Extended by some growing snake themes (some require two lines!) # Should be sourced! But run as a script, they will present a short fzf menu. # This script depend on `coreutils`, `sed`, `fzf` and `jq` packages! SRCDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" export spinnerJson="$SRCDIR/spinners.json" # --- Minimal code for simple backgrounded spinner without json begin here! --- declare shapesInterval=.08 declare spinnerShapes=( ⢎\ ⠎⠁ ⠊⠑ ⠈⠱ \ ⡱ ⢀⡰ ⢄⡠ ⢆⡀ ) declare -i doSpinner=0 # File descriptor to address spinner as background task declare -i spinnerFD # File descriptor for spinner output, in order to not if ! [[ -t $spinnerFD ]]; then # use any of STDOUT or STDERR exec {spinnerFD}>&1 export spinnerFD fi spinner() { # spinner as job. ( Use global spinnerShapes and shapesInterval ) local -i pnt while ! read -rsn 1 -t "$shapesInterval" _; do # while no input # Save cursor position, render frame, restore cursor position printf >&$spinnerFD '\e7%b\e8' \ "${spinnerShapes[pnt++%${#spinnerShapes[@]}]}" done } startSpinner() { # startSpinner # no arguments printf '\e[?25l' >&$spinnerFD # Cursor invisible exec {doSpinner}> >(spinner) # Run spinner with dedicated FD to stop } stopSpinner() { # stopSpinner # no arguments if (( doSpinner )) && echo >&"$doSpinner"; then # Echo newline in FD exec {doSpinner}>&- # Close doSpinner FD doSpinner=0 printf '\e[?25h\n' >&$spinnerFD # Cursor visible fi } # --- Minimal code for simple backgrounded spinner without json end here! --- spinnerShapeFromJSON() { # spinner [interval] local -i pnt { # Run jq once, retrieving interval and frames read -r shapesInterval mapfile -t spinnerShapes } < <(jq -r ".$1"' | "00\(.interval)" , .frames[]' <"$spinnerJson") shapesInterval=00${2:-$shapesInterval} # Convert milliseconds into seconds printf -v shapesInterval %.3f "${shapesInterval::-3}.${shapesInterval: -3}" } spinnerFramesList() { jq -r 'to_entries|.[].key' < "$spinnerJson" } testSpinner() { # testSpinner [loop] [userKeyVarname] [interval] local spinnerTheme=$1 spinnerLoop=${2:-3.5} userKey=${3:-_} toSleep local -i themeInterval themeFrames read -r themeInterval themeFrames twoLines toSleep < <( jq -r <"$spinnerJson" ".$spinnerTheme | \"\(.interval) \(.frames|length) \( .frames[0] | test(\"\\u001b\\\\[A\")) \( (.frames|length)*(3+${4:-.interval})*.001*$spinnerLoop )\" " ) (( themeInterval * themeFrames )) || return [[ $twoLines == true ]] && printf '\n' # Scroll a 2nd line if needed printf '%-20s %4d frames, interval %4dms: ' \ "$spinnerTheme" $themeFrames $themeInterval spinnerShapeFromJSON "$spinnerTheme" "${4:-$themeInterval}" startSpinner # shellcheck disable=SC2229 # $userKey hold a variable name or _ as garbage read -rsn 1 -t "$toSleep" "$userKey" stopSpinner } spinnerThemeDetail() { # spinnerThemeDetail # Intented to by used by fzf ( echo -n "Theme: '$1', " jq -r <"$spinnerJson" "\"\( .$1.frames | length ) frames, interval: \( .$1.interval)ms, \( .$1.frames[0] | if test( \"\\u001b\\\\[A\" ) then \"two lines\" else \"one line\" end )\n\n\n\n\(.$1.frames|join(\"\n\"))\"" ) | sed 's/\o33/\\E/g' | paste - - - - } export -f spinnerThemeDetail spinnerMenu() { # spinnerMenu [loop] testSpinner "$( fzf --preview "spinnerThemeDetail '{}'" --preview-window=80% \ < <(spinnerFramesList) )" "${1:-5.5}" } showAllSpinners() { # showAllSpinners [loop] local theme uKey for theme in $(spinnerFramesList); do testSpinner "$theme" "${1:-3.5}" uKey [[ ${uKey,} == q ]] && break done } complete -W "$(spinnerFramesList)" spinnerShapeFromJSON \ testSpinner spinnerThemeDetail # Exit gracefully here, if sourced [[ $0 = "${BASH_SOURCE[0]}" ]] || { true; return 0; } Quit() { exit 0 ;} while :; do uCmd="$(fzf --preview-window=80% --preview "jq -r < '$spinnerJson' \ '.[]| if has(\"comment\") then . else empty end|.comment' | fold -sw 54" <<-eoMenu show All Spinners spinner Menu Quit eoMenu )" if [[ $uCmd ]]; then "${uCmd// }" else Quit fi done echo