A while back I grew frustrated with iCal, Missing Sync, and my Palm Pilot. Here were the problems I wanted to solve:
-
I use repeating to-dos for a variety of small tasks that I just want to be reminded to do every once in a while, sometimes every day, sometimes up to every couple weeks. I know that GTD folks frown on it, but it works for me, except that iCal barfs on repeating to-dos.
-
I also had some tasks that had to be done on a pretty regular basis (e.g., bills at the start of every month), and I really wanted them to be tasks.
-
Last, I wanted to be able to see the next n days of appointments and all of todays open to-dos without opening an application.
What follows is my solution. I’m sure it’s inelegant in a number of ways, and feedback is welcome. I’m a complete scripting newbie. I hope someone finds some of it helpful as well – just trying to give back
-
I set up a scripts calendar, so I could run these scripts using iCal’s alarm feature, and as that’s all that’s in it, I can just turn off the display
-
Each of the following scripts is run first thing in the morning (in this order, at 5 minute intervals):
a) The following script, which bumps all uncompleted to-dos and moves forward each repeating to-do by the appropriate interval. One will notice that I just use days for the repeats. Anything more complex gets treated a bit later in the event → todo script.
-- I use a palm pilot, missing sync, and repeating to-dos that get checked off each day when I do them
-- I also want to just bump past-due open items to the current day
set thisDate to date (date string of (current date))
tell application "iCal"
launch
repeat with thisCalendar in calendars
-- collect all of the past due todos
set overdueTodos to (todos of thisCalendar whose due date is less than thisDate)
-- if they're not checked complete, move them to today
repeat with thisTodo in overdueTodos
if (not (completion date of thisTodo exists)) then set due date of thisTodo to thisDate
end repeat
-- at this point, there are no todos left that are both uncompleted and past due
-- now collect the repeating todos
-- if they weren't bumped in the first loop then we know they're checked off
set newTodoset to {}
-- I use the @@ in the first line of the notes to indicate a repeating to-do
-- I'm interested in anything past-due and repeating.
set repeatingTodos to (todos of thisCalendar whose (description begins with "@@") and (due date is less than thisDate))
-- now we make new todos
repeat with theTodo in repeatingTodos
set theProps to {}
set theProps to properties of theTodo
set tempWord to first word of description of theProps
set incrementDue to tempWord as number
set newDue to due date of theProps
-- add cycle days to due date until you get to or past today
repeat until newDue is greater than or equal to thisDate
set newDue to newDue + (incrementDue * days)
end repeat
set due date of theProps to newDue
-- now make a new todo with the properties of the old one but the new due date and no completion
-- we know that there is a due date and a description already so no need to test
-- and I don't use alarms for this class of todos.
set newTodoset to newTodoset & {theProps}
end repeat
-- now delete the pre-existing todos
-- for some reason, if I made the new to-dos first, and then deleted the old ones, it broke
-- maybe I was doing it wrong
delete (todos of thisCalendar whose (description begins with "@@") and (due date is less than thisDate))
-- and make new ones
repeat with newProps in newTodoset
tell (make new todo at end of todos of thisCalendar)
set priority to priority of newProps
set summary to summary of newProps
set due date to due date of newProps
if url of theProps is not missing value then set url to url of newProps
set description to description of newProps
end tell
end repeat
end repeat
end tell
b) This script dumps the events from today through the next 7 days, sorted by date, into a unix text file:
-- I'm going to write to a unix text file, because I use geektool and cat
set line_feed to (ASCII character 10)
set this_date to current date
set the start_day to (this_date - (time of this_date))
-- go out 7 days to the last second before midnight
set the end_day to (the start_day + (7 * days) - 1)
set all_events to {}
set what_cal to {}
-- I only use some of my calendars for this
set ok_cals to {"Work", "Admin", "Personal", "Chores", "Unfiled"}
-- the file, which also syncs to my iPod's Notes folder, just in case
set events_file to alias "Macintosh HD:Users:jsw:Documents:Notes to Sync:7_days_events.txt"
try
tell application "iCal"
repeat with i from 1 to count of ok_cals
set this_calendar to (the first calendar whose title is item i of ok_cals)
set returned_events to {}
set the returned_events to (every event of this_calendar whose start date is greater than or equal to start_day and start date is less than or equal to the end_day)
repeat with i from 1 to the count of returned_events
set this_item to item i of returned_events
set all_events to all_events & {this_item}
-- I use what_cal to preserve the calendar each event is in
set what_cal to what_cal & {name of this_calendar}
end repeat
end repeat
-- Craig Smith's bubblesort follows; I'm lazy, so I just put it inline
-- the goal here is to to sort all of the events by date
repeat with i from length of all_events to 2 by -1 --> go backwards
repeat with j from 1 to i - 1 --> go forwards
-- there might be an easier way to set the sort items
set j_props to the properties of item j of all_events
set j_date to start date of j_props
set j1_props to the properties of item (j + 1) of all_events
set j1_date to the start date of j1_props
if j_date > j1_date then
set {all_events's item j, all_events's item (j + 1)} to {all_events's item (j + 1), all_events's item j} -- swap
set {what_cal's item j, what_cal's item (j + 1)} to {what_cal's item (j + 1), what_cal's item j} -- swap in lockstep
end if
end repeat
end repeat
set all_events to all_events
end tell
set the master_data to "Events for Next 7 Days" & line_feed & "======================" & line_feed & events_2_text(all_events, what_cal)
write_to_file(master_data, events_file)
-- I found this useful for troubleshooting before the write-to-file stage
(*tell application "TextEdit"
activate
open events_file
end tell*)
on error error_message number error_number
if the error_number is not -128 then
display dialog error_message buttons {"OK"} default button 1
end if
end try
on write_to_file(this_data, target_file)
tell application "Finder"
try
set the target_file to the target_file as text
set the open_target_file to open for access file target_file with write permission
set eof of the open_target_file to 0
write this_data to the open_target_file starting at eof
close access the open_target_file
return true
on error
try
close access file target_file
end try
return false
end try
end tell
end write_to_file
on events_2_text(the_events, the_cal)
set line_feed to (ASCII character 10)
tell application "iCal"
set the event_count to the count of the_events
set event_summary to ""
if the event_count is 0 then
return the event_summary
end if
repeat with i from 1 to the event_count
set this_event to item i of the_events
set the event_properties to the properties of this_event
set summary_text to the summary of the event_properties
set start_date to date string of (get start date of the event_properties)
set end_date to date string of (get end date of the event_properties)
set starting_time to time string of (get start date of the event_properties)
set ending_time to time string of (get end date of the event_properties)
set event_description to the description of the event_properties
if event_description is missing value then set event_description to ""
set the date_text to start_date
if start_date is not equal to end_date then set the date_text to date_text & " to " & end_date
set the time_text to starting_time & " to " & ending_time
set the description_text to "Notes:" & " " & event_description
set the event_text to summary_text & " " & "(" & item i of the_cal & ")" & line_feed & the date_text & ", " & the time_text & line_feed
if event_description is not "" then set event_text to event_text & "Notes:" & " " & event_description & line_feed
set the event_summary to the event_summary & the event_text & line_feed
end repeat
return the event_summary
end tell
end events_2_text
c) This script does the same thing for todos, but separates by calendar and sorts within each calendar by priority.
-- This script runs through the calendars with todos in them and creates a unix text file from the open todos with today's due date
set this_date to current date
set this_date to (this_date - (time of this_date))
set all_todos to {}
-- I only want to see some of my calendars
set ok_cals to {"Work", "Admin", "Personal", "Chores", "Unfiled"}
-- this is the text file (the directory syncs to my iPod as well)
set todo_file to alias "Macintosh HD:Users:jsw:Documents:Notes to Sync:todays_todos.txt"
-- because I use geektool and cat, the file has to use unix lf, not mac returns
set line_feed to (ASCII character 10)
set the master_data to "Today's To-Dos" & line_feed & "==============" & line_feed
try
tell application "iCal"
activate
repeat with z from 1 to count of ok_cals
set this_calendar to (the first calendar whose title is item z of ok_cals)
set temp_todos to {}
set returned_todos to {}
set sort_priority to {}
set the temp_todos to (every todo of this_calendar whose due date is less than or equal to this_date)
repeat with each_todo in temp_todos
if (not (completion date of each_todo exists)) then set returned_todos to returned_todos & {each_todo}
end repeat
repeat with each_todo in returned_todos
set temp_props to properties of each_todo
--set sort_priority to sort_priority & priority of temp_props
if priority of temp_props is high priority then
set sort_priority to sort_priority & 1
else if priority of temp_props is medium priority then
set sort_priority to sort_priority & 2
else if priority of temp_props is low priority then
set sort_priority to sort_priority & 3
else
set sort_priority to sort_priority & 4
end if
end repeat
-- bubble sort borrowed from Craig Smith's bubblesort
repeat with i from length of sort_priority to 2 by -1 --> go backwards
repeat with j from 1 to i - 1 --> go forwards
if sort_priority's item j > sort_priority's item (j + 1) then
set {returned_todos's item j, returned_todos's item (j + 1)} to {returned_todos's item (j + 1), returned_todos's item j} -- swap
set {sort_priority's item j, sort_priority's item (j + 1)} to {sort_priority's item (j + 1), sort_priority's item j} -- swap in lockstep
end if
end repeat
end repeat
-- if there are todos in the calendar, then add them to the text, sorted by priority order
if returned_todos is not equal to {} then
set the master_data to the master_data & my calendar_todos(returned_todos, sort_priority, item z of ok_cals)
end if
end repeat
end tell
-- write this sucker out
write_to_file(master_data, todo_file)
-- I found this to be useful to test, so I left it in
(*tell application "TextEdit"
activate
make new document at the beginning of documents
set the text of document 1 to the master_data
end tell*)
on error error_message number error_number
if the error_number is not -128 then
display dialog error_message buttons {"OK"} default button 1
end if
end try
--- stripped down version of apple's write subroutine
on write_to_file(this_data, target_file)
tell application "Finder"
try
set the target_file to the target_file as text
set the open_target_file to open for access file target_file with write permission
set eof of the open_target_file to 0
write this_data to the open_target_file starting at eof
close access the open_target_file
return true
on error
try
close access file target_file
end try
return false
end try
end tell
end write_to_file
-- this is reworked from Apple's Event Summmary Script
on calendar_todos(the_todos, priorities, the_calendar)
set line_feed to (ASCII character 10)
tell application "iCal"
set the todo_count to the count of the_todos
set the todo_summary to return & the_calendar & line_feed & "----------" & line_feed
repeat with i from 1 to the todo_count
set this_todo to item i of the_todos
set the todo_properties to the properties of this_todo
set summary_text to the summary of the todo_properties
set priority_text to item i of priorities as string
set todo_description to the description of the todo_properties
set todo_description to ""
set the todo_text to " " & priority_text & " " & summary_text & line_feed & todo_description
set the todo_summary to todo_summary & todo_text
end repeat
return the todo_summary
end tell
end calendar_todos
- Then this script converts any more complex repeating to-dos (e.g, 1st of every month, 15th of month X) to a to-do list. I run it once a month on the first.
-- I have only a limited number of calendars that I want to work on
set ok_cals to {"Work", "Admin", "Personal", "Chores", "Unfiled"}
set start_date to (current date)
set start_date to start_date - (time of start_date)
-- I only run this script 1x/month on the first, so I run it out for the month
set days_out to daysinMonth for start_date
set end_date to start_date + days_out * days
tell application "iCal"
repeat with z from 1 to count of ok_cals
set this_calendar to (the first calendar whose title is item z of ok_cals)
set the_events to (events of this_calendar whose start date is greater than or equal to start_date and start date is less than or equal to end_date and description begins with "@@")
-- I use the "@@" because it never comes up otherwise
-- Now make new to-do's in the relevant calendar
repeat with each_event in the_events
set the_title to summary of each_event
set due_date to start date of each_event
-- I could probably have something set up following the @@ to set the priority, but this is good enough
make todo at end of todos of this_calendar with properties {summary:the_title, due date:due_date, priority:medium priority}
end repeat
end repeat
end tell
-- borrowed from Adam Bell's Dates & Times MacScripter Article
on daysinMonth for theDate
32 - ((theDate + (32 - (theDate's day)) * days)'s day)
end daysinMonth
Last, I use Geektool to cat the two text files (from items 2b and 2c) to the desktop with a periodic automatic refresh. One could also just point geektool at the text files and force refresh. In that case, one could also use regular Mac returns instead of Unix LF’s.