Recently I realised that one thing that my organisational setup couldn’t really handle was reminders to do things after a certain amount of time from now has elapsed. For example, taking something out of the oven, or getting away from the computer. So I wrote a script for this (which took me until just about the time the first thing I wanted to countdown to was due…). My current version is below; I’ve commented out one line to disable my code for pulling appointments from Emacs.

To use, add this to your crontab:

* * * * * /path/to/srem cron

and to your shell init file

echo $DBUS_SESSION_BUS_ADDRESS > ~/.tmp-dbus-addr

To show your current reminders, use srem print; to add use things like

$ srem 6pm start cooking
$ srem 10m pizza ready
$ srem 1h take a break from the computer

To use the script, change the DB = line to refer to your home directory where you want to store your reminders. There are two other references to /home/swhitton that must be altered to point to your home directory (sorry, I was being lazy when I wrote this).

  #!/usr/bin/python

  import cPickle as pickle
  import sys
  import re
  import datetime
  from dateutil import parser as duparser
  import subprocess
  import os

  global DB
  DB = "/home/swhitton/.srem"

  def main():
      # load and cleanup the dictionary
      try:
          f = open(DB, "rb")
          REMS = pickle.load(f)
          f.close()
          REMS = cleanup(REMS)
      except IOError:
          REMS = {'emacs': [], 'manual': []}

      # decide which of three operation functions to call, and call it.  also check that input is sane
      if len(sys.argv) < 2:
          sys.exit("not enough arguments")
      elif sys.argv[1] == "print":
          for r in REMS['emacs'] + REMS['manual']:
              print r
          sys.exit()
      elif sys.argv[1] == "cron":
          doCron(REMS)
      elif sys.argv[1] == "emacs":
          REMS = doEmacs(REMS)
      elif len(sys.argv) > 2 and re.match(r'[0-9]+[mh]', sys.argv[1]) != None:
          REMS = addCD(REMS, sys.argv[1])
      elif len(sys.argv) > 2 and re.match(r'[0-9]{1,2}(:[0-9][0-9])?(am|pm)?', sys.argv[1]) != None:
          REMS = addT(REMS, duparser.parse(sys.argv[1]).time())
      else:
          sys.exit("invalid arguments")

      # save the dictionary
      REMS['lastrun'] = datetime.datetime.today()
      # print "DEBUG:", REMS
      # print "--- DEBUG ---"
      # for r in REMS['emacs'] + REMS['manual']:
      #     print r
      # print "setting last run time to yesterday"
      # REMS['lastrun'] = REMS['lastrun'].replace(day = 16)
      # print "DEBUG:", REMS
      pickle.dump(REMS, open(DB, "wb"))

  def cleanup(rems):
      # cleanup required on the first run of the day
      # delta = datetime.datetime.today() - rems['lastrun']
      # if delta.days > 0:
      if not (datetime.datetime.today().day == rems['lastrun'].day):
          rems['manual'] = []
          #rems = doEmacs(rems)

      # don't actually need to clear out the list except at the start of the # day, and anyway the below code doesn't work

      # else:                    # now do the stuff required on normal runs
      #     now = datetime.datetime.today()
      #     date = datetime.date.today()
      #     rems['manual'] = [i for i in rems['manual']
      #                       if (now - datetime.datetime.combine(date, i[0])).total_seconds() < 0]

      return rems

  def doCron(rems):
      rems = rems['emacs'] + rems['manual']
      now = datetime.datetime.today().time().replace(second = 0, microsecond = 0)
      hour = now.hour
      minute = now.minute
      rems = [rem[1] for rem in rems
              if rem[0].hour == hour and rem[0].minute == minute]

      zenerr = open('/tmp/zenityerr', 'a')

      # make sure zenity can get at dbus to send its notification
      dbf = open('/home/swhitton/.tmp-dbus-addr', 'r')
      dbv = dbf.readline()
      dbf.close()
      os.environ['DBUS_SESSION_BUS_ADDRESS'] = dbv

      for rem in rems:
          subprocess.Popen(['/usr/bin/zenity',
                            '--notification',
                            '--text=' + rem,
                            '--display=:0.0'], stderr = zenerr, env = os.environ)
          subprocess.call(['/usr/bin/aplay', '/home/swhitton/lib/annex/sounds/beep.wav'])
      zenerr.close()

  def doEmacs(rems):
      dn = open('/dev/null', 'a')
      sp = subprocess.Popen(["/usr/bin/emacs", "-batch",
                             "-l", "/home/swhitton/.emacs.d/init.el",
                             "-eval", "(setq org-agenda-sticky nil)",
                             "-eval", "(org-batch-agenda \"D\")"],
                               stdout = subprocess.PIPE,
                               stderr = dn)
      dn.close()

      remindersAt = [60, 15, 0]
      rems['emacs'] = []
      # f = open('/tmp/diary.txt', 'r')
      f = sp.stdout
      f.readline()
      f.readline()
      r = re.compile(r' ([0-9]{1,2}:[0-9][0-9])[-]{0,1}[0-9:]{0,5}[.]* (.*)$')
      for line in f:
          if line[0] != " ":         # break once we're onto the next day
              break
          match = r.search(line)
          if match == None:
              continue            # ignore the line if no match
          dt = duparser.parse(match.group(1))
          desc = match.group(2)
          hour = str(int(dt.strftime("%I")))
          if dt.minute == 0:
              minute = ""
          else:
              minute = ":" + dt.strftime("%M")
          ampm = dt.strftime("%p").lower()
          string = hour + minute + ampm + " " + desc
          for interval in remindersAt:
              rems['emacs'].append([(dt - datetime.timedelta(minutes = interval)).time(),
                                    string])
          # rems['emacs'].append([dt.time(),
          #                       string])
      return rems

  def addCD(rems, offset):
      if offset[-1:] == 'h':
          offset = 60 * int(offset[:-1])
      else:
          offset = int(offset[:-1])
      clock = datetime.datetime.today() + datetime.timedelta(minutes = offset)
      clock = clock.time()
      return addT(rems, clock)

  def addT(REMS, clock):
      # try:
      #     time = datetime.time(int(time[0:2]), int(time[2:]))
      # except ValueError:
      #     sys.exit("invalid time")
      clock = clock.replace(second = 0, microsecond = 0)
      # print clock

      # check that the given time is not in the past.  If it is,
      # silently fail to add the time
      if not(clock < datetime.datetime.today().time()):
          REMS['manual'].append([clock, ' '.join(sys.argv[2:])])

      return REMS

  if __name__ == "__main__":
      main()

Requires the python dateutil library; the Debian package is python-dateutil. Requires zenity to pop-up the notifications.