#!/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. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # Generic rain-like engine and presets _rain_build_colors() { local base_color="$1" RAIN_ENGINE_COLORS=() local use_truecolor=0 _rainbow_supports_truecolor && 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 i 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 if awk -v s="$raw_speed" 'BEGIN { exit !(s < 1) }'; then printf "%s" "$raw_speed" else 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_supports_truecolor() { case "${COLORTERM:-}" in *truecolor*|*24bit*) return 0 ;; esac case "${TERM:-}" in *direct*|*truecolor*) return 0 ;; esac return 1 } _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 } _rain_engine() { local step_duration="$1" local base_color="$2" local mode="$3" local charset="$4" local density_override="$5" command -v tput >/dev/null 2>&1 || { disp E "The program 'tput' is required but not installed." return 1 } _rain_build_colors "$base_color" _rain_build_chars "$mode" "$charset" || return 1 local rain_colors=("${RAIN_ENGINE_COLORS[@]}") local rain_chars=("${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" 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=$(awk -v s="$step_duration" 'BEGIN { printf "%.3f", s * 1.15 }') elif ((term_area > 5000)); then ((max_rain_width = term_area / 2)) frame_sleep=$(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 } do_exit() { exit_st=1 } do_render() { local idx=0 y=0 drop_color="" current_char="" render_color="" 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 printf "\e[%d;%dH " "$y" "$X" 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 rains=("${rains[@]:0:idx}" "${rains[@]:idx+num_rain_metadata:num_rains*num_rain_metadata}") ((num_rains--)) 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 printf "\e[%d;%dH%b%s" "$y" "$X" "$render_color" "$current_char" done done } trap do_exit TERM INT trap sigwinch WINCH stty -echo printf "\e[?25l" printf "\e[2J" local rains=() local num_rains=0 local ch="" sigwinch while ((exit_st <= 0)); do read -r -n 1 -t "$frame_sleep" ch case "$ch" in q|Q) do_exit ;; 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=("${rains[@]}" "$X" "$Y" "$rain_drop" "$drop_color" "$drop_length") ((num_rains++)) fi do_render done printf "\e[%d;1H\e[0K" "$term_height" printf "\e[?25h" stty echo trap - TERM INT trap - WINCH } # ------------------------------------------------------------------------------ # 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:\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 "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." _rain_show_usage return 1 } shift else disp E "--speed requires a numeric value." _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 "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}" local charset="${MATRIX_DEFAULT_CHARSET:-binary}" 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." _matrix_show_usage return 1 } shift else disp E "--speed requires a numeric value." _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 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 to quit.\n" return 0 ;; -s|--speed) frame_sleep=$(_rain_normalize_speed "$2") || { disp E "--speed requires a numeric value." return 1 } shift 2 ;; --) shift break ;; *) disp E "Invalid options, use \"rainbow --help\" to display usage." return 1 ;; esac done command -v tput >/dev/null 2>&1 || { disp E "The program 'tput' is required but not installed." 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="" ch="" sigwinch() { term_width=$(tput cols) term_height=$(tput lines) ((term_width < 1)) && term_width=1 ((term_height < 1)) && term_height=1 if _rainbow_supports_truecolor; then use_truecolor=1 else use_truecolor=0 fi palette_width=$term_width _rainbow_build_palette "$palette_width" "$use_truecolor" } do_exit() { exit_st=1 } trap do_exit TERM INT trap sigwinch WINCH stty -echo printf "\e[?25l" printf "\e[2J" sigwinch while ((exit_st <= 0)); do read -r -n 1 -t "$frame_sleep" ch case "$ch" in q|Q) do_exit ;; esac row_line="" for ((col = 0; col < term_width; col++)); do idx=$((col - offset)) while ((idx < 0)); do ((idx += palette_width)) done idx=$((idx % palette_width)) row_line+="${RAINBOW_BG_PALETTE[idx]} " done row_line+=$'\e[0m' for ((row = 1; row <= term_height; row++)); do printf "\e[%d;1H%b" "$row" "$row_line" done ((offset = (offset + 1) % palette_width)) done printf "\e[0m\e[2J\e[H" printf "\e[?25h" stty echo trap - TERM INT trap - WINCH } export -f rainbow # ------------------------------------------------------------------------------ load_conf "rain" # EOF