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
 |