rain bug fix and new engine

This commit is contained in:
2026-10-13 15:00:04 +02:00
parent d28b602827
commit 32dc96ef0f
3 changed files with 182 additions and 100 deletions
+6 -6
View File
@@ -248,14 +248,14 @@ change the default without having to pass flags every time.
| Key | Default | Description |
| --- | --- | --- |
| `RAIN_DEFAULT_SPEED` | `0.1` | Falling speed for `rain` |
| `RAIN_DEFAULT_COLOR` | `Green` | Colour for `rain` |
| `RAIN_DEFAULT_SPEED` | `5` | Delay between frames for `rain`: a value ≥ 1 is in hundredths of a second (`5` = 0.05 s), a value < 1 is in seconds; must be > 0 |
| `RAIN_DEFAULT_COLOR` | `white` | Colour for `rain` (`green`, `blue`, `red`, `yellow`, `cyan`, `white`; case-insensitive) |
| `RAIN_DEFAULT_DENSITY` | dynamic | Maximum number of simultaneous falling elements for `rain` |
| `RAINBOW_DEFAULT_SPEED` | `0.04` | Horizontal color shift speed for `rainbow` |
| `MATRIX_DEFAULT_SPEED` | `0.05` | Falling speed for `matrix` |
| `MATRIX_DEFAULT_COLOR` | `Green` | Colour for `matrix` |
| `RAINBOW_DEFAULT_SPEED` | `4` | Frame delay (hundredths of a second) of the colour shift for `rainbow` |
| `MATRIX_DEFAULT_SPEED` | `3.5` | Frame delay for `matrix`, same scale as `RAIN_DEFAULT_SPEED` |
| `MATRIX_DEFAULT_COLOR` | `green` | Colour for `matrix` (same names as `rain`) |
| `MATRIX_DEFAULT_DENSITY` | dynamic | Maximum number of simultaneous falling elements for `matrix` |
| `MATRIX_DEFAULT_CHARSET` | `binary` | Character set for `matrix` (`binary`, `kana`, `ascii`) |
| `MATRIX_DEFAULT_CHARSET` | `binary` | Character set for `matrix` (`binary`, `kana`, `ascii`; case-insensitive) |
**`[ssh]`**
+12
View File
@@ -140,6 +140,18 @@ Versions follow `MAJOR.MINOR.PATCH-REVISION_STAGE_N` (e.g. `3.99.1-4_rc_1`).
(they were evaluated as arithmetic expressions); `pwdscore` classifies
characters with locale-proof classes; README default of
`GENPWD_DEFAULT_OCCURS` corrected.
- `rain.sh`: the terminal was left with the last drop colour active, the cursor
parked over the frozen last frame and the user's `INT`/`TERM`/`WINCH` traps
deleted and `stty` forced to `echo`: the screen is now cleared and the traps
and terminal settings restored; a resize now clears the screen and drops
(drops placed for the old size lingered); a drop vanished for one frame each
time another one left the screen; each frame is written with one `printf`
instead of one per character (output is byte-identical, a lot faster);
colour and charset names are case-insensitive (`RAIN_DEFAULT_COLOR=Green`, as
documented, fell back to white), an unknown colour is an error instead of a
silent white, a null speed no longer spins the CPU, a non-terminal stdin or
stdout is refused instead of looping, and the usage mentions `q` to quit;
README defaults of the `[rain]` keys corrected.
- `compress.sh`: option flags and loop variables no longer leak into the
interactive shell.
+164 -94
View File
@@ -41,9 +41,19 @@ load_conf "rain"
_rain_build_colors()
{
local base_color="$1"
# 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.
@@ -63,7 +73,7 @@ _rain_build_colors()
*) r1=40; g1=40; b1=45; r2=220; g2=220; b2=255 ;;
esac
local i r g b
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) ))
@@ -134,6 +144,12 @@ _rain_normalize_speed()
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
@@ -205,6 +221,54 @@ _rainbow_build_palette()
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 <command-name>
_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 <winch_handler_function>
_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"
@@ -213,16 +277,14 @@ _rain_engine()
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_need_tty "$mode" || return 1
_rain_build_colors "$base_color"
_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'
@@ -235,39 +297,48 @@ _rain_engine()
local new_rain_odd=0 falling_odd=0
local term_area=0
local frame_sleep="$step_duration"
local saved_stty="" saved_traps=""
sigwinch()
# 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))
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))
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))
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))
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))
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
@@ -275,16 +346,21 @@ _rain_engine()
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'
}
do_exit()
# 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()
{
exit_st=1
}
do_render()
{
local idx=0 y=0 drop_color="" current_char="" render_color=""
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]}
@@ -292,15 +368,19 @@ _rain_engine()
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"
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
rains=("${rains[@]:0:idx}" "${rains[@]:idx+num_rain_metadata:num_rains*num_rain_metadata}")
# 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
@@ -324,27 +404,24 @@ _rain_engine()
current_char="$rain_drop"
render_color="$drop_color"
fi
printf "\e[%d;%dH%b%s" "$y" "$X" "$render_color" "$current_char"
# 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
}
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
_rain_enter _rain_sigwinch
_rain_sigwinch
while ((exit_st <= 0)); do
read -r -n 1 -t "$frame_sleep" ch
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)
do_exit
_rain_stop
;;
esac
@@ -358,18 +435,16 @@ _rain_engine()
fi
X=$((term_width * RANDOM / 32768 + 1))
Y=$((1 - drop_length))
rains=("${rains[@]}" "$X" "$Y" "$rain_drop" "$drop_color" "$drop_length")
rains+=("$X" "$Y" "$rain_drop" "$drop_color" "$drop_length")
num_rains=$(( num_rains + 1 ))
fi
do_render
_rain_render
done
printf "\e[%d;1H\e[0K" "$term_height"
printf "\e[?25h"
stty echo
trap - TERM INT
trap - WINCH
_rain_leave
unset -f _rain_sigwinch _rain_render
return 0
}
# ------------------------------------------------------------------------------
@@ -388,13 +463,14 @@ rain()
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 "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"
}
@@ -413,13 +489,13 @@ rain()
-s|--speed)
if [[ -n "$2" && ! "$2" =~ ^- ]]; then
step_duration=$(_rain_normalize_speed "$2") || {
disp E "--speed requires a numeric value."
disp E "--speed requires a numeric value greater than 0."
_rain_show_usage
return 1
}
shift
else
disp E "--speed requires a numeric value."
disp E "--speed requires a numeric value greater than 0."
_rain_show_usage
return 1
fi
@@ -486,6 +562,7 @@ matrix()
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"
}
@@ -493,7 +570,9 @@ matrix()
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
@@ -505,13 +584,13 @@ matrix()
-s|--speed)
if [[ -n "$2" && ! "$2" =~ ^- ]]; then
step_duration=$(_rain_normalize_speed "$2") || {
disp E "--speed requires a numeric value."
disp E "--speed requires a numeric value greater than 0."
_matrix_show_usage
return 1
}
shift
else
disp E "--speed requires a numeric value."
disp E "--speed requires a numeric value greater than 0."
_matrix_show_usage
return 1
fi
@@ -597,9 +676,8 @@ rainbow()
local frame_sleep
frame_sleep=$(_rain_normalize_speed "$_raw_speed") || frame_sleep=0.040
local -a _pargs=()
readarray -td '' _pargs < <(printf '%s' "$PARSED" | xargs printf '%s\0')
set -- "${_pargs[@]}"
# Restore the getopt-normalised (shell-quoted) argument list
eval set -- "$PARSED"
while true; do
case "$1" in
-h|--help)
@@ -611,12 +689,12 @@ rainbow()
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"
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."
disp E "--speed requires a numeric value greater than 0."
return 1
}
shift 2
@@ -632,18 +710,17 @@ rainbow()
esac
done
command -v tput >/dev/null 2>&1 || {
disp E "The program 'tput' is required but not installed."
return 1
}
_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="" ch=""
local row_line="" screen="" ch=""
local saved_stty="" saved_traps=""
sigwinch()
# WINCH handler (and initial sizing): rebuild the palette for the new width
_rainbow_sigwinch()
{
term_width=$(tput cols)
term_height=$(tput lines)
@@ -657,52 +734,45 @@ rainbow()
fi
palette_width=$term_width
offset=0
_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
_rain_enter _rainbow_sigwinch
_rainbow_sigwinch
while ((exit_st <= 0)); do
read -r -n 1 -t "$frame_sleep" ch
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)
do_exit
_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))
while ((idx < 0)); do
idx=$(( idx + palette_width ))
done
idx=$((idx % palette_width))
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
printf "\e[%d;1H%b" "$row" "$row_line"
screen+="\e[${row};1H${row_line}"
done
printf '%b' "$screen"
((offset = (offset + 1) % palette_width))
offset=$(( (offset + 1) % palette_width ))
done
printf "\e[0m\e[2J\e[H"
printf "\e[?25h"
stty echo
trap - TERM INT
trap - WINCH
_rain_leave
unset -f _rainbow_sigwinch
unset RAINBOW_BG_PALETTE
return 0
}
export -f rainbow
# ------------------------------------------------------------------------------