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.