#!/usr/bin/env bash set -o nounset -o pipefail # Before doing anything, check if the required programs are installed. See # . 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 . Save # the original stdout to file descriptor 3 (see ). 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 , xterm(1), # terminfo(5), and . tput smcup # TODO: explain. See # . tput civis # Don't echo characters typed on the tty. See . stty -echo # Output empty lines before the message so the message is displayed at the bottom of the # terminal. See . 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 # . 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 . 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 # . 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 # . 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 # . sleep 1 & # See . 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 . read -r -d '' -t 0.1 -n 1000 } # FIXME: why `dummy` (http://wiki.bash-hackers.org/commands/builtin/read#press_any_key). pause() { # See . 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