dotfiles

custom linux config files managed with gnu stow

dotfiles

fun/bin/bonsai


#!/usr/bin/env bash

# I'm a bonsai-making machine!

#################################################
##
# author: John Allbritten
# my website: theSynAck.com
#
# repo: https://gitlab.com/jallbrit
#  script can be found in the bin/bin/fun folder.
#
# license: this script is published under GPLv3.
#  I don't care what you do with it, but I do ask
#  that you leave this message please!
#
# inspiration: http://andai.tv/bonsai/
#  andai's version was written in JS and served
#  as the basis for this script. Originally, this
#  was just a port.
##
#################################################

# ------ vars ------
# CLI options

flag_h=false
live=false
infinite=false

termCols=$(tput cols)
termRows=$(tput lines)
geometry="$((termCols - 1)),$termRows"

leafchar='&'
termColors=false

message=""
flag_m=false
basetype=1
multiplier=5

lifeStart=28
steptime=0.01	# time between steps 

# non-CLI options
lineWidth=4	# words per line

# ------ parse options ------

OPTS="hlt:ig:c:Tm:b:M:L:"	# the colon means it requires a value
LONGOPTS="help,live,time:,infinite,geo:,leaf:,termcolors,message:,base:,multiplier:,life:"

parsed=$(getopt --options=$OPTS --longoptions=$LONGOPTS -- "$@")
eval set -- "${parsed[@]}"

while true; do
	case "" in
		-h|--help)
			flag_h=true
			shift
			;;

		-l|--live)
			live=true
			shift
			;;

		-t|--time)
			steptime=""
			shift 2
			;;

		-i|--infinite)
			infinite=true
			shift
			;;

		-g|--geo)
			geo=
			shift 2
			;;

		-c|--leaf)
			leafchar=""
			shift 2
			;;

		-T|--termcolors)
			termColors=true
			shift
			;;

		-m|--message)
			flag_m=true
			message=""
			shift 2
			;;

		-b|--basetype)
			basetype=""
			shift 2
			;;

		-M|--multiplier)
			multiplier=""
			shift 2
			;;

		-L|--life)
			lifeStart=""
			shift 2
			;;

		--) # end of arguments
			shift
			break
			;;

		*)
			echo "error while parsing CLI options"
			flag_h=true
			;;
	esac
done

HELP="Usage: bonsai [-h] [-i] [-l] [-T] [-m message] [-t time] 
              [-g x,y] [ -c char] [-M 0-9]

bonsai.sh is a static and live bonsai tree generator, written in bash.

optional args:
  -l, --live             enable live generation
  -t, --time time        time between each step of growth [default: 0.01]
  -m, --message text     attach a message to the tree
  -b, --basetype 0-2     which ascii-art plant base to use (0 for none) [default: 1]
  -i, --infinite         keep generating trees until quit (2s between each)
  -T, --termcolors       use terminal colors
  -g, --geo geo          set custom geometry [default: fit to terminal]
  -c, --leaf char        character used for leaves [default: &]
  -M, --multiplier 0-9   branch multiplier; higher equals more branching [default: 5]
  -L, --life int         life of tree; higher equals more overall growth [default: 28]
  -h, --help             show help"

# check for help
$flag_h && echo -e "$HELP" && exit 0

# geometry processing
cols=$(echo "$geometry" | cut -d ',' -f1)	# width; X
rows=$(echo "$geometry" | cut -d ',' -f2)	# height; Y

IFS=$'\n'	# delimit strings by newline
tabs 4 		# set tabs to 4 spaces

declare -A gridMessage

# message processing
if [ $flag_m = true ]; then

	messageWidth=20

	# make room for the message to go on the right side
	cols=$((cols - messageWidth - 8 ))

	# wordwrap message, delimiting by spaces
	message="$(echo "$message" | fold -sw $messageWidth)"
	
	# get number of lines in the message
	messageLineCount=0
	for line in $message; do
		messageLineCount=$((messageLineCount + 1))
	done

	messageOffset=$((rows - messageLineCount - 7))

	# put lines of message into a grid
	index=$messageOffset
	for line in $message; do
		gridMessage[$index]="$line"
		index=$((index + 1))
	done
fi

# define colors
if [ $termColors = true ]; then
	LightBrown='\e[1;33m'
	DarkBrown='\e[0;33m'
	BrownGreen='\e[1;32m'
	Green='\e[0;32m'
else
	LightBrown='\e[38;5;172m'
	DarkBrown='\e[38;5;130m'
	BrownGreen='\e[38;5;142m'
	Green='\e[38;5;106m'
fi
Grey='\e[1;30m'
R='\e[0m'

# create ascii base in lines
base=""
case $basetype in
	0)
		base="" ;;
	
	1)
		width=15
		art="\
${Grey}:${Green}___________${DarkBrown}./~~\.${Green}___________${Grey}:
 \                          /
  \________________________/
  (_)                    (_)"
		;;

	2)
		width=7
		art="\
${Grey}(${Green}---${DarkBrown}./~~\.${Green}---${Grey})
 (          )
  (________)"
		;;
esac

# get base height
baseHeight=0
for line in $art; do
	baseHeight=$(( baseHeight + 1 ))
done

# add spaces before base so that it's in the middle of the terminal
iter=1
for line in $art; do
	filler=''
	for (( i=0; i < $(( (cols / 2) - width )); i++)); do
		filler+=" "
	done
	base+="${filler}${line}"
	[ $iter -ne $baseHeight ] && base+='\n'
	iter=$((iter+1))
done	
unset IFS	# reset delimiter

rows=$((rows - baseHeight))

declare -A grid	# must be done outside function for unknown reason

trap 'echo "press q to quit"' SIGINT	# disable CTRL+C

init() {
	branches=0
	shoots=0

	branchesMax=$((multiplier * 110))
	shootsMax=$multiplier

	# fill grid full of spaces
	for (( row=0; row < $rows; row++ )); do
		for (( col=0; col < $cols; col++ )); do
			grid[$row,$col]=' '
		done
	done

	# No echo stdin and hide the cursor
	if [ $live = true ]; then
		stty -echo
		echo -ne "\e[?25l"

	 	echo -ne "\e[2J"
	fi
}

grow() {
	local start=$((cols / 2))

	local x=$((cols / 2))		# start halfway across the screen
	local y=$rows	# start just above the base

	branch $x $y trunk $lifeStart
}

branch() {
	# argument declarations
	local x=
	local y=
	local type=
	local life=
	local dx=0
	local dy=0

	# check if the user is hitting q
	timeout=0.001
	[ $live = "false" ] && timeout=.0001
	read -n 1 -t $timeout input
	[ "$input" = "q" ] && clean "quit"

	branches=$((branches + 1))

	# as long as we're alive...
	while [ $life -gt 0 ]; do
		
		life=$((life - 1))	# ensure life ends

		# case $life in
		# 	[0]) type=dead ;;
		# 	[1-4]) type=dying ;;
		# esac

		# set dy based on type
		case $type in
			shoot*)	# if this is a shoot, trend horizontal/downward growth
				case "$((RANDOM % 10))" in
					[0-1]) dy=-1 ;;
					[2-7]) dy=0 ;;
					[8-9]) dy=1 ;;
				esac
				;;

			dying) # discourage vertical growth
				case "$((RANDOM % 10))" in
					[0-1]) dy=-1 ;;
					[2-8]) dy=0 ;;
					[9-10]) dy=1 ;;
				esac
				;;
				
			*)	# otherwise, let it grow up/not at all
				dy=0
				[ $life -ne $lifeStart ] && [ $((RANDOM % 10)) -gt 2 ] && dy=-1
				;;
		esac
		# if we're about to hit the ground, cut it off
		[ $dy -gt 0 ] && [ $y -gt $(( rows - 1 )) ] && dy=0
		[ $type = "trunk" ] && [ $life -lt 4 ] && dy=0

		# set dx based on type
		case $type in
			shootLeft)	# tend left: dx=[-2,1]
				case $(( RANDOM % 10 )) in
					[0-1]) dx=-2 ;;
					[2-5]) dx=-1 ;;
					[6-8]) dx=0 ;;
					[9]) dx=1 ;;
				esac ;;

			shootRight)	# tend right: dx=[-1,2]
				case $(( RANDOM % 10 )) in
					[0-1]) dx=2 ;;
					[2-5]) dx=1 ;;
					[6-8]) dx=0 ;;
					[9]) dx=-1 ;;
				esac ;;

			dying)	# tend left/right: dx=[-3,3]
				dx=$(( (RANDOM % 7) - 3)) ;;

			*)	# tend equal: dx=[-1,1]
				dx=$(( (RANDOM % 3) - 1)) ;;

		esac

		# re-branch upon conditions
		if [ $branches -lt $branchesMax ]; then
			
			# branch is dead
			if [ $life -lt 3 ]; then
				branch $x $y dead $life

			# branch is dying and needs to branch into leaves
			elif [ $type = trunk ] && [ $life -lt $((multiplier + 2)) ]; then
				branch $x $y dying $life

			elif [[ $type = "shoot"* ]] && [ $life -lt $((multiplier + 2)) ]; then
				branch $x $y dying $life

			# re-branch if: not close to the base AND (pass a chance test OR be a trunk, not have too man shoots already, and not be about to die)
			elif [[ $type = trunk && $life -lt $((lifeStart - 8)) \
			&& ( $(( RANDOM % (16 - multiplier) )) -eq 0 \
			|| ($type = trunk && $(( life % 5 )) -eq 0 && $life -gt 5) ) ]]; then

				# if a trunk is splitting and not about to die, chance to create another trunk
				if [ $((RANDOM % 3)) -eq 0 ] && [ $life -gt 7 ]; then
					branch $x $y trunk $life

				elif [ $shoots -lt $shootsMax ]; then

					# give the shoot some life
					tmpLife=$(( life + multiplier - 2 ))
					[ $tmpLife -lt 0 ] && tmpLife=0

					# first shoot is randomly directed	
					if [ $shoots -eq 0 ]; then 
						tmpType=shootLeft
						[ $((RANDOM % 2)) -eq 0 ] && tmpType=shootRight


					# secondary shoots alternate from the first
					else
						case $tmpType in
							shootLeft) # last shoot was left, shoot right
								tmpType=shootRight ;;
							shootRight) # last shoot was right, shoot left
								tmpType=shootLeft ;;
						esac
					fi
					branch $x $y $tmpType $tmpLife
					shoots=$((shoots + 1))
				fi
			fi
		else # if we're past max branches but want to branch...
			char='<>'
		fi

		# implement dx,dy
		x=$((x + dx))
		y=$((y + dy))
		
		# choose color
		case $type in
			trunk|shoot*)
				color=${DarkBrown} 
				[ $(( RANDOM % 4 )) -eq 0 ] && color=${LightBrown}
				;;

			dying) color=${BrownGreen} ;;

			dead) color=${Green} ;;
		esac

		# choose branch character
		case $type in
			trunk)
				if [ $dx -lt 0 ]; then
					char='\'
				elif [ $dx -eq 0 ]; then
					char='/|'
				elif [ $dx -gt 0 ]; then
					char='/'
				fi
				[ $dy -eq 0 ] && char='/~'	# not growing
				#[ $dy -lt 0 ] && char='/~'	# growing
				;;

			# shoots tend to look horizontal
			shootLeft)
				case $dx in
					[-3,-1]) 	char='\|' ;;
					[0]) 		char='/|' ;;
					[1,3]) 		char='/' ;;
				esac
				#[ $dy -lt 0 ] && char='/~'	# growing up
				[ $dy -gt 0 ] && char='/'	# growing down
				[ $dy -eq 0 ] && char='\_'	# not growing
				;;

			shootRight)
				case $dx in
					[-3,-1]) 	char='\|' ;;
					[0]) 		char='/|' ;;
					[1,3]) 		char='/' ;;
				esac
				#[ $dy -lt 0 ] && char=''	# growing up
				[ $dy -gt 0 ] && char='\'	# growing down
				[ $dy -eq 0 ] && char='_/'	# not growing
				;;

			#dead)
			#	#life=$((life + 1))
			#	char="${leafchar}"	
			#	[ $dx -lt -2 ] || [ $dx -gt 2 ] && char="${leafchar}${leafchar}"
			#	;;

			esac

		# set leaf if needed
		[ $life -lt 4 ] && char="${leafchar}"

		# uncomment for help debugging
		#echo -e "$life:\t$x, $y: $char"
		
		# put character in grid
		grid[$y,$x]="${color}${char}${R}"

		# if live, print what we have so far and let the user see it
		if [ $live = true ]; then
			print
			sleep $steptime
		fi
	done	
}

print() {
	# parse grid for output
	output=""
	for (( row=0; row < $rows; row++)); do

		line=""

		for (( col=0; col < $cols; col++ )); do

			# this prints a space at 0,0 and is necessary at the moment
			[ $live = true ] && echo -ne "\e[0;0H "

			# grab the character from our grid
			line+="${grid[$row,$col]}"
		done

		# add our message
		if [ $flag_m = true ]; then
			# remove trailing whitespace before we add our message
			line=$(sed -r 's/[ \t]*$//' <(printf "$line"))
			line+="   \t${gridMessage[$row]}"
		fi

		line="${line}\n"

		# end 'er with the ol' newline
		output+="$line"
	done

	# add the ascii-art base we generated earlier
	output+="$base"
	
	# output, removing trailing whitespace
	sed -r 's/[ \t]*$//' <(printf "$output")
}

clean() {
	# Show cursor and echo stdin
	if [ $live = true ]; then
		echo -ne "\e[?25h"
		stty echo
	fi

	echo ""	# ensure the cursor resets to the next line

	# if we wanna quit
	if [ "" = "quit" ]; then
		trap SIGINT	
		exit 0
	fi
}

bonsai() {
	init
	grow
	print
	clean
}

bonsai

while [ $infinite = true ]; do
	sleep 2
	bonsai
done

Download

raw zip tar