diff --git a/README.md b/README.md index d594fdc..60bf995 100644 --- a/README.md +++ b/README.md @@ -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]`** diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index 1b06e06..039a78a 100755 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -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. diff --git a/profile.d/rain.sh b/profile.d/rain.sh index 7fec65a..3d4e8d8 100644 --- a/profile.d/rain.sh +++ b/profile.d/rain.sh @@ -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 +_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" @@ -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 # ------------------------------------------------------------------------------