I use a calendar to schedule various tasks and I always wanted a desktop popup for due tasks with the ability to add the tasks immediately to a todo list, so when I acknowledge the notification, the popup disappears and the tasks is added to my todo items. Furthermore, I like to see a permanent indicator on the screen if I have pending todo items.
That's what this script does. It's pretty much does what I want and I don't plan many more improvements, so I put it here as is, if someone's interested.
The script also provides an effortless way to add new items to the calendar using
the Quick Add syntax of Google Calendar. The Quick Add format is so easy and effortless to use that I even use it to schedule trivial tasks at home for which I wouldn't use a calendar otherwise. E.g. "walk the dog 5pm"
The script is completely keyboard driven:
pause shows the todo list if there are items.
pause again shows the quick add dialog which can be used to add calendar items or regular todo items
in the quickadd dialog enter adds the typed item, esc cancels the dialog. It will be a calendar item if the last word contains a number (e.g. "do something 4pm" - see the quickadd syntax), otherwise it will be a regular todo item. In case of a calendar item if the item text contains the word "silent" then no msgbox is shown when the entry is due and it is added silently to the todo list.
delete deletes items from the list.
insert can be used to quickly reschedule an exitsing item, so it disappears from the list until its time comes again.
If the script is called with a parameter then it fetches new items from the calendar when started. It also fetches new items every time when a new calendar item is added.
Code:
datadir = %A_AppData%\gcalahk
listfile = %datadir%\list
incomingfile = %datadir%\incoming
configfile = %datadir%\config
max_list_backups = 10
gui2_w = 40
gui2_h = 40
gui2_bgcolor = 99FF00
Gui 2: +LastFound -Caption +AlwaysOnTop +ToolWindow
Gui 2: Font, s20
Gui 2: Color, %gui2_bgcolor%
Gui 2: Add, Text, vCount, 99
SysGet, Monitor, Monitor
gui2_x := MonitorRight * 2 / 3
Gui 2: show, w%gui2_w% h%gui2_h% x%gui2_x% y0
Gui 2: hide
GuiControlGet Count, 2:Pos
countx := (gui2_w - countw) / 2
county := (gui2_h - counth) / 2
GuiControl 2: Move, Count, x%countx% y%county%
ifnotexist %datadir%
{
FileCreateDir %datadir%
if (errorlevel <> 0)
{
msgbox Cannot create data directory: %datadir%
exitapp
}
}
ifnotexist %configfile%
{
MsgBox Please create the config file: %configfile%
exitapp
}
Gui +LastFound -Caption +ToolWindow
Gui, Font, s20
Gui, Color, 87CEFA
Gui, Add, ListBox, w600 h400 vTodoList AltSubmit
gui3_w := 400
edit3_w := gui3_w - 30
Gui 3: Font, s15
Gui 3: Add, Edit, vQuickAdd w%edit3_w%
read_entries()
if (%0% > 0)
{
param = %1%
if (param == "delay")
sleep 30000
fetch_events()
}
refresh_listbox()
SetTimer check_for_notification, 60000
gosub check_for_notification
#ifwinactive Conspicuous todo list ahk_class AutoHotkeyGUI
pause::
show_quickadd(false, "")
return
#ifwinactive
pause::
if (listbox_count == 0)
show_quickadd(false, "")
else
Gui, Show, , Conspicuous todo list
Return
#ifwinactive Quick Add ahk_class AutoHotkeyGUI
enter::
gosub guiclose
if (quickadd_reschedule)
gosub delete_current_item
gosub quickadd
return
pause::
show_quickadd(false, "")
Gui 3: Hide
return
#ifwinactive
#ifwinactive Conspicuous todo list ahk_class AutoHotkeyGUI
insert::
guicontrolget pos,,TodoList
text := listbox_text_%pos%
time := listbox_time_%pos%
hour := substr(time, 9, 2)
minute := substr(time, 11, 2)
date := a_now
date += 1, day
year := substr(date, 1, 4)
month := substr(date, 5, 2)
day := substr(date, 7, 2)
date = %year%/%month%/%day% %hour%:%minute%
show_quickadd(true, text . " " . date)
return
delete::
gosub delete_current_item
if (listbox_count == 0)
gosub guiclose
return
#ifwinactive
delete_current_item:
guicontrolget pos,,TodoList
listboxpos := pos
delete_text := listbox_text_%pos%
delete_time := listbox_time_%pos%
loop %num_of_entries%
{
if (entry_text_%a_index% == delete_text and entry_time_%a_index% == delete_time)
{
entry_state_%a_index% := "deleted"
break
}
}
write_list()
refresh_listbox(listboxpos)
return
guiescape:
gosub guiclose
return
3guiescape:
gosub guiclose
return
guiclose:
gui, cancel
gui 3: cancel
return
check_for_notification:
StringLeft now, a_now, 12
newevent := false
loop %num_of_entries%
{
text := entry_text_%a_index%
time := entry_time_%a_index%
state := entry_state_%a_index%
if (state == "pending" and time <= now)
{
if (RegExMatch(text, " s$") == 0)
{
MsgBox 1, Todo, %text%
IfMsgBox OK
entry_state_%a_index% := "active"
else
entry_state_%a_index% := "deleted"
}
else
entry_state_%a_index% := "active"
newevent := true
}
}
if (newevent)
{
write_list()
refresh_listbox()
}
return
read_entries()
{
global
StringLeft today, a_now, 8
today .= 0000
num_of_entries = 0
Loop, read, %listfile%
{
;; why do we need to use the clipboard for unicode trasformation?
oldclip := clipboard
transform clipboard, unicode, %A_LoopReadLine%
entry := clipboard
clipboard := oldclip
stringsplit fields, entry, %A_Tab%
time := fields2
state := fields3
; purge old deleted items
if (state == "deleted" and time < today)
continue
num_of_entries += 1
entry_text_%num_of_entries% := fields1
entry_time_%num_of_entries% := time
entry_state_%num_of_entries% := state
}
}
refresh_listbox(deletedpos = 0)
{
global
entries =
listbox_count = 0
loop %num_of_entries%
{
text := entry_text_%a_index%
time := entry_time_%a_index%
state := entry_state_%a_index%
if (state == "active")
{
entries .= "|" . text
listbox_count += 1
listbox_text_%listbox_count% := text
listbox_time_%listbox_count% := time
}
}
if (listbox_count == 0)
entries = |
GuiControl,, TodoList, %entries%
if (listbox_count > 9)
GuiControl 2: , Count, %listbox_count%
else
GuiControl 2: , Count, %a_space%%listbox_count%
if (listbox_count == 0)
Gui 2:Hide
else
Gui 2:Show, NoActivate
if (deletedpos == 0)
GuiControl, Choose, TodoList, 1
else
{
if (deletedpos > listbox_count)
deletedpos := listbox_count
GuiControl, Choose, TodoList, %deletedpos%
}
}
quickadd:
GuiControlGet event, 3:, QuickAdd
calendar_event := false
FoundPos := RegExMatch(event, ".*\s(\S+)", match)
if (foundpos <> 0)
{
FoundPos := RegExMatch(match1, "[0-9]")
if (foundpos <> 0)
calendar_event := true
}
if (calendar_event)
fetch_events(event)
else
{
StringLeft now, a_now, 12
num_of_entries += 1
entry_text_%num_of_entries% := event
entry_time_%num_of_entries% := now
entry_state_%num_of_entries% := "active"
write_list()
refresh_listbox()
}
return
fetch_events(newevent = "")
{
global
RunWait python cal.py "%incomingfile%" "%newevent%",,hide useerrorlevel
if (errorlevel <> 0)
{
msgbox Error when contacting server. Check the error log file.
return
}
new_event_added := false
Loop, read, %incomingfile%
{
;; why do we need to use the clipboard for unicode trasformation?
oldclip := clipboard
transform clipboard, unicode, %A_LoopReadLine%
entry := clipboard
clipboard := oldclip
stringsplit fields, entry, %A_Tab%
text := fields1
time := fields2
entry_is_known := false
loop %num_of_entries%
{
if (entry_text_%a_index% == text and entry_time_%a_index% == time)
{
entry_is_known := true
break
}
}
if (entry_is_known)
continue
num_of_entries += 1
entry_text_%num_of_entries% := text
entry_time_%num_of_entries% := time
entry_state_%num_of_entries% := "pending"
new_event_added := true
}
if (new_event_added)
write_list()
}
write_list()
{
global
entrydata =
loop %num_of_entries%
{
oldclip := clipboard
clipboard := entry_text_%a_index% . a_tab . entry_time_%a_index% . a_tab . entry_state_%a_index% "`n"
transform text, unicode
entrydata .= text
clipboard := oldclip
}
filecopy %listfile%, %listfile%.%A_Now%.%A_TickCount%
filedelete %listfile%
fileappend %entrydata%, %listfile%
oldfiles =
Loop, %datadir%\list.*
{
if (oldfiles <> "")
oldfiles = %oldfiles%`n
oldfiles = %oldfiles%%A_LoopFileTimeModified%`t%A_LoopFileName%
}
Sort, oldfiles, R
count := 0
Loop, parse, oldfiles, `n
{
count += 1
if (count > max_list_backups)
{
StringSplit, FileItem, A_LoopField, %A_Tab%
FileDelete %datadir%\%FileItem2%
}
}
}
show_quickadd(reschedule, text)
{
global
GuiControl 3:, QuickAdd, %text%
quickadd_reschedule := reschedule
Gui, Hide
GuiControl 3: Focus ,QuickAdd
send ^{end}^{left}^{left}^+{end}
Gui 3: Show, w%gui3_w%, Quick Add
}
Accessing Google Calendar is done with the Python calendar library.
Here are the instructions on how to install it, and here's the python script called by the AHK script. Put them in the same directory.
If you want to test calendar access by running the python script manually first, then the first parameter of the script is the path of the file into which the fetched calendar items are stored. The config file must be in the same directory as the file, so the script uses this path to determine the location of the config file.
Code:
import gdata.calendar.service
import atom
import codecs
import datetime
import re
import sys
import os.path
import ConfigParser
import logging
import logging.handlers
LOG_LEVEL = logging.INFO
LOG_FORMAT = '%(asctime)s %(levelname)-8s %(message)s'
#----------------------------------------------------------------------
logging.basicConfig(level = LOG_LEVEL, format = LOG_FORMAT)
collog = logging.handlers.RotatingFileHandler("errorlog", None, 100000, 10)
collog.setLevel(LOG_LEVEL)
collog.setFormatter(logging.Formatter(LOG_FORMAT))
logging.getLogger().addHandler(collog)
#----------------------------------------------------------------------
try:
outfile = sys.argv[1]
quickadd = sys.argv[2] if len(sys.argv) > 2 else None
config = ConfigParser.ConfigParser()
config.read(os.path.join(os.path.dirname(outfile), "config"))
user = config.get('config', 'user')
password = config.get('config', 'pwd')
calendar = config.get('config', 'calendar')
calendar_service = gdata.calendar.service.CalendarService()
calendar_service.email = user
calendar_service.password = password
calendar_service.source = 'Google-Calendar_Python_Sample-1.0'
calendar_service.ProgrammaticLogin()
if quickadd:
event = gdata.calendar.CalendarEventEntry()
event.content = atom.Content(text = quickadd)
event.quick_add = gdata.calendar.QuickAdd(value='true')
new_event = calendar_service.InsertEvent(event, '/calendar/feeds/%s/private/full' % calendar)
query = gdata.calendar.service.CalendarEventQuery(calendar, 'private', 'full')
query.start_min = datetime.date.today().strftime("%Y-%m-%d")
query.start_max = (datetime.date.today() + datetime.timedelta(1)).strftime("%Y-%m-%d")
feed = calendar_service.CalendarQuery(query)
f = codecs.open(outfile, "w+", "utf-8")
for i, an_event in enumerate(feed.entry):
for a_when in an_event.when:
# skip timezone info, since the time
# seems local anyway
m = re.match("^(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})(T(?P<hour>\d{2}):(?P<minute>\d{2}):)?",
a_when.start_time)
assert m
start_time = m.group("year") + m.group("month") + m.group("day")
if m.group("hour"):
start_time += m.group("hour") + m.group("minute")
else:
start_time += "0000"
print >> f, '%s\t%s' % (an_event.title.text.decode("utf-8"), start_time)
f.close()
except:
logging.exception("Unhandled exception")
sys.exit(1)
Fill the config file with the values required to access the calendar and put it in datadir (see the AHK script for its location). Calendar can be your primary calendar (default) or you can create a separate calendar for these todo items (I do that). In the latter case find the private calendar url in calendar settings and copy the email address-like part from it (....@group.calendar.google.com). That's the calendar id.
Code:
[config]
user=xxx@gmail.com
pwd=yourpassword
calendar=default