Adding Custom Animated Spinners to Bash: A Complete Step-by-Step Guide (with Installer, Hooks, and Advanced Features)

Add animated, colorful spinners to Bash with preexec hooks. See HHGTTG quotes, towels, and sci‑fi emojis while commands run. Includes installer, scripts, and fully customizable spinner styles for fun, responsive terminals.

Adding Custom Animated Spinners to Bash: A Complete Step-by-Step Guide (with Installer, Hooks, and Advanced Features)
Photo by madatx / Unsplash

Full Step‑By‑Step Manual Adding Custom Animated Spinners to Bash

Everything you need → concepts → scripts → installer → integration → extensions

1. Introduction

A spinner is a tiny animation that runs while a command is doing work.
In Bash the animation is nothing more than a loop that repeatedly prints a
different character (or emoji) on the same line, sleeps a short time, and then
updates the line again.

Why is this useful?

  • Visual feedback – you know something is happening, even for long make or git jobs.
  • Fun – a little emoji‑style animation makes the terminal feel alive.
  • Modular – you can swap styles, colours, and speeds in a single place.

Bash does not have native “pre‑command”/“post‑command” hooks like Zsh, so we need a small helper library called bash‑preexec. It creates two functions you can implement:

  • preexec() – runs right before each command starts.
  • precmd() – runs right after the prompt is displayed (i.e. after the command finishes).

Our spinner will be started from preexec() and killed from precmd().

2. How Bash Hooking Works

2.1 What Bash normally offers

Feature Bash Zsh
preexec hook ❌ (no built‑in)
precmd hook ❌ (no built‑in)
PROMPT_COMMAND ✅ (runs before each prompt)

Because Bash lacks preexec, the bash‑preexec script works around this limitation by:

  1. Setting a DEBUG trap that runs just before a command is executed.
  2. Overriding PROMPT_COMMAND to run a post‑prompt function.

The library is tiny (≈ 250 lines) and safe for most environments.

2.2 Installing the library

The installer we will write automatically downloads the latest version from GitHub
if it isn’t already present:

curl -sSL https://raw.githubusercontent.com/rcaloras/bash-preexec/master/bash-preexec.sh \
     -o "$INSTALL_DIR/bash-preexec.sh"

After that we just source it and the two hook functions become available.

3. How a Spinner Works Internally

Below is a minimal “bare‑bones” spinner (no colours, no fancy emojis).
Understanding it will make the later, more feature‑rich version easier to follow.

# 1️⃣  Define the animation frames (any characters you like)
frames=( '-' '\' '|' '/' )    # classic rotating bar

# 2️⃣  Function that draws the spinner while $1 (a PID) is alive
spinner() {
    local pid=$1               # PID of the command we are watching
    local i=0                  # index into frames array
    while kill -0 "$pid" 2>/dev/null; do   # “kill -0” checks if PID exists
        printf "\r[%s] Working..." "${frames[i]}"
        i=$(( (i + 1) % ${#frames[@]} ))   # wrap around
        sleep 0.1                         # <-- speed (seconds per frame)
    done
    printf "\r✔️  Done!%*s\n" "$(tput cols)" ""   # clear line, show check‑mark
}
  • kill -0 $pid – does not send a signal; it merely asks “does this PID exist?” – perfect for a poll loop.
  • \r – carriage return; brings the cursor back to the start of the line without a newline, allowing us to overwrite the same line.
  • sleep 0.1 – controls the animation speed. Smaller numbers → faster spin, larger numbers → slower spin.

The spinner must run in the background so that the main command can continue:

spinner "$CMD_PID" &    # start spinner in background
SPINNER_PID=$!          # $! is the PID of the last background job

When the command finishes we kill the spinner:

kill "$SPINNER_PID" 2>/dev/null || true   # ignore “no such process” errors
unset SPINNER_PID

4. Directory Structure for a Clean Installation

All files live under a single hidden directory in the user’s home folder:

~/.hhgttg/
   ├── hhgttg.sh          # main spinner + hook functions
   ├── towel.txt          # optional “keep your towel” lore (fun)
   ├── bash-preexec.sh    # downloaded automatically if missing
   └── config.sh          # optional user‑config (style, speed, colours)

Why this layout?

  • Isolation – nothing clutters $HOME or /etc.
  • Version control – you can git init inside ~/.hhgttg and track changes.
  • Easy uninstall – just delete the folder and the lines we add to ~/.bashrc.

5. Main Spinner Script (hhgttg.sh)

Copy the following into ~/.hhgttg/hhgttg.sh.
It contains:

  • Randomized spinner sets (emoji, Braille, sci‑fi, etc.)
  • Colour handling (ANSI escape codes)
  • Adjustable speed via an environment variable (HHGTTG_SPINNER_SPEED)
  • Optional HHGTTG quote + towel output after each command
  • Integration with bash‑preexec (preexec/precmd functions)
#!/usr/bin/env bash
#====================================================================
# HHGTTG (The Hitchhiker's Guide to the Galaxy) – Bash Spinner
#====================================================================
# This file is meant to be sourced from ~/.bashrc.
# It defines:
#   * preexec   – start a spinner in the background
#   * precmd    – kill the spinner and print a random quote/towel
#   * spinner() – the animation engine
#====================================================================

# ------------------------------------------------------------------
# 1️⃣  Helper: random quote -------------------------------------------------
_hhg_quote() {
    local quotes=(
        "Don't Panic."
        "Time is an illusion. Lunchtime doubly so."
        "The answer is 42."
        "The ships hung in the air in much the same way bricks don't."
        "We apologize for the inconvenience."
        "Mostly harmless."
        "Life... is like a grapefruit."
        "So long, and thanks for all the fish."
    )
    # Pick one at random
    echo "${quotes[$RANDOM % ${#quotes[@]}]}"
}

# ------------------------------------------------------------------
# 2️⃣  Helper: towel (optional lore) ------------------------------------
_hhg_towel() {
    # If the file does not exist, just skip output
    [[ -f "$HOME/.hhgttg/towel.txt" ]] || return
    # Prefix each line with a small bullet for visual separation
    sed -e 's/^/🔹 /' "$HOME/.hhgttg/towel.txt"
}

# ------------------------------------------------------------------
# 3️⃣  Helper: pick a random spinner set --------------------------------
_hhg_spinners() {
    # You can add more sets here; each set is a space‑separated string.
    case $((RANDOM % 7)) in
        0) echo "🌑 🌒 🌓 🌔 🌕 🌖 🌗 🌘" ;;          # Moon phases
        1) echo "◐ ◓ ◑ ◒" ;;                     # Classic circle quadrants
        2) echo "⠁ ⠂ ⠄ ⡀ ⢀ ⠠ ⠐ ⠈" ;;           # Braille pattern
        3) echo "🛸 👽 ⭐ 💫" ;;                  # Sci‑fi emojis
        4) echo "🔄 🔃 🔁" ;;                     # Arrow loop
        5) echo "⏳ ⏱️ ⏲️" ;;                     # Hourglass set
        6) echo "🐍 🐉 🐲" ;;                     # Mythical dragons (just for fun)
    esac
}

# ------------------------------------------------------------------
# 4️⃣  Core spinner function ---------------------------------------------
spinner() {
    local pid=$1                # PID of the command we watch
    local speed="${HHGTTG_SPINNER_SPEED:-0.12}"   # seconds per frame, can be overridden
    local frames=($( _hhg_spinners ))            # turn the string into an array
    local i=0
    local colour="\e[33m"        # yellow (you can change or make it configurable)

    printf "%b" "$colour"
    while kill -0 "$pid" 2>/dev/null; do
        printf "\r[%s] Working..." "${frames[i]}"
        i=$(( (i + 1) % ${#frames[@]} ))
        sleep "$speed"
    done
    # When the loop exits the command is finished
    printf "\r\e[32m✔️  Done!%*s\e[0m\n" "$(tput cols)" ""
}
# ------------------------------------------------------------------
# 5️⃣  Hook: preexec → start spinner ---------------------------------------
preexec() {
    # $BASH_COMMAND contains the command line that is about to be executed.
    # $$ is the PID of the current shell – the spinner will watch **that**
    # PID because the command runs as a *child* of the shell.
    (spinner "$$") &
    SPINNER_PID=$!
}
# ------------------------------------------------------------------
# 6️⃣  Hook: precmd → stop spinner, show quote/towel ----------------------
precmd() {
    # 1️⃣  Stop the background spinner (if any)
    if [[ -n "$SPINNER_PID" ]]; then
        kill "$SPINNER_PID" 2>/dev/null || true
        unset SPINNER_PID
    fi

    # 2️⃣  Print a colourful quote on a new line
    echo -e "\n\e[36m$(_hhg_quote)\e[0m"

    # 3️⃣  Optionally print the towel text (grey colour)
    if [[ -f "$HOME/.hhgttg/towel.txt" ]]; then
        echo -e "\e[90m$(_hhg_towel)\e[0m"
    fi
}
# ------------------------------------------------------------------
# 7️⃣  Export the hook functions for bash‑preexec to see ------------------
export -f preexec precmd spinner
# --------------------------------------------------------------------

What the script does, line‑by‑line

Line / Section Why it matters
#!/usr/bin/env bash Makes the file runnable if you ever execute it directly (not required for source).
export -f … bash-preexec runs the functions in a sub‑shell; exporting makes them visible there.
HHGTTG_SPINNER_SPEED env var Allows you to change the animation speed on the fly, e.g. export HHGTTG_SPINNER_SPEED=0.05.
printf "\e[33m" Sets yellow colour for the spinner; reset later with \e[0m.
kill -0 "$pid" Non‑destructive way to check if the watched PID is still alive.
preexec/precmd Hook functions that are automatically called by the bash‑preexec library.
tput cols Returns terminal width; we pad the “Done!” line so the old spinner chars disappear completely.

8. One‑File Installer Script (install_hhgttg.sh)

Save the following as install_hhgttg.sh (anywhere you like, e.g. your Downloads folder).
Running it will:

  1. Create ~/.hhgttg (the install directory).
  2. Drop towel.txt (the fun lore file).
  3. Drop hhgttg.sh (the spinner script).
  4. Download bash-preexec.sh if not already present.
  5. Append the required source lines to ~/.bashrc (only once).
  6. Print a friendly “installation complete” message.
#!/usr/bin/env bash
#====================================================================
# Installer for the HHGTTG Bash spinner + bash‑preexec integration
#====================================================================
set -euo pipefail   # fail fast, treat unset vars as errors

# --------------------------------------------------------------------
# 1️⃣  Define where everything will live
INSTALL_DIR="$HOME/.hhgttg"
BASHRC="$HOME/.bashrc"
PREEXEC_URL="https://raw.githubusercontent.com/rcaloras/bash-preexec/master/bash-preexec.sh"

# --------------------------------------------------------------------
# 2️⃣  Create directory
mkdir -p "$INSTALL_DIR"

# --------------------------------------------------------------------
# 3️⃣  Install the optional towel text
cat > "$INSTALL_DIR/towel.txt" <<'EOF'
Always know where your towel is.
EOF

# --------------------------------------------------------------------
# 4️⃣  Install the main spinner script (hhgttg.sh)
cat > "$INSTALL_DIR/hhgttg.sh" <<'EOF'
# --------------------------------------------------------------------
# HHGTTG Spinner – copied verbatim from the manual (section 5)
# --------------------------------------------------------------------
#!/usr/bin/env bash
_hhg_quote() {
    local quotes=(
        "Don't Panic."
        "Time is an illusion. Lunchtime doubly so."
        "The answer is 42."
        "The ships hung in the air in much the same way bricks don't."
        "We apologize for the inconvenience."
        "Mostly harmless."
        "Life... is like a grapefruit."
        "So long, and thanks for all the fish."
    )
    echo "${quotes[$RANDOM % ${#quotes[@]}]}"
}
_hhg_towel() {
    [[ -f "$HOME/.hhgttg/towel.txt" ]] || return
    sed -e 's/^/🔹 /' "$HOME/.hhgttg/towel.txt"
}
_hhg_spinners() {
    case $((RANDOM % 7)) in
        0) echo "🌑 🌒 🌓 🌔 🌕 🌖 🌗 🌘" ;;
        1) echo "◐ ◓ ◑ ◒" ;;
        2) echo "⠁ ⠂ ⠄ ⡀ ⢀ ⠠ ⠐ ⠈" ;;
        3) echo "🛸 👽 ⭐ 💫" ;;
        4) echo "🔄 🔃 🔁" ;;
        5) echo "⏳ ⏱️ ⏲️" ;;
        6) echo "🐍 🐉 🐲" ;;
    esac
}
spinner() {
    local pid=$1
    local speed="${HHGTTG_SPINNER_SPEED:-0.12}"
    local frames=($( _hhg_spinners ))
    local i=0
    local colour="\e[33m"
    printf "%b" "$colour"
    while kill -0 "$pid" 2>/dev/null; do
        printf "\r[%s] Working..." "${frames[i]}"
        i=$(( (i + 1) % ${#frames[@]} ))
        sleep "$speed"
    done
    printf "\r\e[32m✔️  Done!%*s\e[0m\n" "$(tput cols)" ""
}
preexec() {
    (spinner "$$") &
    SPINNER_PID=$!
}
precmd() {
    if [[ -n "$SPINNER_PID" ]]; then
        kill "$SPINNER_PID" 2>/dev/null || true
        unset SPINNER_PID
    fi
    echo -e "\n\e[36m$(_hhg_quote)\e[0m"
    if [[ -f "$HOME/.hhgttg/towel.txt" ]]; then
        echo -e "\e[90m$(_hhg_towel)\e[0m"
    fi
}
export -f preexec precmd spinner
EOF

# --------------------------------------------------------------------
# 5️⃣  Download bash‑preexec (if we don't already have it system‑wide)
if [[ -f /etc/bash-preexec.sh ]]; then
    PREEXEC_SOURCE="/etc/bash-preexec.sh"
else
    PREEXEC_SOURCE="$INSTALL_DIR/bash-preexec.sh"
    if [[ ! -f "$PREEXEC_SOURCE" ]]; then
        echo "Downloading bash-preexec..."
        curl -fsSL "$PREEXEC_URL" -o "$PREEXEC_SOURCE"
        echo "bash-preexec downloaded to $PREEXEC_SOURCE"
    fi
fi

# --------------------------------------------------------------------
# 6️⃣  Append source lines to ~/.bashrc (only once)
if ! grep -Fxq "# HHGTTG PROMPT + SPINNER" "$BASHRC"; then
    cat >> "$BASHRC" <<EOF

# ------------------------------------------------------------
# HHGTTG PROMPT + SPINNER
# ------------------------------------------------------------
# Load bash‑preexec (provides preexec()/precmd() hooks)
source "$PREEXEC_SOURCE"

# Load the HHGTTG spinner functions
source "$INSTALL_DIR/hhgttg.sh"
EOF
    echo "Added source lines to $BASHRC"
else
    echo "Source lines already present in $BASHRC – skipping."
fi

# --------------------------------------------------------------------
# 7️⃣  Final instructions
echo "✨ HHGTTG spinner installed successfully!"
echo "To start using it, either:"
echo "  • Open a new terminal, or"
echo "  • Run: source \"$BASHRC\""
echo ""
echo "You can customise the speed by adding, for example:"
echo "  export HHGTTG_SPINNER_SPEED=0.05   # faster"
echo "to your ~/.bashrc before the HHGTTG block."

How to run the installer

chmod +x install_hhgttg.sh   # make it executable (once)
./install_hhgttg.sh          # run it

The script is idempotent: running it again will not duplicate entries
or re‑download files unnecessarily.

9. Verify Everything Works

  1. Try a longer command (e.g., find . -type f | wc -l) and notice the spinner stays visible the whole time.
  2. Force a specific style (optional). Edit ~/.hhgttg/hhgttg.sh and replace the case in _hhg_spinners() with a single line, e.g.:
echo "⏳ ⏱️ ⏲️"

Save, reload ~/.bashrc, and enjoy the new animation.

Change the speed on the fly:

export HHGTTG_SPINNER_SPEED=0.05   # faster
sleep 2

The animation should now be noticeably quicker.

Run a command that takes a couple of seconds, e.g.:

sleep 3

You should see something like:

[🌑] Working... (spins through the chosen set)
✔️  Done!
Don't Panic.
🔹 Always know where your towel is.

Reload your shell (or open a new terminal):

source ~/.bashrc

10. Adding Your Own Custom Spinner

  1. Locate the function _hhg_spinners().

Save and reload:

source ~/.bashrc

Increase the modulo range in the case line:

case $((RANDOM % 8)) in   # now 0‑7 inclusive

Add a new case branch, for example:

7) echo "🦀 🐙 🐠 🐡" ;;   # marine life set

Open the spinner file:

nano "$HOME/.hhgttg/hhgttg.sh"

Your new style will now appear randomly among the others.

Advanced customisation

Variable Purpose Example
HHGTTG_SPINNER_SPEED Seconds per frame (float). export
HHGTTG_SPINNER_SPEED
=0.08
HHGTTG_SPINNER_COLOUR ANSI colour code (without \e[ and m). export
HHGTTG_SPINNER_COLOUR
="35"
(magenta)
HHGTTG_QUOTES_FILE Path to a custom quote file (one line per quote). export
HHGTTG_QUOTES_FILE
="$HOME/.myquotes"

To honour these variables, replace the hard‑coded colour line in spinner() with:

local colour="\e[${HHGTTG_SPINNER_COLOUR:-33}m"

and change _hhg_quote() to read from $HHGTTG_QUOTES_FILE if it exists.

11. Uninstall

If you ever want to remove the spinner completely:

# 1️⃣  Remove the installation directory
rm -rf "$HOME/.hhgttg"

# 2️⃣  Delete the lines we added to ~/.bashrc
sed -i '/# HHGTTG PROMPT + SPINNER/,+5d' "$HOME/.bashrc"
# (the `+5d` removes the block of six lines that follows the comment)

# 3️⃣  Reload the shell (or start a new terminal)
source "$HOME/.bashrc"

All traces of the spinner, the preexec library (if it was only in the
~/.hhgttg folder), and the optional lore file are gone.

12. Summary

✅ What you now have
A fully‑functional spinner that starts automatically before any command and stops after it finishes.
Randomised animation sets (emoji, Braille, sci‑fi, moons, dragons, …).
Configurable speed & colour via environment variables.
HHGTTG‑themed quote + towel printed after each command (optional).
A tiny installer (install_hhgttg.sh) that sets everything up in ~/.hhgttg.
Clean, modular directory layout – easy to back up, version‑control, or share.
Simple uninstall – one rm -rf and a sed command.
Extensible – add new spinner styles, quotes, or even hook into other Bash events.

Enjoy the new visual feedback, and remember: Don’t Panic – your spinner has got your back (and your towel). 🎉

Read next