#!/usr/bin/env bash # ------------------------------------------------------------------------------ # Copyright (c) 2013-2026 Geoffray Levasseur # Protected by the BSD3 license. Please read bellow for details. # # * Redistribution and use in source and binary forms, # * with or without modification, are permitted provided # * that the following conditions are met: # * # * Redistributions of source code must retain the above # * copyright notice, this list of conditions and the # * following disclaimer. # * # * Redistributions in binary form must reproduce the above # * copyright notice, this list of conditions and the following # * disclaimer in the documentation and/or other materials # * provided with the distribution. # * # * Neither the name of the copyright holder nor the names # * of any other contributors may be used to endorse or # * promote products derived from this software without # * specific prior written permission. # * # * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND # * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, # * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR # * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # * OF SUCH DAMAGE. # ------------------------------------------------------------------------------ load_conf "rain" # ------------------------------------------------------------------------------ # Generic rain-like engine and presets _rain_build_colors() { # Case-insensitive: the configuration documents capitalised names (Green) local base_color="${1,,}" local i RAIN_ENGINE_COLORS=() case "$base_color" in green|blue|red|yellow|cyan|white) ;; *) disp E "Unknown color: ${1} (supported: green, blue, red, yellow, cyan, white)." return 1 ;; esac local use_truecolor=0 # term_set() already sets TERM=*-direct when truecolor is available; # honour COLORTERM as a belt-and-suspenders fallback. [[ "$TERM" == *direct* || "${COLORTERM:-}" == "truecolor" || "${COLORTERM:-}" == "24bit" ]] && use_truecolor=1 if (( use_truecolor )); then # 24-bit gradient from a near-black shade to a vivid hue. # 20 steps provide smooth depth variation across simultaneous drops. local steps=20 local r1 g1 b1 r2 g2 b2 case "${base_color}" in green) r1=0; g1=12; b1=2; r2=50; g2=255; b2=90 ;; blue) r1=0; g1=5; b1=25; r2=60; g2=140; b2=255 ;; red) r1=15; g1=0; b1=0; r2=255; g2=40; b2=40 ;; yellow) r1=25; g1=18; b1=0; r2=255; g2=240; b2=50 ;; cyan) r1=0; g1=18; b1=18; r2=50; g2=255; b2=240 ;; *) r1=40; g1=40; b1=45; r2=220; g2=220; b2=255 ;; esac local r g b for ((i = 0; i < steps; i++)); do r=$(( r1 + (r2 - r1) * i / (steps - 1) )) g=$(( g1 + (g2 - g1) * i / (steps - 1) )) b=$(( b1 + (b2 - b1) * i / (steps - 1) )) RAIN_ENGINE_COLORS+=("\e[38;2;${r};${g};${b}m") done else # Fallback: 256-colour palettes. case $base_color in green) for i in {22..28} {34..40} {46..48}; do RAIN_ENGINE_COLORS+=("\e[38;5;${i}m"); done ;; blue) for i in {17..21} {27..33} {39..45}; do RAIN_ENGINE_COLORS+=("\e[38;5;${i}m"); done ;; red) for i in {52..52} {88..88} {124..124} {160..160} {196..201}; do RAIN_ENGINE_COLORS+=("\e[38;5;${i}m"); done ;; yellow) for i in {58..58} {100..100} {142..142} {184..184} {226..229}; do RAIN_ENGINE_COLORS+=("\e[38;5;${i}m"); done ;; cyan) for i in {30..31} {37..38} {44..45} {50..51}; do RAIN_ENGINE_COLORS+=("\e[38;5;${i}m"); done ;; *) RAIN_ENGINE_COLORS=("\e[37m" "\e[37;1m") for i in {244..255}; do RAIN_ENGINE_COLORS+=("\e[38;5;${i}m"); done ;; esac fi } _rain_build_chars() { local mode="$1" local charset="$2" RAIN_ENGINE_CHARS=() case "$mode" in matrix) case "$charset" in ""|binary) RAIN_ENGINE_CHARS=("0" "1") ;; kana|kanji) # Half-width katakana set, generally rendered as single-cell glyphs. RAIN_ENGINE_CHARS=("ア" "イ" "ウ" "エ" "オ" "カ" "キ" "ク" "ケ" "コ" "サ" "シ" "ス" "セ" "ソ" "タ" "チ" "ツ" "テ" "ト" "ナ" "ニ" "ヌ" "ネ" "ノ" "ハ" "ヒ" "フ" "ヘ" "ホ" "マ" "ミ" "ム" "メ" "モ" "ヤ" "ユ" "ヨ" "ラ" "リ" "ル" "レ" "ロ" "ワ" "ン") ;; ascii) RAIN_ENGINE_CHARS=("0" "1" "2" "3" "4" "5" "6" "7" "8" "9" "A" "B" "C" "D" "E" "F") ;; *) disp E "Unknown charset: ${charset} (supported: binary, kana, ascii)." return 1 ;; esac ;; *) RAIN_ENGINE_CHARS=("|" "│" "┃" "┆" "┇" "┊" "┋" "╽" "╿") ;; esac return 0 } _rain_normalize_speed() { local raw_speed="$1" # Accept integer/floating values. UI scale is centiseconds by default: # 5 -> 0.05s, 2.5 -> 0.025s. Values < 1 are treated as direct seconds # for backward compatibility (e.g. 0.03). if [[ ! "$raw_speed" =~ ^[0-9]+([.][0-9]+)?$ ]]; then return 1 fi # A null delay is refused: "read -t 0" does not wait, the animation loop # would spin and use a whole CPU. if LC_ALL=C awk -v s="$raw_speed" 'BEGIN { exit !(s == 0) }'; then return 1 fi if LC_ALL=C awk -v s="$raw_speed" 'BEGIN { exit !(s < 1) }'; then printf "%s" "$raw_speed" else LC_ALL=C awk -v s="$raw_speed" 'BEGIN { printf "%.3f", s / 100 }' fi } _rain_normalize_density() { local raw_density="$1" if [[ ! "$raw_density" =~ ^[0-9]+$ || "$raw_density" -lt 1 ]]; then return 1 fi printf "%s" "$raw_density" } _rainbow_build_palette() { local palette_width="$1" local use_truecolor="$2" local x=0 wheel_pos=0 band=0 blend=0 local red=0 green=0 blue=0 RAINBOW_BG_PALETTE=() if (( palette_width < 1 )); then RAINBOW_BG_PALETTE+=("\e[41m") return 0 fi if (( use_truecolor )); then for ((x = 0; x < palette_width; x++)); do wheel_pos=$((x * 1536 / palette_width)) band=$((wheel_pos / 256)) blend=$((wheel_pos % 256)) case "$band" in 0) red=255; green=$blend; blue=0 ;; 1) red=$((255 - blend)); green=255; blue=0 ;; 2) red=0; green=255; blue=$blend ;; 3) red=0; green=$((255 - blend)); blue=255 ;; 4) red=$blend; green=0; blue=255 ;; *) red=255; green=0; blue=$((255 - blend)) ;; esac RAINBOW_BG_PALETTE+=("\e[48;2;${red};${green};${blue}m") done else local ansi_palette=(41 101 43 103 42 102 46 106 44 104 45 105) local ansi_count=${#ansi_palette[@]} for ((x = 0; x < palette_width; x++)); do RAINBOW_BG_PALETTE+=("\e[${ansi_palette[x * ansi_count / palette_width]}m") done fi } # Stop request of the animation loops: sets exit_st, a local of the running # animation (dynamic scoping). Installed as the TERM/INT trap. _rain_stop() { exit_st=1 } # The animations read a key from the terminal and draw on it, and need tput. # Usage: _rain_need_tty _rain_need_tty() { if [[ ! -t 0 || ! -t 1 ]]; then disp E "$1: a terminal is required (standard input and output must be a terminal)." return 1 fi command -v tput >/dev/null 2>&1 || { disp E "The program 'tput' is required but not installed." return 1 } } # Prepare the terminal for an animation: save its settings and the TERM, INT # and WINCH traps of the caller, install the animation traps, disable echo, # hide the cursor and clear the screen. # saved_stty and saved_traps are locals of the calling animation. # Usage: _rain_enter _rain_enter() { saved_stty=$(stty -g 2>/dev/null) saved_traps=$(trap -p TERM INT WINCH) trap _rain_stop TERM INT trap "$1" WINCH stty -echo printf '\e[?25l\e[2J' } # Undo _rain_enter: reset the colours (the last drawn drop colour would # otherwise stay active), show the cursor, clear the screen, then restore the # terminal settings and the caller's traps. _rain_leave() { printf '\e[0m\e[?25h\e[2J\e[H' [[ -n "$saved_stty" ]] && stty "$saved_stty" trap - TERM INT WINCH [[ -n "$saved_traps" ]] && eval "$saved_traps" return 0 } _rain_engine() { local step_duration="$1" local base_color="$2" local mode="$3" local charset="$4" local density_override="$5" _rain_need_tty "$mode" || return 1 _rain_build_colors "$base_color" || return 1 _rain_build_chars "$mode" "$charset" || return 1 local rain_colors=("${RAIN_ENGINE_COLORS[@]}") local rain_chars=("${RAIN_ENGINE_CHARS[@]}") unset RAIN_ENGINE_COLORS RAIN_ENGINE_CHARS local rain_color_tab=${#rain_colors[@]} local rain_tab=${#rain_chars[@]} local matrix_head_color=$'\e[1;97m' local exit_st=0 local num_rain_metadata=5 local term_height=0 term_width=0 local X=0 Y=0 drop_length=0 rain_drop=0 local max_rain_width=0 max_rain_height=0 local new_rain_odd=0 falling_odd=0 local term_area=0 local frame_sleep="$step_duration" local saved_stty="" saved_traps="" # The drops: 5 consecutive entries each (X, Y of the top, character, colour, # length). local rains=() local num_rains=0 local ch="" # WINCH handler (and initial sizing). Called with the animation running, it # sees its locals through dynamic scoping. _rain_sigwinch() { term_width=$(tput cols) term_height=$(tput lines) term_area=$(( term_width * term_height )) case "$mode" in matrix) max_rain_width=$(( term_area / 3 )) max_rain_height=$(( term_height < 8 ? 1 : term_height / 6 )) new_rain_odd=$(( term_height > 50 ? 100 : term_height * 2 )) new_rain_odd=$(( new_rain_odd * 85 / 100 )) falling_odd=100 # Adapt cadence and density to terminal size for smoother rendering. if ((term_area < 1200)); then max_rain_width=$(( term_area / 4 )) frame_sleep=$(LC_ALL=C awk -v s="$step_duration" 'BEGIN { printf "%.3f", s * 1.15 }') elif ((term_area > 5000)); then max_rain_width=$(( term_area / 2 )) frame_sleep=$(LC_ALL=C awk -v s="$step_duration" 'BEGIN { printf "%.3f", s * 0.85 }') else frame_sleep="$step_duration" fi ;; *) max_rain_width=$(( term_area / 4 )) max_rain_height=$(( term_height < 10 ? 1 : term_height / 10 )) new_rain_odd=$(( term_height > 50 ? 100 : term_height * 2 )) new_rain_odd=$(( new_rain_odd * 75 / 100 )) falling_odd=$(( term_height > 25 ? 100 : term_height * 4 )) falling_odd=$(( falling_odd * 90 / 100 )) frame_sleep="$step_duration" ;; esac if [[ -n "$density_override" ]]; then max_rain_width="$density_override" fi # The drops were placed for the previous size and the screen content # does not fit the new one: start again from an empty screen. rains=() num_rains=0 printf '\e[2J' } # Draw one frame. The whole frame (erasing the previous drop positions, # then the new ones) is built in a string and written with one printf: a # write per character made the animation slow and flickering. _rain_render() { local idx=0 y=0 drop_color="" current_char="" render_color="" out="" local esc=$'\e' for ((idx = 0; idx < num_rains * num_rain_metadata; idx += num_rain_metadata)); do X=${rains[idx]} Y=${rains[idx + 1]} drop_length=${rains[idx + 4]} for ((y = Y; y < Y + drop_length; y++)); do ((y < 1 || y > term_height)) && continue out+="${esc}[${y};${X}H " done done for ((idx = 0; idx < num_rains * num_rain_metadata; idx += num_rain_metadata)); do if ((100 * RANDOM / 32768 < falling_odd)); then if ((++rains[idx + 1] > term_height)); then # The drop left the screen: remove it. The next drop moves # into this slot, which must be processed again (the loop # increment would skip it, and it would vanish for a frame). rains=("${rains[@]:0:idx}" "${rains[@]:idx+num_rain_metadata}") num_rains=$(( num_rains - 1 )) idx=$(( idx - num_rain_metadata )) continue fi fi X=${rains[idx]} Y=${rains[idx + 1]} rain_drop=${rains[idx + 2]} drop_color=${rains[idx + 3]} drop_length=${rains[idx + 4]} for ((y = Y; y < Y + drop_length; y++)); do ((y < 1 || y > term_height)) && continue if [[ "$mode" == "matrix" ]]; then current_char="${rain_chars[rain_tab * RANDOM / 32768]}" if ((y == Y + drop_length - 1)); then render_color="$matrix_head_color" else render_color="$drop_color" fi else current_char="$rain_drop" render_color="$drop_color" fi # render_color holds a literal \e[...m: hence %b below out+="${esc}[${y};${X}H${render_color}${current_char}" done done [[ -n "$out" ]] && printf '%b' "$out" return 0 } _rain_enter _rain_sigwinch _rain_sigwinch while ((exit_st <= 0)); do ch="" # A timeout makes read fail (status > 128): expected, not an error read -r -n 1 -t "$frame_sleep" ch || true case "$ch" in q|Q) _rain_stop ;; esac if ((num_rains < max_rain_width)) && ((100 * RANDOM / 32768 < new_rain_odd)); then rain_drop="${rain_chars[rain_tab * RANDOM / 32768]}" drop_color="${rain_colors[rain_color_tab * RANDOM / 32768]}" if [[ "$mode" == "matrix" ]]; then drop_length=$((max_rain_height * RANDOM / 32768 + 2)) else drop_length=$((max_rain_height * RANDOM / 32768 + 1)) fi X=$((term_width * RANDOM / 32768 + 1)) Y=$((1 - drop_length)) rains+=("$X" "$Y" "$rain_drop" "$drop_color" "$drop_length") num_rains=$(( num_rains + 1 )) fi _rain_render done _rain_leave unset -f _rain_sigwinch _rain_render return 0 } # ------------------------------------------------------------------------------ # Let the rain fall (current style) # Usage: rain [OPTIONS] rain() { local _rain_show_usage _rain_show_usage() { printf "Usage: rain [OPTIONS]\n" printf "Options:\n" printf "\t-s, --speed NUM\tSet speed value (default: 5 => 0.050s).\n" printf "\t\t\t\tValues >=1 use a /100 scale (5 => 0.05s).\n" printf "\t\t\t\tValues <1 are interpreted as raw seconds.\n" printf "\t-d, --density NUM\tMaximum number of simultaneous falling elements.\n" printf "\t-c, --color COLOR\tSet the color theme (default: white).\n" printf "\t-h, --help\t\tDisplay this help message and exit.\n\n" printf "Available Colors (case-insensitive):\n" printf "\t\e[32mgreen\e[0m\t: Matrix-like green shades\n" printf "\t\e[34mblue\e[0m\t: Deep ocean blue gradients\n" printf "\t\e[31mred\e[0m\t: Crimson/Blood rain\n" printf "\t\e[33myellow\e[0m\t: Amber and gold tones\n" printf "\t\e[36mcyan\e[0m\t: Electric cyan/turquoise\n" printf "\twhite\t: Greyscale and white (original style)\n\n" printf "Press q (or Ctrl-C) to quit.\n" printf "Example: rain --color green --speed 3\n" } local _raw_speed="${RAIN_DEFAULT_SPEED:-5}" local step_duration step_duration=$(_rain_normalize_speed "$_raw_speed") || step_duration=0.050 local base_color="${RAIN_DEFAULT_COLOR:-white}" local density_override="${RAIN_DEFAULT_DENSITY:-}" if [[ -n "$density_override" ]]; then density_override=$(_rain_normalize_density "$density_override") || density_override="" fi while [[ "$#" -gt 0 ]]; do case $1 in -s|--speed) if [[ -n "$2" && ! "$2" =~ ^- ]]; then step_duration=$(_rain_normalize_speed "$2") || { disp E "--speed requires a numeric value greater than 0." _rain_show_usage return 1 } shift else disp E "--speed requires a numeric value greater than 0." _rain_show_usage return 1 fi ;; -c|--color) if [[ -n "$2" && ! "$2" =~ ^- ]]; then base_color="$2" shift else disp E "--color requires a color name." _rain_show_usage return 1 fi ;; -d|--density) if [[ -n "$2" && ! "$2" =~ ^- ]]; then density_override=$(_rain_normalize_density "$2") || { disp E "--density requires a positive integer value." _rain_show_usage return 1 } shift else disp E "--density requires a positive integer value." _rain_show_usage return 1 fi ;; -h|--help) _rain_show_usage return 0 ;; --) shift break ;; *) disp E "Unknown option: $1" _rain_show_usage return 1 ;; esac shift done _rain_engine "$step_duration" "$base_color" "rain" "" "$density_override" } export -f rain # ------------------------------------------------------------------------------ # Matrix style digital rain # Usage: matrix [OPTIONS] matrix() { local _matrix_show_usage _matrix_show_usage() { printf "Usage: matrix [OPTIONS]\n" printf "Options:\n" printf "\t-s, --speed NUM\tSet speed value (default: 3.5 => 0.035s).\n" printf "\t\t\t\tValues >=1 use a /100 scale (3.5 => 0.035s).\n" printf "\t\t\t\tValues <1 are interpreted as raw seconds.\n" printf "\t-d, --density NUM\tMaximum number of simultaneous falling elements.\n" printf "\t-c, --color COLOR\tSet color theme (default: green).\n" printf "\t-C, --charset SET\tCharacter set: binary, kana, ascii (default: binary).\n" printf "\t-h, --help\t\tDisplay this help message and exit.\n\n" printf "Press q (or Ctrl-C) to quit.\n" printf "Example: matrix -C kana -c green --speed 2\n" } local _raw_speed="${MATRIX_DEFAULT_SPEED:-3.5}" local step_duration step_duration=$(_rain_normalize_speed "$_raw_speed") || step_duration=0.035 local base_color="${MATRIX_DEFAULT_COLOR:-green}" # Lower-cased, like the value of -C: the configuration may say "Kana" local charset="${MATRIX_DEFAULT_CHARSET:-binary}" charset="${charset,,}" local density_override="${MATRIX_DEFAULT_DENSITY:-}" if [[ -n "$density_override" ]]; then density_override=$(_rain_normalize_density "$density_override") || density_override="" fi while [[ "$#" -gt 0 ]]; do case $1 in -s|--speed) if [[ -n "$2" && ! "$2" =~ ^- ]]; then step_duration=$(_rain_normalize_speed "$2") || { disp E "--speed requires a numeric value greater than 0." _matrix_show_usage return 1 } shift else disp E "--speed requires a numeric value greater than 0." _matrix_show_usage return 1 fi ;; -c|--color) if [[ -n "$2" && ! "$2" =~ ^- ]]; then case "${2,,}" in binary|kana|kanji|ascii) disp W "'${2}' looks like a charset value. Use -C/--charset for clarity." charset="${2,,}" ;; *) base_color="$2" ;; esac shift else disp E "--color requires a color name." _matrix_show_usage return 1 fi ;; -C|--charset) if [[ -n "$2" && ! "$2" =~ ^- ]]; then charset="${2,,}" shift else disp E "--charset requires a value." _matrix_show_usage return 1 fi ;; -d|--density) if [[ -n "$2" && ! "$2" =~ ^- ]]; then density_override=$(_rain_normalize_density "$2") || { disp E "--density requires a positive integer value." _matrix_show_usage return 1 } shift else disp E "--density requires a positive integer value." _matrix_show_usage return 1 fi ;; -h|--help) _matrix_show_usage return 0 ;; --) shift break ;; *) disp E "Unknown option: $1" _matrix_show_usage return 1 ;; esac shift done _rain_engine "$step_duration" "$base_color" "matrix" "$charset" "$density_override" } export -f matrix # ------------------------------------------------------------------------------ # Full-screen rainbow screensaver # Usage: rainbow [OPTIONS] rainbow() { local PARSED PARSED=$(getopt -o hs: --long help,speed: -n 'rainbow' -- "$@") # shellcheck disable=SC2181 # getopt return code is checked immediately after if [[ $? -ne 0 ]]; then disp E "Invalid options, use \"rainbow --help\" to display usage." return 1 fi local _raw_speed="${RAINBOW_DEFAULT_SPEED:-4}" local frame_sleep frame_sleep=$(_rain_normalize_speed "$_raw_speed") || frame_sleep=0.040 # Restore the getopt-normalised (shell-quoted) argument list eval set -- "$PARSED" while true; do case "$1" in -h|--help) printf "Usage: rainbow [OPTIONS]\n" printf "Options:\n" printf "\t-s, --speed NUM\tSet horizontal color shift speed (default: 4 => 0.040s).\n" printf "\t\t\t\tValues >=1 use a /100 scale (4 => 0.04s).\n" printf "\t\t\t\tValues <1 are interpreted as raw seconds.\n" printf "\t-h, --help\t\tDisplay this help message and exit.\n\n" printf "The rainbow fills the whole terminal with background colors only.\n" printf "It uses truecolor when supported, otherwise ANSI bright backgrounds.\n" printf "Press q (or Ctrl-C) to quit.\n" return 0 ;; -s|--speed) frame_sleep=$(_rain_normalize_speed "$2") || { disp E "--speed requires a numeric value greater than 0." return 1 } shift 2 ;; --) shift break ;; *) disp E "Invalid options, use \"rainbow --help\" to display usage." return 1 ;; esac done _rain_need_tty rainbow || return 1 local exit_st=0 local term_height=0 term_width=0 local use_truecolor=0 local offset=0 row=0 col=0 palette_width=0 idx=0 local row_line="" screen="" ch="" local saved_stty="" saved_traps="" # WINCH handler (and initial sizing): rebuild the palette for the new width _rainbow_sigwinch() { term_width=$(tput cols) term_height=$(tput lines) ((term_width < 1)) && term_width=1 ((term_height < 1)) && term_height=1 if [[ "$TERM" == *direct* || "${COLORTERM:-}" == "truecolor" || "${COLORTERM:-}" == "24bit" ]]; then use_truecolor=1 else use_truecolor=0 fi palette_width=$term_width offset=0 _rainbow_build_palette "$palette_width" "$use_truecolor" } _rain_enter _rainbow_sigwinch _rainbow_sigwinch while ((exit_st <= 0)); do ch="" # A timeout makes read fail (status > 128): expected, not an error read -r -n 1 -t "$frame_sleep" ch || true case "$ch" in q|Q) _rain_stop ;; esac # One row of colours, shifted by offset (col - offset is at least # 1 - palette_width, so adding palette_width makes it positive) row_line="" for ((col = 0; col < term_width; col++)); do idx=$(( (col - offset + palette_width) % palette_width )) row_line+="${RAINBOW_BG_PALETTE[idx]} " done row_line+=$'\e[0m' # Every row is the same: the whole screen goes out in one write screen="" for ((row = 1; row <= term_height; row++)); do screen+="\e[${row};1H${row_line}" done printf '%b' "$screen" offset=$(( (offset + 1) % palette_width )) done _rain_leave unset -f _rainbow_sigwinch unset RAINBOW_BG_PALETTE return 0 } export -f rainbow # ------------------------------------------------------------------------------ # EOF