#!/usr/bin/env bash
# stim - simple timer
# xmpp:[email protected]
# mailto:[email protected]
# ------------------------------------------------------------------- #
shopt -s extglob

# Defaults: set timer to one minute.
c=1     # Count to set.
u=60    # Unit to use, in seconds.

PROGNAME="${0##*/}"

# Usage
print_usage() {
 read -rd '' text << _EOF
 ${PROGNAME} - simple timer
 usage: ${PROGNAME} [options] [count] [message]

 description:
   stim will wait for desired duration, and then notify the user by
   flashing the terminal until it reads a keypress.

 options:
   -h          : print this help message
   -c          : count to use
   -q          : disable time end notification
   -u {unit}   : unit to use. sec/min/hour/(number of seconds)
   -e {string} : evaluate provided command string when time runs out

 examples:
   set timer to one minute, notify after:
     stim # it's the default behaviour

   set timer to 2 hours, display "call sean" after:
     stim -u h 2 call sean

   set timer to 30 seconds, don't notify but play testospin after:
     stim -q -u sec -e "cmus-remote -f ~/testospin.ogg" 30

   same as above, but using different syntax and the && shell operand:
     stim -qu 1 30 && cmus-remote -f ~/testospin.ogg

 notes:
   With some caveats, stim can be backgrounded with standard shell
   features like the & operand or the SIGTSTP signal commonly bound
   to Ctrl+Z.
   If you're stuck with a flashing screen and pressing keys doesn't
   stop it, you're likely looking to foreground the stim job using
   the `fg` shell command.
_EOF
 echo "${text}"
}

# Parse options. Takes $@, returns a number.
# Do shift $number and the $@ becomes just the arguments.
parse_options() {
 while getopts 'hqu:e:c:' opt
 do
   case "${opt}" in
     h)
       print_usage
       exit
     ;;
     c)
             if is_number "${OPTARG}"
             then
                     c="${OPTARG}" ; skiparg[c]=1
             else
               echo "${PROGNAME} options error: ${OPTARG} is not a number" >&2
               exit 31
             fi
           ;;
     u)
       case "${OPTARG}" in
               s*) u=1 ;;
               m*) u=60 ;;
               h*) u=3600 ;;
         +([0-9])) u="${OPTARG}";;
                *) print_usage >&2 ; exit 2 ;;
       esac
     ;;
     e)
       e_string="${OPTARG}"
     ;;
     q)
             quiet=1
           ;;
     *)
       echo "internal error" >&2
       exit 3
     ;;
   esac
 done
 return $(( OPTIND - 1 ))
}

# Helpers
is_number() {
       local j
       for j
       do
       [[ "${j}" =~ ^[0-9]+$ ]] || return 1
       done
       unset j
}

# Invoked when time runs out.
notify() {
 echo "${1:-${PROGNAME}: ${s}s has passed, timer ends}"
 until read -rn 1 -t 0.5
 do
   tput flash; echo -ne "\a"; sleep 0.1
   tput flash; echo -ne "\a"
 done
 return 0
}

# Takes count and units as arguments
# Prints remaining time every passing unit
wait_count() {
       is_number "${@}" || exit 4
       local i=${1}
       until (( --i < 0 ))
       do
               sleep ${2}
   ((  quiet   )) || echo $(( i*u ))
       done
       unset i
}

# Main runtime
main() {
 parse_options "${@}"
 shift ${?}

 # If a number is provided, set the count.
 if [[ ${1} =~ ^[0-9]+$ ]] && ! (( skiparg[c] ))
 then
         c=${1}
         shift
 fi

 # DEBUG / Some feedback
 echo "Unit $u Count $c" >&2
 echo "Timer set to $(( c*u )) seconds" >&2

 # Time to... time
 if wait_count ${c} ${u}
 then
   (( e_string )) && eval "${e_string}"
   ((  quiet   )) || notify "${*}"
 else
   echo "${PROGNAME}: I can't sleep" >&2
   exit 1
 fi
}

# ------------------------------------------------------------------- #
main "${@}"

### Wishlist of features:
# exec something on time end ( DONE )
# quiet mode ( DONE )
#
# pausing - do this with trapping signals or something
# relevant:
# - help jobs
# - help [fb]g
# - stty -a
# - trap
# - ^Z doesn't really work now for pausing but should if we
#   `sleep 1` 60 times instead of `sleep 60` once. It would also
#   allow for a countdown display.
#
# backgrounding - on the other hand, ^Z is perfect for backgrounding
# with the `sleep 60` implementation, except it won't let the notify
# fire.
#
# more on sleep 60 vs sleep 1 implementation details:
# - several sleeps give more meaning to unit and count definitions