dotfiles/.local/bin/muccadoro
2020-07-04 14:23:27 +02:00

305 lines
9.2 KiB
Bash
Executable File

#!/usr/bin/env bash
set -o nounset -o pipefail
# Before doing anything, check if the required programs are installed. See
# <https://stackoverflow.com/q/592620>. Some of these can probably be reasonably assumed
# to be present, but err on the side of caution.
for prog in awk cowsay figlet notify-send stty tput; do
if ! hash "$prog" 2>/dev/null; then
printf 'muccadoro: %s: command not found\n' "$prog" >&2
exit 127
fi
done
temp_loc=/tmp/pomptemptimes
temp_timeloc=/tmp/mucc_time
statusbar="dwmblocks"
update_signal="3"
freetime=${2:-w}
if [ "$freetime" = "f" ]; then
temp_loc=/tmp/pomptemptimesft
fi
declare -i silly=1
# http://mywiki.wooledge.org/BashFAQ/035#getopts
# http://wiki.bash-hackers.org/howto/getopts_tutorial
while getopts ':s' opt; do
case $opt in
s) (( ++silly ));;
esac
done
shift "$((OPTIND-1))" # Shift off the options and optional --.
# One pomodoro lasts "$1" minutes. The default duration is 25 minutes.
declare -i duration=$((${1:-25}*60)) num_pomodoros=4
(( silly %= 5 ))
if (( silly )); then
declare -i silliness=$((2**(4-silly)))
# `apps` stands for appearances, of course.
declare -a apps=('' '-b' '-d' '-g' '-p' '-s' '-t' '-w' '-e oO' '-e Oo' '-e ><' '-e -o'
'-e o-' '-e >o' '-e o<')
num_apps=${#apps[@]}
cowtell() {
app_num=$((RANDOM % (silliness * num_apps)))
(( app_num >= num_apps )) && app_num=0
cowsay -n ${apps[app_num]}
}
else
cowtell() {
cowsay -n
}
fi
summary=
# Standard output must be a terminal. See <https://unix.stackexchange.com/q/91638>. Save
# the original stdout to file descriptor 3 (see <https://unix.stackexchange.com/q/80988>).
exec 3>&1 &>/dev/tty
# Save the current terminal settings.
initial_tty_settings=$(stty -g)
# Revert all changed terminal settings (FIXME: restore everything from saved settings) and
# print a summary.
cleanup() {
tput rmcup
tput cnorm
stty "$initial_tty_settings"
[[ $summary ]] && echo -ne "$summary" >&3
rm -f $temp_loc
rm -f $temp_timeloc
pkill -RTMIN+$update_signal $statusbar
}
trap cleanup EXIT
# Switch to the alternate screen. See <https://unix.stackexchange.com/q/27941>, xterm(1),
# terminfo(5), and <https://stackoverflow.com/q/11023929>.
tput smcup
# TODO: explain. See
# <http://www.unix.com/shell-programming-and-scripting/176837-bash-hide-terminal-cursor.html>.
tput civis
# Don't echo characters typed on the tty. See <https://unix.stackexchange.com/a/28620>.
stty -echo
# Output empty lines before the message so the message is displayed at the bottom of the
# terminal. See <https://stackoverflow.com/a/29314659>. Also, instead of `clear`ing
# (which causes flickering), pad all lines of the message with spaces all the way to the
# right edge of the terminal, thereby overwriting any currently displayed characters. See
# <https://stackoverflow.com/questions/9394408>. TODO: probably just use Bash and not
# awk.
pad() {
awk -v lines="$(tput lines)" -v cols="$(tput cols)" '
NR!=1 && FNR==1 { n=lines-NR; for(; n>0; n--) printf "%-"cols"s\n", "" }
NR==FNR { next }
{ printf "%-"cols"s\n", $0 }' <(echo "$1"){,}
}
pp() {
tput cup 0 0 # TODO: explain.
pad "$1"
}
ppp() {
tput cup 0 0
# FIXME: probably just check once if we have lolcat.
pad "$1" | { lolcat 2>/dev/null || cat; }
}
declare -a lyrics
declare -i line_index=0
lyrics=(
"Can't stop, addicted to the shindig;"
"Chop Top, he says I'm gonna win big;"
"Choose not a life of imitation;"
"Distant cousin to the reservation;"
"Defunct the pistol that you pay for;"
"This punk, the feeling that you stay for;"
"In time I want to be your best friend;"
"East side lovers living on the west end;"
"Knocked out but boy you better come to;"
"Don't die, you know the truth as some do;"
"Go write your message on the pavement;"
"Burn so bright I wonder what the wave meant;"
)
declare -i state=0
cant-stop() {
(( state == 2 )) && return
state=2
tty_settings=$(stty -g)
trap '' INT
stty susp undef
pp "$(cowsay -e '><' -W $(($(tput cols)-3)) ${lyrics[line_index]})"
((++line_index)); ((line_index%=${#lyrics[@]}))
sleep 2 & wait $!
stty "$tty_settings"
count-state
}
# SIGTSTP handler.
on-tstp() {
# Signal all processes in the process group $$ (the group leader) to continue. See
# kill(1), and <https://unix.stackexchange.com/q/139222>. Pomodoros are not
# interruptible.
kill -CONT -- -$$
if (( state == 1 )); then
cant-stop
fi
}
trap on-tstp TSTP
count-state() {
# 130 is the exit status for termination by Ctrl-C. See
# <http://www.tldp.org/LDP/abs/html/exitcodes.html>.
trap 'trap on-int INT; on-int; return 130' INT
state=1
}
dead-state() {
trap on-int INT
state=0
}
pause-state() {
trap on-int INT
state=0
}
on-int() {
if (( state==0 )); then
# We are supposed to kill ourselves with SIGINT instead of using `exit`. See
# <http://mywiki.wooledge.org/SignalTrap#Special_Note_On_SIGINT>.
trap - INT
kill -INT $$
elif (( state==1 )); then
dead-state
elif (( state==2 )); then
count-state
fi
}
# XXX: beware of bugs due to SIGINT (Ctrl-C) being received during the short timeframe in
# which another function invoked by this one is executing. The `return 1` statement of
# the SIGINT trap will be ran in the context of the inner function.
pomodoro() {
count-state
while :; do
# Handle signals immediately, not after `sleep` exits. See
# <http://mywiki.wooledge.org/SignalTrap#When_is_the_signal_handled.3F>.
sleep 1 &
# See <http://mywiki.wooledge.org/BashFAQ/002>.
planned_end_time=$(( $start_time_secs + $duration ))
seconds=$(( $planned_end_time - $( date +'%s') ))
the_time=$((seconds/60)):$(printf '%02d' $((seconds%60)))
# Keep in mind that almost everything causes new values to be assigned to `$?`:
# $ false
# $ (( $? )) && echo $?
# 0
# $ false || { (( $? != 148 )) && echo $?; }
# 0
# In both cases, when `echo $?` is executed, `$?` is no longer 1.
fail=$?
if (( fail && fail != 148 )); then
return $fail
fi
text=$(figlet -f small "$the_time" ) #remove | cowtell
fail=$?
if (( ! fail )); then
pp "$text"
fail=$?
echo "$the_time" > $temp_timeloc
pkill -RTMIN+$update_signal $statusbar
(( fail && fail != 148 )) && return $fail
elif (( fail != 148 )); then
return $fail
fi
wait
((--seconds <= 0)) && return 0
done
return 1
}
flush-stdin() {
# See <https://superuser.com/q/276531>.
read -r -d '' -t 0.1 -n 1000
}
# FIXME: why `dummy` (http://wiki.bash-hackers.org/commands/builtin/read#press_any_key).
pause() {
# See <http://wiki.bash-hackers.org/syntax/pe#use_an_alternate_value>.
read -r -n 1${1:+ -t $1}
}
for (( n=1; n<=num_pomodoros; ++n )); do
declare -i seconds=$duration
declare -i start_time_secs=$(date +'%s')
start_time=$(date --date "@$start_time_secs" +'%H:%M')
pomodoro
fail=$?
if (( fail == 130 )); then
end_time=$(date +'%H:%M')
end_time_secs=$(date -d $end_time +'%s')
day=$(date '+%Y%b%d')
summary+="Abandoned: $start_time to $end_time ($((($end_time_secs - $start_time_secs)/ 60))) $day\n"
#summary+="Abandoned: $start_time to $(date +'%H:%M')\n"
# Pomodoros are atomic.
pp "$(cowsay -d -W $(($(tput cols)-3)) 'You abandoned pomodoro '$n'. Press any' \
'key to restart it.')"
rm -f $temp_timeloc
pkill -RTMIN+$update_signal $statusbar
pause
(( --n ))
continue
elif (( fail )); then
exit $fail
fi
pause-state
tty_settings=$(stty -g)
stty susp undef
end_time_secs=$(date +'%s')
end_time=$(date --date "@$end_time_secs" +'%H:%M')
#end_time_secs=$(date -d $end_time +'%s')
#start_time_secs=$(date -d $start_time +'%s')
day=$(date '+%Y%b%d')
summary+="Pomodoro $n: $start_time to $end_time ($(( ($end_time_secs - $start_time_secs )/60))) $day\n"
#summary+="Pomodoro $n: $start_time to $(date +'%H:%M') \n"
if (( n!=num_pomodoros )); then
start_time=$(date +'%s')
notify-send "You completed pomodoro $n. Take a short break (3-5 minutes)."
#
#
echo "($n*$duration)/60" | bc > $temp_loc
pkill -RTMIN+$update_signal $statusbar
# TODO: it may be nice to create this message asynchronously with `lolcat -f` since
# lolcat is a bit slow. That's not a priority, though.
ppp "$(cowsay -e '^^' -W $(($(tput cols)-3)) 'You completed pomodoro '$n'. Take' \
'a short break (3-5 minutes), then press any key to continue.')"
rm -f $temp_timeloc
pkill -RTMIN+$update_signal $statusbar
flush-stdin
if ! pause 180; then
pp "$(cowsay -w -W $(($(tput cols)-3)) 'Press any key to continue.')"
pause 120 || {
notify-send -u critical 'Time to start the next pomodoro.'; pause;
}
fi
break_duration=$((($(date +'%s')-start_time+30)/60))
summary+="Break: about $break_duration minute"
(( break_duration != 1 )) && summary+=s # plural
summary+='\n'
fi
stty "$tty_settings"
done
notify-send "You completed all $num_pomodoros pomodoros!"
# vim: tw=90 sts=-1 sw=3 et