diff --git a/.config/pacman/aur-packages.txt b/.config/pacman/aur-packages.txt index a41fda83..cec13c48 100644 --- a/.config/pacman/aur-packages.txt +++ b/.config/pacman/aur-packages.txt @@ -31,7 +31,6 @@ pacseek 1.8.2-1 pcsx2-latest-bin 1.7.5201-1 pika-backup 1:0.6.2-1 pinterest-downloader-git 7d540b1.20221130.157-1 -pipes.sh 1.3.0-1 proton-ge-custom-bin 1:GE_Proton8_25-1 protontricks 1.10.5-1 pyprland 1.6.8-1 @@ -58,8 +57,6 @@ tgpt-bin 2.2.1-1 topgrade 13.0.0-1 ttf-ms-win11-auto 10.0.22621.525-1 ttf-symbola 14.00-1 -tty-clock-git 1:2.3.r40.f2f847c-2 -unimatrix-git r66.75f7402-1 universal-android-debloater 0.6.1-1 upscayl-bin 2.9.5-1 ventoy-bin 1.0.96-1 diff --git a/.local/bin/cbonsai b/.local/bin/cbonsai new file mode 100755 index 00000000..177c3293 Binary files /dev/null and b/.local/bin/cbonsai differ diff --git a/.local/bin/pipes b/.local/bin/pipes new file mode 100755 index 00000000..73346ef2 --- /dev/null +++ b/.local/bin/pipes @@ -0,0 +1,385 @@ +#!/usr/bin/env bash +# pipes.sh: Animated pipes terminal screensaver. +# https://github.com/pipeseroni/pipes.sh +# +# Copyright (c) 2015-2018 Pipeseroni/pipes.sh contributors +# Copyright (c) 2013-2015 Yu-Jie Lin +# Copyright (c) 2010 Matthew Simpson +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +VERSION=1.3.0 + +M=32768 # Bash RANDOM maximum + 1 +p=1 # number of pipes +f=75 # frame rate +s=13 # probability of straight fitting +r=2000 # characters limit +t=0 # iteration counter for -r character limit +w=80 # terminal size +h=24 + +# ab -> sets[][idx] = a*4 + b +# 0: up, 1: right, 2: down, 3: left +# 00 means going up , then going up -> ┃ +# 12 means going right, then going down -> ┓ +sets=( + "┃┏ ┓┛━┓ ┗┃┛┗ ┏━" + "│╭ ╮╯─╮ ╰│╯╰ ╭─" + "│┌ ┐┘─┐ └│┘└ ┌─" + "║╔ ╗╝═╗ ╚║╝╚ ╔═" + "|+ ++-+ +|++ +-" + "|/ \/-\ \|/\ /-" + ".. .... .... .." + ".o oo.o o.oo o." + "-\ /\|/ /-\/ \|" # railway + "╿┍ ┑┚╼┒ ┕╽┙┖ ┎╾" # knobby pipe +) +SETS=() # rearranged all pipe chars into individul elements for easier access + +# pipes' +x=() # current position +y=() +l=() # current directions + # 0: up, 1: right, 2: down, 3: left +n=() # new directions +v=() # current types +c=() # current escape codes + +# selected pipes' +V=() # types (indexes to sets[]) +C=() # color indices for tput setaf +VN=0 # number of selected types +CN=0 # number of selected colors +E=() # pre-generated escape codes from BOLD, NOCOLOR, and C + +# switches +RNDSTART=0 # randomize starting position and direction +BOLD=1 +NOCOLOR=0 +KEEPCT=0 # keep pipe color and type + + +# print help message in 72-char width +print_help() { + local cgap + printf -v cgap '%*s' $((15 - ${#COLORS})) '' + cat <= 0 + is_N() { + [[ -n $1 && -z ${1//[0-9]} ]] + } + + + # test if $1 is a hexadecimal string + is_hex() { + [[ -n $1 && -z ${1//[0-9A-Fa-f]} ]] + } + + + # print error message for invalid argument to standard error, this + # - mimics getopts error message + # - use all positional parameters as error message + # - has a newline appended + # $arg and $OPTARG are the option name and argument set by getopts. + pearg() { + printf "%s: -$arg invalid argument -- $OPTARG; %s\n" "$0" "$*" >&2 + } + + + OPTIND=1 + while getopts "p:t:c:f:s:r:RBCKhv" arg; do + case $arg in + p) + if is_N "$OPTARG" && ((OPTARG > 0)); then + p=$OPTARG + else + pearg 'must be an integer and greater than 0' + return 1 + fi + ;; + t) + if [[ "$OPTARG" = c???????????????? ]]; then + V+=(${#sets[@]}) + sets+=("${OPTARG:1}") + elif is_N "$OPTARG" && ((OPTARG < ${#sets[@]})); then + V+=($OPTARG) + else + pearg 'must be an integer and from 0 to' \ + "$((${#sets[@]} - 1)); or a custom type" + return 1 + fi + ;; + c) + if [[ $OPTARG == '#'* ]]; then + if ! is_hex "${OPTARG:1}"; then + pearg 'unrecognized hexadecimal string' + return 1 + fi + if ((16$OPTARG >= COLORS)); then + pearg 'hexadecimal must be from #0 to' \ + "#$(printf '%X' $((COLORS - 1)))" + return 1 + fi + C+=($((16$OPTARG))) + elif is_N "$OPTARG" && ((OPTARG < COLORS)); then + C+=($OPTARG) + else + pearg "must be an integer and from 0 to $((COLORS - 1));" \ + 'or a hexadecimal string with # prefix' + return 1 + fi + ;; + f) + if is_N "$OPTARG" && ((OPTARG >= 20 && OPTARG <= 100)); then + f=$OPTARG + else + pearg 'must be an integer and from 20 to 100' + return 1 + fi + ;; + s) + if is_N "$OPTARG" && ((OPTARG >= 5 && OPTARG <= 15)); then + s=$OPTARG + else + pearg 'must be an integer and from 5 to 15' + return 1 + fi + ;; + r) + if is_N "$OPTARG"; then + r=$OPTARG + else + pearg 'must be a non-negative integer' + return 1 + fi + ;; + R) RNDSTART=1;; + B) BOLD=0;; + C) NOCOLOR=1;; + K) KEEPCT=1;; + h) + print_help + exit 0 + ;; + v) echo "$(basename -- "$0") $VERSION" + exit 0 + ;; + *) + return 1 + esac + done + + shift $((OPTIND - 1)) + if (($#)); then + printf "$0: illegal arguments -- $*; no arguments allowed\n" >&2 + return 1 + fi +} + + +cleanup() { + # clear out standard input + read -t 0.001 && cat /dev/null + + tput reset # fix for konsole, see pipeseroni/pipes.sh#43 + tput rmcup + tput cnorm + stty echo + printf "$SGR0" + exit 0 +} + + +resize() { + w=$(tput cols) h=$(tput lines) +} + + +init_pipes() { + # +_CP_init_pipes + local i + + ci=$((KEEPCT ? 0 : CN * RANDOM / M)) + vi=$((KEEPCT ? 0 : VN * RANDOM / M)) + for ((i = 0; i < p; i++)); do + (( + n[i] = 0, + l[i] = RNDSTART ? RANDOM % 4 : 0, + x[i] = RNDSTART ? w * RANDOM / M : w / 2, + y[i] = RNDSTART ? h * RANDOM / M : h / 2, + v[i] = V[vi] + )) + c[i]=${E[ci]} + ((ci = (ci + 1) % CN, vi = (vi + 1) % VN)) + done + # -_CP_init_pipes +} + + +init_screen() { + stty -echo + tput smcup + tput civis + tput clear + trap cleanup HUP TERM + + resize + trap resize SIGWINCH +} + + +main() { + # simple pre-check of TERM, tput's error message should be enough + tput -T "$TERM" sgr0 >/dev/null || return $? + + # +_CP_init_termcap_vars + COLORS=$(tput colors) # COLORS - 1 == maximum color index for -c argument + SGR0=$(tput sgr0) + SGR_BOLD=$(tput bold) + # -_CP_init_termcap_vars + + parse "$@" || return $? + + # +_CP_init_VC + # set default values if not by options + ((${#V[@]})) || V=(0) + VN=${#V[@]} + ((${#C[@]})) || C=(1 2 3 4 5 6 7 0) + CN=${#C[@]} + # -_CP_init_VC + + # +_CP_init_E + # generate E[] based on BOLD (SGR_BOLD), NOCOLOR, and C for each element in + # C, a corresponding element in E[] = + # SGR0 + # + SGR_BOLD, if BOLD + # + tput setaf C, if !NOCOLOR + local i + for ((i = 0; i < CN; i++)) { + E[i]=$SGR0 + ((BOLD)) && E[i]+=$SGR_BOLD + ((NOCOLOR)) || E[i]+=$(tput setaf ${C[i]}) + } + # -_CP_init_E + + # +_CP_init_SETS + local i j + for ((i = 0; i < ${#sets[@]}; i++)) { + for ((j = 0; j < 16; j++)) { + SETS+=("${sets[i]:j:1}") + } + } + unset i j + # -_CP_init_SETS + + init_screen + init_pipes + + # any key press exits the loop and this script + trap 'break 2' INT + + local i + while REPLY=; do + read -t 0.0$((1000 / f)) -n 1 2>/dev/null + case "$REPLY" in + P) ((s = s < 15 ? s + 1 : s));; + O) ((s = s > 3 ? s - 1 : s));; + F) ((f = f < 100 ? f + 1 : f));; + D) ((f = f > 20 ? f - 1 : f));; + B) ((BOLD = (BOLD + 1) % 2));; + C) ((NOCOLOR = (NOCOLOR + 1) % 2));; + K) ((KEEPCT = (KEEPCT + 1) % 2));; + ?) break;; + esac + for ((i = 0; i < p; i++)); do + # New position: + # l[] direction = 0: up, 1: right, 2: down, 3: left + # +_CP_newpos + ((l[i] % 2)) && ((x[i] += -l[i] + 2, 1)) || ((y[i] += l[i] - 1)) + # -_CP_newpos + + # Loop on edges (change color on loop): + # +_CP_warp + ((!KEEPCT && (x[i] >= w || x[i] < 0 || y[i] >= h || y[i] < 0))) \ + && { c[i]=${E[CN * RANDOM / M]}; ((v[i] = V[VN * RANDOM / M])); } + ((x[i] = (x[i] + w) % w, + y[i] = (y[i] + h) % h)) + # -_CP_warp + + # new turning direction: + # $((s - 1)) in $s, going straight, therefore n[i] == l[i]; + # and 1 in $s that pipe makes a right or left turn + # + # s * RANDOM / M - 1 == 0 + # n[i] == -1 + # => n[i] == l[i] + 1 or l[i] - 1 + # +_CP_newdir + (( + n[i] = s * RANDOM / M - 1, + n[i] = n[i] >= 0 ? l[i] : l[i] + (2 * (RANDOM % 2) - 1), + n[i] = (n[i] + 4) % 4 + )) + # -_CP_newdir + + # Print: + # +_CP_print + printf '\e[%d;%dH%s%s' \ + $((y[i] + 1)) $((x[i] + 1)) ${c[i]} \ + "${SETS[v[i] * 16 + l[i] * 4 + n[i]]}" + # -_CP_print + l[i]=${n[i]} + done + ((r > 0 && t * p >= r)) && tput reset && tput civis && t=0 || ((t++)) + done + + cleanup +} + + +# when being sourced, $0 == bash, only invoke main when they are the same +[[ "$0" != "$BASH_SOURCE" ]] || main "$@" diff --git a/.local/bin/tty-clock b/.local/bin/tty-clock new file mode 100755 index 00000000..135b6a6f Binary files /dev/null and b/.local/bin/tty-clock differ diff --git a/.local/bin/unimatrix b/.local/bin/unimatrix new file mode 100755 index 00000000..098f9289 --- /dev/null +++ b/.local/bin/unimatrix @@ -0,0 +1,766 @@ +#!/usr/bin/env python3 +# +# unimatrix.py +# +# +# Python script to simulate the display from "The Matrix" in terminal. Uses +# half-width katakana unicode characters by default, but can use custom +# character sets. Accepts keyboard controls while running. +# +# Based on CMatrix by Chris Allegretta and Abishek V. Ashok. The following +# option should produce virtually the same output as CMatrix: +# $ unimatrix -n -s 96 -l o +# +# Unimatrix is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# Unimatrix is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License at +# for more details. +# +# Created by William Mannard +# 2018/01/19 + +import argparse +import curses +import time +from random import choice, randint + +help_msg = ''' +USAGE + unimatrix [-a] [-b] [-c COLOR] [-f] [-g COLOR] [-h] [-l CHARACTER_LIST] [-n] + [-o] [-s SPEED] [-u CUSTOM_CHARACTERS] + +OPTIONAL ARGUMENTS + -a Asynchronous scroll. Lines will move at varied speeds. + + -b Use only bold characters + + -c COLOR One of: green (default), red, blue, white, yellow, cyan, + magenta, black + + -f Enable "flashers," characters that continuously change. + + -g COLOR Background color (See -c). Defaults to keeping + terminal's current background. + + -h Show this help message and exit + + -i Ignore keyboard + + -l CHARACTER_LIST Select character set(s) using a string over letter + codes (see CHARACTER SETS below.) + + -n Do not use bold characters (overrides -b) + + -o Disable on-screen status + + -s SPEED Integer up to 100. 0 uses a one-second delay before + refreshing, 100 uses none. Use negative numbers for + even lower speeds. Default=85 + + -t TIME Exit the process after TIME seconds + + -u CUSTOM_CHARACTERS Your own string of characters to display. Enclose in + single quotes ('') to escape special characters. For + example: -u '#$(' + + -w Single-wave mode: Does a single burst of green rain, + exits. You can put in a .bashrc file to run when your + terminal launches. Works well with speed at 95. + +LONG ARGUMENTS + -a --asynchronous + -b --all-bold + -c --color=COLOR + -f --flashers + -g --bg-color=COLOR + -h --help + -i --ignore-keyboard + -l --character-list=CHARACTER_LIST + -s --speed=SPEED + -n --no-bold + -o --status-off + -t --time + -u --custom-characters=CUSTOM_CHARACTERS + -w --single-wave + +CHARACTER SETS + When using '-l' or '--character-list=' option, follow it with one or more of + the following letters: + + a Lowercase alphabet + A Uppercase alphabet + c Lowercase Russian Cyrillic alphabet + C Uppercase Russian Cyrillic alphabet + e A few common emoji ( ☺☻✌♡♥❤⚘❀❃❁✼☀✌♫♪☃❄❅❆☕☂★ ) + g Lowercase Greek alphabet + G Uppercase Greek alphabet + k Japanese katakana (half-width) + m Default 'Matrix' set, equal to 'knnssss' + n Numbers 0-9 + o 'Old' style non-unicode set, like cmatrix. Equal to 'AaSn' + p Klingon pIqaD (requires 'Horta' family font)* + P Klingon pIqaD (requires 'Klingon-pIqaD' or 'Code2000' family font)* + r Lowercase Roman numerals ( mcclllxxxxvvvvviiiiii ) + R Uppercase Roman numerals ( MCCLLLXXXXVVVVVIIIIII ) + s A subset of symbols actually used in the Matrix films ( -=*_+|:<>" ) + S All common keyboard symbols ( `-=~!z#$%^&*()_+[]{}|\;':",./<>?" ) + u Custom characters selected using -u switch + + For example: '-l naAS' or '--character-list=naAS' will give something similar + to the output of the original cmatrix program in its default mode. + '-l ACG' will use all the upper-case character sets. Use the same + letter multiple times to increase the frequency of the character set. For + example, the default setting is equal to '-l knnssss'. + + * With most modern Linux terminals (gnome-terminal, konsole, lxterminal, + xfce4-terminal, mate-terminal) simply having the font installed system-wide + is enough. The terminal will fall back to it for the Klingon, meaning that + you don't have to select the font in your terminal settings. 'Horta' seems + not to work in Konsole. Fonts may need to be set manually as fallbacks in + .Xresources for older terminals, such as urxvt and xterm. + +KEYBOARD CONTROL + SPACE, CTRL-c or q exit + - or LEFT decrease speed by 1 + + or RIGHT increase speed by 1 + [ or DOWN decrease speed by 10 + ] or UP increase speed by 10 + a toggle asynchronous scrolling + b cycle through bold character options + (bold off-->bold on-->all bold) + f toggle flashing characters + o toggle on-screen status + 1 to 9 set color: (1) Green (2) Red (3) Blue (4) White + (5) Yellow (6) Cyan (7) Magenta (8) Black + (9) Terminal default + ! to ( set background color (same colors as above, but pressing + shift + number) + +EXAMPLES + Mimic default output of cmatrix (no unicode characters, works in TTY): + $ unimatrix -n -s 96 -l o + + Use the letters from the name of your favorite operating system in bold blue: + $ unimatrix -B -u Linux -c blue + + Use default character set, plus dollar symbol (note single quotes around + special character): + $ unimatrix -l knnssssu -u '$' + + No bold characters, slowly, using emojis, numbers and a few symbols: + $ unimatrix -n -l ens -s 50 +''' + +### Set up parser and apply arguments settings + +parser = argparse.ArgumentParser(add_help=False) + +parser.add_argument('-a', '--asynchronous', + action='store_true', + help='use asynchronous scrolling') +parser.add_argument('-b', '--all-bold', + action='store_true', + help='use all bold characters') +parser.add_argument('-c', '--color', + default='green', + help='one of: green (default), red, blue, white, yellow, \ + cyan, magenta, black', + type=str) +parser.add_argument('-f', '--flashers', + action='store_true', + help='some characters will continuously change in place') +parser.add_argument('-g', '--bg-color', + default='default', + help='background color (see -c)', + type=str) +parser.add_argument('-h', '--help', + help='display extended usage information and exit.', + action='store_true') +parser.add_argument('-i', '--ignore-keyboard', + help='ignore all keyboard input.', + action='store_true') +parser.add_argument('-l', '--character-list', + help='character set. See details below', + type=str) +parser.add_argument('-n', '--no-bold', + action='store_true', + help='do not use bold characters') +parser.add_argument('-o', '--status-off', + action='store_true', + help='Disable on-screen status') +parser.add_argument('-s', '--speed', + help='speed, integer up to 100. Default=85', + default=85, + type=int) +parser.add_argument('-t', '--time', + help='time. See details below', + type=int) +parser.add_argument('-u', '--custom-characters', + help='your own string of characters to display', + default='', + type=str) +parser.add_argument('-w', '--single-wave', + help='runs a single "wave" of green rain then exits', + action='store_true') + +args = parser.parse_args() + +if args.help: + print(help_msg) + exit() + +char_set = { + + 'a': 'qwertyuiopasdfghjklzxcvbnm', + 'A': 'QWERTYUIOPASDFGHJKLZXCVBNM', + 'c': 'абвгдежзиклмнопрстуфхцчшщъыьэюя', + 'C': 'АБВГДЕЖЗИКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ', + 'e': '☺☻✌♡♥❤⚘❀❃❁✼☀✌♫♪☃❄❅❆☕☂★', + 'g': 'αβγδεζηθικλμνξοπρστυφχψως', + 'G': 'ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ', + 'k': 'ヲァィゥェォャュョッーアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン', + 'm': 'ヲァィゥェォャュョッーアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン1234567890' + '1234567890-=*_+|:<>"-=*_+|:<>"-=*_+|:<>"-=*_+|:<>"', + 'n': '1234567890', + 'o': 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890' + '`-=~!@#$%^&*()_+[]{}|\;\':",./<>?"', + 'p': '', + 'P': '', + 'r': 'mcclllxxxxvvvvviiiiii', + 'R': 'MCCLLLXXXXVVVVVIIIIII', + 's': '-=*_+|:<>"', + 'S': '`-=~!@#$%^&*()_+[]{}|\;\':",./<>?"', + 'u': args.custom_characters} + +colors_str = { + 'green': curses.COLOR_GREEN, + 'red': curses.COLOR_RED, + 'blue': curses.COLOR_BLUE, + 'white': curses.COLOR_WHITE, + 'yellow': curses.COLOR_YELLOW, + 'cyan': curses.COLOR_CYAN, + 'magenta': curses.COLOR_MAGENTA, + 'black': curses.COLOR_BLACK, + 'default': -1} + +start_color = colors_str[args.color] +start_bg = colors_str[args.bg_color] + +speed = args.speed +start_delay = (100 - speed) * 10 + +runtime = None + +if args.time: + runtime = args.time + +# "-l" option has been used +if args.character_list: + chars = '' + for letter in args.character_list: + try: + chars += char_set[letter] + except KeyError: + print("Letter '%s' does not represent a valid character list." + % letter) + exit() + +# "-l" not used, but "-u" is set +elif args.custom_characters: + chars = args.custom_characters + +# Neither "-l" nor "-u" has been set, use default characters +else: + chars = char_set['m'] + +if args.no_bold: + args.all_bold = False + +chars_len = len(chars) - 1 + + +### Classes + +class Canvas: + """ + Represents the whole screen and stores its height and width. Gets + overwritten whenever the screen resizes. Serves as a container for columns. + """ + + def __init__(self, screen): + screen.clear() + rows, cols = screen.getmaxyx() + self.col_count = cols + self.row_count = rows + self.size_changed = False + self.columns = [] + for col in range(0, cols, 2): + self.columns.append(Column(col, self.row_count)) + self.nodes = [] + self.flashers = set() + + # Draw a background + for x in range(self.row_count): + try: + screen.addstr(x, 0, ' ' * self.col_count, curses.color_pair(1)) + except curses.error: + pass + + +class Status: + """ + Displays a status message at top left when a setting is changed. + """ + + def __init__(self, screen): + curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_WHITE) + self.screen = screen + self.countdown = 0 + self.last_message = '' + + def update(self, message, delay): + """ + Writes new message to the status area + """ + if not args.status_off: + message_str = message.ljust(11) + self.screen.addstr(0, 0, message_str, curses.color_pair(3)) + self.last_message = message_str + # More frames for faster speeds: + self.countdown = (100 // (delay // 10 + 1)) + 2 + + def refresh(self): + """ + Used to keep refreshing status message until countdown runs out + """ + message_str = self.last_message + self.screen.addstr(0, 0, message_str, curses.color_pair(3)) + + def clear(self): + """ + Erases message with spaces when the countdown runs out + """ + self.screen.addstr(0, 0, ' ' * 11, curses.color_pair(1)) + + +class Column: + """ + Creates nodes (points that move down the screen) that are then stored in + canvas.nodes. Countdown timer determines time to spawn new node. + """ + + def __init__(self, x_coord, row_count): + self.drawing = None # None means not yet. Later will be True or False + self.x_coord = x_coord + self.timer = randint(1, row_count) + self.async_speed = randint(1, 3) + if args.single_wave: + # Speeds it up a bit + self.timer = int(0.6 * self.timer) + + def spawn_node(self, canvas): + """ + Creates nodes: points that move down the screen either writing or + erasing characters as they go down + """ + if args.single_wave and self.drawing is False: + return + + self.drawing = not self.drawing + + # Multiplier (mult) is for spawning slow-moving asynchronous nodes + # less frequently in order to maintain their length + if args.asynchronous: + mult = self.async_speed + else: + mult = 1 + + if self.drawing: + # "max_range" prevents crash with very small terminal height + max_range = max((3 * mult), ((canvas.row_count - 3) * mult)) + self.timer = randint(3 * mult, max_range) + if args.single_wave: + # A bit faster for single wave mode + self.timer = int(0.8 * self.timer) + else: + self.timer = randint(1 * mult, canvas.row_count * mult) + + x = self.x_coord + n_type = 'eraser' + async_speed = self.async_speed + white = False + if self.drawing: + n_type = 'writer' + if randint(0, 2) == 0: + white = True + + canvas.nodes.append(Node(x, n_type, async_speed, white)) + + +class Node: + """ + A point that runs down the screen drawing or erasing characters. + n_type -> 'writer' or 'eraser' + white -> Bool. If True, a white char is written before the green one. + last_char -> Stores last character, since white characters have to be + overwritten with the same one in green one. + expired -> Bool. If True, node is marked for deletion + """ + + def __init__(self, x_coord, n_type, async_speed, white=False): + self.x_coord = x_coord + self.y_coord = 0 + self.n_type = n_type + self.white = white + self.last_char = None + self.expired = False + self.async_speed = async_speed + + +class KeyHandler: + """ + Handles keyboard input. + """ + + def __init__(self, screen, stat): + self.screen = screen + self.stat = stat + self.screen.nodelay(True) + self.delay = start_delay + self.fg = start_color + self.bg = start_bg + + def cycle_bold(self): + """ + Called on 'b' press. Cycles though Bold options: + off -> on -> all bold + """ + if args.all_bold: + args.no_bold = True + args.all_bold = False + self.stat.update('Bold: off', self.delay) + elif args.no_bold: + args.no_bold = False + args.all_bold = False + self.stat.update('Bold: on', self.delay) + else: + args.no_bold = False + args.all_bold = True + self.stat.update('Bold: all', self.delay) + + def get(self): + """ + Handles key presses. Returns True if a key was found, False otherwise. + """ + if args.ignore_keyboard: + return False; + + kp = self.screen.getch() + + if kp == -1: + return False + elif kp == ord(" ") or kp == ord("q") or kp == 27: # 27 = ESC + exit() + elif kp == ord('a'): + args.asynchronous = not args.asynchronous + on_off = 'on' if args.asynchronous else 'off' + self.stat.update('Async: %s' % on_off, self.delay) + elif kp == ord('b'): + self.cycle_bold() + elif kp == ord('f'): + args.flashers = not args.flashers + on_off = 'on' if args.flashers else 'off' + self.stat.update('Flash: %s' % on_off, self.delay) + elif kp == ord('o'): + self.toggle_status() + + # Speed control + elif kp == ord('-') or kp == ord('_') or kp == curses.KEY_LEFT: + self.delay = min(self.delay + 10, 10990) + self.show_speed() + elif kp == ord('=') or kp == ord('+') or kp == curses.KEY_RIGHT: + self.delay = max(self.delay - 10, 0) + self.show_speed() + elif kp == ord('[') or kp == curses.KEY_DOWN: + self.delay = min(self.delay + 100, 10990) + self.show_speed() + elif kp == ord(']') or kp == curses.KEY_UP: + self.delay = max(self.delay - 100, 0) + self.show_speed() + + # Foreground color control + elif kp == ord('1'): + self.set_fg_color('Green') + elif kp == ord('2'): + self.set_fg_color('Red') + elif kp == ord('3'): + self.set_fg_color('Blue') + elif kp == ord('4'): + self.set_fg_color('White') + elif kp == ord('5'): + self.set_fg_color('Yellow') + elif kp == ord('6'): + self.set_fg_color('Cyan') + elif kp == ord('7'): + self.set_fg_color('Magenta') + elif kp == ord('8'): + self.set_fg_color('Black') + elif kp == ord('9'): + self.set_fg_color('default') + + # Background color control + elif kp == ord('!'): + self.set_bg_color('Green') + elif kp == ord('@'): + self.set_bg_color('Red') + elif kp == ord('#'): + self.set_bg_color('Blue') + elif kp == ord('$'): + self.set_bg_color('White') + elif kp == ord('%'): + self.set_bg_color('Yellow') + elif kp == ord('^'): + self.set_bg_color('Cyan') + elif kp == ord('&'): + self.set_bg_color('Magenta') + elif kp == ord('*'): + self.set_bg_color('Black') + elif kp == ord('('): + self.set_bg_color('default') + + return True + + def set_fg_color(self, name): + """ + Set foreground color + """ + self.fg = colors_str[name.lower()] + curses.init_pair(1, self.fg, self.bg) + if name == 'default': + name = "Def't color" + self.stat.update(name, self.delay) + + def set_bg_color(self, name): + """ + Set background color + """ + self.bg = colors_str[name.lower()] + curses.init_pair(1, self.fg, self.bg) + curses.init_pair(2, curses.COLOR_WHITE, self.bg) + self.stat.update('BG: %s' % name, self.delay) + + def show_speed(self): + """ + Display current speed (-999 to 100) when it is changed by keypress + """ + self.stat.update('Speed: %d' % (100 - self.delay // 10), self.delay) + + def toggle_status(self): + """ + On 'o' keypress, turn status display on or off + """ + args.status_off = not args.status_off + on_off = 'off' if args.status_off else 'on' + self.stat.update('Status: %s' % on_off, self.delay) + + +class Writer: + """ + Initializes character writing options and contains methods for writing and + erasing characters from the screen. + """ + + def __init__(self, screen): + self.screen = screen + self.screen.scrollok(0) + curses.curs_set(0) + curses.use_default_colors() + curses.init_pair(1, start_color, start_bg) + curses.init_pair(2, curses.COLOR_WHITE, start_bg) + curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_WHITE) + self.white = curses.color_pair(2) + + @staticmethod + def get_char(): + """ + Returns a random character from the active character set + """ + return chars[randint(0, chars_len)] + + @staticmethod + def get_attr(node, above=False): + """ + Returns either A_BOLD attribute or A_NORMAL based on Bold setting + "above=True" means it an extra green character used to overwrite the + while head character. + """ + if args.no_bold: + return curses.A_NORMAL + elif args.all_bold: + return curses.A_BOLD + else: + if node.white and not above: + return curses.A_BOLD + else: + return choice([curses.A_BOLD, curses.A_NORMAL]) + + def draw(self, node): + """ + Draws characters, included spaces to overwrite/erase characters. + """ + y = node.y_coord + x = node.x_coord + character = ' ' + attr = self.get_attr(node) + color = curses.color_pair(1) + if node.n_type == 'writer': + if not node.white and node.last_char: + # Special green character for overwriting last white one + # at bottom of column that was not being overwritten. + character = node.last_char + else: + character = self.get_char() + if node.white: + color = curses.color_pair(2) + + try: + # Draw the character + self.screen.addstr(y, x, character, color | attr) + if node.white: + if node.last_char: + # If it's a white node, also write a green character above + # to overwrite last white character + attr = self.get_attr(node, above=True) + self.screen.addstr(y - 1, x, node.last_char, + curses.color_pair(1) | attr) + node.last_char = character + except curses.error: + # Override scrolling error if characters pushed off the screen. + pass + + def draw_flasher(self, flasher): + """ + Draws characters, included spaces to overwrite/erase characters. + """ + color = curses.color_pair(1) + attr = choice([curses.A_BOLD, curses.A_NORMAL]) + y = flasher[0] + x = flasher[1] + try: + self.screen.addstr(y, x, self.get_char(), color | attr) + except curses.error: + pass + + +### Main loop + +def _main(screen): + writer = Writer(screen) + stat = Status(screen) + key = KeyHandler(screen, stat) + # Prevent single_wave mode from shutting down too early: + if args.single_wave: + wave_delay = 10 + else: + wave_delay = 0 + + starttime = time.time() + + # Keep restarting however many times the screen resizes + while True: + canvas = Canvas(screen) + # Set a rhythm for asynchronous movement + async_clock = 5 + # Loop to draw the green rain + while not canvas.size_changed: + if runtime and time.time() - starttime > runtime: + exit() + # Catch keypress + if key.get(): + continue + # Spawn new nodes + for col in canvas.columns: + if col.timer == 0: + col.spawn_node(canvas) + col.timer -= 1 + + for node in canvas.nodes: + + if args.flashers: + if node.n_type == 'writer' and not randint(0, 9): + canvas.flashers.add((node.y_coord, node.x_coord)) + elif node.n_type == 'eraser': + try: + canvas.flashers.remove((node.y_coord, node.x_coord)) + except KeyError: + pass + + if args.asynchronous: + if async_clock % node.async_speed == 0: + writer.draw(node) + node.y_coord += 1 + else: + writer.draw(node) + node.y_coord += 1 + + # Mark old nodes for deletion + if node.y_coord >= canvas.row_count: + if node.white: + # Stop white nodes from staying 'stuck' on last row. + # Creates a special green node with a last_char + # attribute to overwrite last white node. + node.white = False + node.y_coord -= 1 + else: + node.expired = True + + if args.flashers and (not async_clock % 3): + for f in canvas.flashers: + writer.draw_flasher(f) + + # Rewrite nodes list without expired nodes + canvas.nodes = [node for node in canvas.nodes if not node.expired] + + if args.single_wave: + if len(canvas.nodes) == 0 and wave_delay < 0: + exit() + wave_delay -= 1 + + # End of loop, refresh screen + if stat.countdown > 0: + if stat.countdown == 1: + stat.clear() + else: + stat.refresh() + stat.countdown -= 1 + screen.refresh() + + # Check for screen resize + if screen.getmaxyx() != (canvas.row_count, canvas.col_count): + canvas.size_changed = True + + # Add delay before next loop + curses.napms(key.delay) + + # update async clock + if async_clock: + async_clock -= 1 + else: + async_clock = 5 + + +def main(): + # Wrapper to allow CTRL-C to exit smoothly: + try: + curses.wrapper(_main) + except KeyboardInterrupt: + pass + + +if __name__ == '__main__': + main()