Thanks All: Sharing Scripts for iCal Tasks

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

  1. 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

  2. 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
  1. 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.

Thanks a bunch for this script–it’s been a lifesaver more than once. I had a quick question: if I open the text files created in emacs, the formatting is pretty poor–“^@” is interspersed everywhere. The reason I’m reading with this emacs in the first place is to access my todo list remotely (via ssh). I was wondering if you had any idea whence this formatting problem stemmed, and how to fix it?

Thanks again!

-a.

Model: 2.1 iMac G5
AppleScript: 1.10.7
Browser: Firefox 2.0.0.3
Operating System: Mac OS X (10.4)

@ aresnick -

It’s a file formatting problem. The text file seems to work OK in pico if it’s in UTF-8 (I don’t emacs). Try replacing the write line in the write to file subroutine with this line:

write this_data to the open_target_file starting at eof as «class utf8»

That should do the trick.

Incidentally, I have refined the scripts a bit more, and have found a way to use a shell script to get geektool to update the text files periodically any time iCal is open, so no intervention is required to correct the text files if you change them during the day. I’m happy to email the new versions if you like.

Thanks a lot–that did the trick. And it would be fantastic if you’d be willing to email those scripts to me.

Gratefully,
a.