305 lines
9.2 KiB
Bash
Executable File
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
|