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
 |