#!/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. # ------------------------------------------------------------------------------ # Private helpers of profile.d/rain.sh, loaded by the module itself. # Fill RAIN_ENGINE_COLORS with the escape sequences of the drop colours (head to # tail) of the colour theme $1: a 20-step 24-bit gradient when the terminal # supports truecolor, a short 256-colour palette otherwise. # Usage: _rain_build_colors _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 } # Fill RAIN_ENGINE_CHARS with the characters a drop may be made of, according to # the animation ($1: rain or matrix) and the matrix charset name ($2). # Usage: _rain_build_chars _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 } # Validate a speed and print it as a delay in seconds for read -t / sleep: # values >= 1 are centiseconds (5 -> 0.050), values < 1 are raw seconds. # Return 1 for a non-numeric or null value. # Usage: _rain_normalize_speed _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 } # Validate a density (maximum number of simultaneous drops, an integer >= 1) # and print it; return 1 otherwise. # Usage: _rain_normalize_density _rain_normalize_density() { local raw_density="$1" if [[ ! "$raw_density" =~ ^[0-9]+$ || "$raw_density" -lt 1 ]]; then return 1 fi printf "%s" "$raw_density" } # Fill RAINBOW_BG_PALETTE with background colour escapes sweeping the # hue wheel: 6 bands of 256 steps in 24-bit colour, or 12 fixed ANSI colours # spread over the width otherwise. # Usage: _rainbow_build_palette _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 _trap_restore "$saved_traps" return 0 } # Shared engine of the rain and matrix animations: draws the falling drops until # a key is pressed, a signal is received, or the terminal is resized (the # state is rebuilt). The terminal state is always restored on exit. # Usage: _rain_engine _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 }