AutoHotkey Community

It is currently May 26th, 2012, 8:53 pm

All times are UTC [ DST ]




Post new topic Reply to topic  [ 13 posts ] 
Author Message
PostPosted: June 22nd, 2009, 6:36 pm 
Offline

Joined: October 9th, 2004, 8:55 pm
Posts: 217
Location: Budapest, Hungary
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


Last edited by keyboardfreak on August 21st, 2009, 5:26 pm, edited 10 times in total.

Report this post
Top
 Profile  
Reply with quote  
 Post subject:
PostPosted: June 22nd, 2009, 6:43 pm 
Offline

Joined: July 10th, 2006, 2:12 pm
Posts: 6
I also want on of this. Thanks!


Report this post
Top
 Profile  
Reply with quote  
 Post subject:
PostPosted: June 22nd, 2009, 8:55 pm 
Has anyone got this to work on windows 7? I tried it with python 2.6 and 3.0 and get the same error "Error when contacting server Errorlevel: ERROR"

or, has anyone come across another script to do quick adds to google calendar without opening the browser?

(I'd love to be able to use this!)


Report this post
Top
  
Reply with quote  
 Post subject:
PostPosted: June 22nd, 2009, 8:59 pm 
Offline

Joined: October 9th, 2004, 8:55 pm
Posts: 217
Location: Budapest, Hungary
cedardoc wrote:
Has anyone got this to work on windows 7? I tried it with python 2.6 and 3.0 and get the same error "Error when contacting server Errorlevel: ERROR"


The python script creates an error log file when a problem occurs. Take a look at it. It may give you a more detailed error message.


Report this post
Top
 Profile  
Reply with quote  
 Post subject:
PostPosted: June 22nd, 2009, 11:38 pm 
keyboardfreak wrote:
cedardoc wrote:
Has anyone got this to work on windows 7? I tried it with python 2.6 and 3.0 and get the same error "Error when contacting server Errorlevel: ERROR"


The python script creates an error log file when a problem occurs. Take a look at it. It may give you a more detailed error message.


I've searched and searched but can't find an error log. I looked for all ".txt" and everything with "log" or "error" but can't find it. I also looked in both places where the autohotkey script is and refers to.

Any hints? (p.s. I know zero about python- just trying to follow instructions correctly)

Thanks


Report this post
Top
  
Reply with quote  
 Post subject:
PostPosted: June 23rd, 2009, 6:58 am 
Offline

Joined: October 9th, 2004, 8:55 pm
Posts: 217
Location: Budapest, Hungary
cedardoc wrote:
I've searched and searched but can't find an error log. I looked for all ".txt" and everything with "log" or "error" but can't find it. I also looked in both places where the autohotkey script is and refers to.

Any hints? (p.s. I know zero about python- just trying to follow instructions correctly)


The file name is "errorlog" and it should be in the same directory as the python script I think. If it isn't then change its location in the script to a known path, e.g. C:/errorlog:

Code:
collog = logging.handlers.RotatingFileHandler("errorlog", None, 100000, 10)



If it doesn't help then there is an other possibility. Errors are logged only after the script was started properly, it could import its necessary libraries at the top, etc. You can try running the python script from the command prompt to see if it actually gets to the "try:" statement from which errors are logged. If there is an error then it is printed to the console and it contains the line number where the error occurs.

If the script doesn't get to the "try:" line then you have a problem with your python install. Have you run the Google client library tests as suggested in the installation instructions?


Report this post
Top
 Profile  
Reply with quote  
 Post subject:
PostPosted: July 1st, 2009, 12:46 pm 
Offline

Joined: October 9th, 2004, 8:55 pm
Posts: 217
Location: Budapest, Hungary
I'm getting very used to the fact there is an instant google calendar quickadd dialog accessible from anywhere. It's very easy to juggle your tasks with it and send yourself quick reminders as Gina Trapani describes in this article:

http://lifehacker.com/127823/geek-to-li ... o-calendar

She uses Yahoo Calendar, but the concept is the same.


Report this post
Top
 Profile  
Reply with quote  
 Post subject: Error
PostPosted: July 1st, 2009, 4:16 pm 
Offline

Joined: September 15th, 2006, 10:25 am
Posts: 567
this is my errorlog file..

Code:
2009-07-01 20:43:47,217 ERROR    Unhandled exception
Traceback (most recent call last):
  File "D:\Programs\gdata\shajulsoft\gcal\pycal.py", line 28, in <module>
    outfile = sys.argv[1]
IndexError: list index out of range


couldn't get it to work.. the quick add dialog does not have any add button also.. either i am dumb or the tutorial is..


Report this post
Top
 Profile  
Reply with quote  
 Post subject: Re: Error
PostPosted: July 1st, 2009, 4:28 pm 
Offline

Joined: October 9th, 2004, 8:55 pm
Posts: 217
Location: Budapest, Hungary
shajul wrote:
couldn't get it to work.. the quick add dialog does not have any add button also.. either i am dumb or the tutorial is..


Nah, it's not dumb, only a bit sparse. :D


The error you get most probably occurs because you try to run the python script manually from the command line without any argument. It requires an argument as it is described in the first post:

Quote:
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.


The quick add dialog doesn't have any buttons, because it is designed for efficency. Use of a mouse is discouraged, because it's horrible inefficent.

You can add your event by simply pressing Enter after typing it, or cancel the dialog with Esc.


Report this post
Top
 Profile  
Reply with quote  
 Post subject:
PostPosted: July 8th, 2009, 3:13 pm 
Offline

Joined: October 9th, 2004, 8:55 pm
Posts: 217
Location: Budapest, Hungary
The great thing in a calendar-based todo list is that it's very easy to hide tasks which you cannot work on at the moment and get notified when the task is due again.

To streamline the process even more I added two small improvements to the reschedule/postpone feature:

1. The default date for reschedule is tomorrow at the same time the task was scheduled originally, so you can quickly postpone the task by a day. Of course, the date can be modified to anything you want, it's just the default.

2. The date/time part is highlighted automatically in the quickadd dialog when the task is rescheduled, so you can easily overwrite it by simply typing a new time (e.g. 4pm) if you only want to postpone the task by a few hours.

See the updated script in the first post.


Report this post
Top
 Profile  
Reply with quote  
 Post subject:
PostPosted: July 14th, 2009, 3:09 pm 
Offline

Joined: October 9th, 2004, 8:55 pm
Posts: 217
Location: Budapest, Hungary
Until now the user had to choose when adding a new item if the item is a calendar item or a normal item. This was not convenient and as it turned out it was unnecessary too.

From now on the quickadd dialog is a single input field. If the user types an item in which the last word contains a number (e.g. walk the dog 4pm) then it will be handled as a calendar item and it is added to the calendar.

Otherwise, it will be handled as a regular item and added to the todo list immediately (e.g. empty trashcan).

So the only rule is the last word must contain a number, but it is always true for calendar items and not usual for normal todo items.

If you specify the day too then make sure you write it before the time and everything will be OK. E.g. instead of "walk the dog 4pm friday" write "walk the dog friday 4pm"


See the updated script in the first post.


Report this post
Top
 Profile  
Reply with quote  
 Post subject:
PostPosted: July 21st, 2009, 5:26 pm 
Offline

Joined: October 9th, 2004, 8:55 pm
Posts: 217
Location: Budapest, Hungary
Sometimes when we can't deal with a task we reschedule it to a later time, so that it disappears from the current todo list. If we do that and the task is not time sensitive then popping up a msgbox is not always needed when the task is due again, since we don't need a new notification we only want the task on the todo list now that we can deal with it.

I added support for this by cheking if the task description contains the word "silent". If it does then the task is added to the todo list automatically when it's due again. No questions asked.

See the updated script in the first post.


Report this post
Top
 Profile  
Reply with quote  
 Post subject:
PostPosted: August 21st, 2009, 5:27 pm 
Offline

Joined: October 9th, 2004, 8:55 pm
Posts: 217
Location: Budapest, Hungary
The silent option mentioned in the previous post proved to be useful, but it's too verbose, so from now on if an entry ends with " s" (before the time specification) then it is considered silent.


Report this post
Top
 Profile  
Reply with quote  
Display posts from previous:  Sort by  
Post new topic Reply to topic  [ 13 posts ] 

All times are UTC [ DST ]


Who is online

Users browsing this forum: Google Feedfetcher and 12 guests


You can post new topics in this forum
You can reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot post attachments in this forum

Search for:
Powered by phpBB® Forum Software © phpBB Group