(* This script will read the and parse lines in the todo.txt file. OmniFocus tasks whose ID matches a completed task in todo.txt will be marked as complete in OmniFocus. A task will be created in OmniFocus for each task in todo.txt which do not have an OmniFocus ID. The contents of the todo.txt file will be replaced with an export from the "Active Export" perspective (which should not be grouped). If growl is available, a message will be displayed informing the user when the process finishes. This expects to have a perspective setup in OmniFocus called "Active Export" which is an uncategorized list of all tasks that you would like to "sync" to todo.txt The only items synced from todo.txt to OmniFocus are new tasks and tasks that are marked as done. For everything else, OmniFocus as the system of record and the data stored there will overwrite the data in todo.txt. Known Issues: If you create a project in todo.txt, it will not be created in OF. If a project name in OF has a new-line in its name, tasks added to that project in todo.txt will not be added to that project in OF. The only things that are synced from todo.txt to OF are new taks, and tasks marked as done. New-lines and spaces are replaced with an underscore character in projects and contexts. This can cause issues if your projects or contexts have underscores in them. New-lines are replaced with a space in task names (from OF to todo.txt) The OF ID must be the last item on the line in todo.txt to be parsed properly. '@' and '+' are used to identify contexts and projects in todo.txt - having these characters in either project names, contexts or task names will cause them to be interpreted as contexts and projects in todo.txt and not part of the name *) -- ---------- --------- ---------- -- settings -- ---------- --------- ---------- set ofPerspectiveName to "ActiveExport" set homePath to POSIX path of (path to home folder) set todoTxtPath to homePath & "/Dropbox/todo/todo.txt" -- ---------- --------- ---------- -- begin the main routine -- ---------- --------- ---------- --get current items in todo.txt open for access todoTxtPath set todoData to paragraphs of (read todoTxtPath) close access todoTxtPath --look at each item from todo.txt and update the corresponding task --in OmniFocus or add the item if it does not exist in OmniFocus repeat with taskLine in todoData if length of taskLine > 0 then set t to makeTask(taskLine) set taskName to taskName of t set taskId to taskId of t --already exists in omnifocus if it has an ID if taskId is not equal to "" then if done of t then --if todo.txt has it marked as done, mark it as done in OmniFocus markDoneInOmniFocus(taskName, taskId) end if else --does not exist in omnifocus, so create it createOmniFocusTask(t) end if end if end repeat --pull items from OmniFocus and genereate a new list of tasks for todo.txt set todoTaskList to {} tell application "OmniFocus" tell the default document to tell the front document window set perspective name to ofPerspectiveName set oTrees to trees of content set n to count of oTrees repeat with i from 1 to count of oTrees set t to value of (item i of oTrees) set my todoTaskList's end to my createTaskFromOFTask(i, t) end repeat end tell end tell --write the todo's out to a new file set nf to open for access todoTxtPath & "-new" with write permission set eof nf to 0 --erase the file in case it's there already repeat with t in todoTaskList if not waiting of t then write (getLine() of t) to nf write " " to nf end if end repeat close access nf --move the new file into place -- backup the original first do shell script "mv " & quote & todoTxtPath & quote & " " & quote & todoTxtPath & "-old" & quote do shell script "mv " & quote & todoTxtPath & "-new" & quote & " " & quote & todoTxtPath & quote -- growl("Sync with todo.txt script complete") -- ---------- --------- ---------- -- end of the main routine -- ---------- --------- ---------- (* This is essentially a class of "task" that can be used to parse a todo.txt line or build a todo.txt line *) on makeTask(taskLine) script task property tmpLine : "" property taskId : "" property taskName : "" property done : false property creationDate : "" property doneDate : "" property dueDate : "" --not currently used property startDate : "" --not currently used property priority : "" property tprojects : {} --a list of projects to which the todo.txt task is assigned property tcontexts : {} --a list of contexts to which the todo.txt task is assigned property waiting : false --not currently implemented --parse a line from a todo.txt file on parseLine(inputLine) set my tmpLine to inputLine parseDone() parsePriority() parsecreationDate() parseProject() parseContext() parseId() set my taskName to tmpLine end parseLine --generate a line suitable for inclusion in a todo.txt file on getLine() set newLine to "" if my done then set newLine to "x " & doneDate else if my priority is not equal to "" then set newLine to "(" & priority & ") " end if if my creationDate is not equal to "" then set newLine to newLine & creationDate & " " end if set newLine to newLine & replaceSubString(my taskName, " ", " ") --technically, tasks from OF will not have multiple projects, but since todo.txt CAN have multiple projects assigned to one task, I assume that in all cases. repeat with p in replaceStringInList(replaceStringInList(my tprojects, " ", "_"), " ", "_") set newLine to newLine & " +" & p end repeat --see above repeat with c in replaceStringInList(replaceStringInList(my tcontexts, " ", "_"), " ", "_") set newLine to newLine & " @" & c end repeat if my startDate is not equal to "" then set newLine to newLine & " start:" & my startDate end if if my dueDate is not equal to "" then set newLine to newLine & " due:" & my dueDate end if --add the OF task ID to the end of the line set newLine to newLine & " $ID:" & my taskId return newLine end getLine on parseDone() set d to character 1 of my tmpLine if d is equal to "x" then set my done to true set my tmpLine to (characters 3 thru -1 of my tmpLine) as string parseDoneDate() else set my done to false end if end parseDone on parseDoneDate() if my done then --done date will be the next caracters after x space of the following format yyyy-mm-dd set my doneDate to (characters 1 thru 10 of my tmpLine) as string set my tmpLine to (characters 12 thru -1 of my tmpLine) as string end if end parseDoneDate on parsecreationDate() --I really should do some validation around these dates that I'm pulling out set my creationDate to (characters 1 thru 10 of my tmpLine) as string set my tmpLine to (characters 12 thru -1 of my tmpLine) as string end parsecreationDate on parsePriority() if character 1 of my tmpLine is equal to "(" and character 3 of my tmpLine is equal to ")" and character 4 of my tmpLine is equal to " " then set my priority to character 2 of my tmpLine set my tmpLine to (characters 5 thru -1 of my tmpLine) as string else set my priority to "" end if end parsePriority --split on the delimiter of " $ID:", the part before that is the task line, the part after is the ID, so ID must be the last thing on the line. on parseId() -- save delimiters to restore old settings set oldDelimiters to AppleScript's text item delimiters -- set delimiters to delimiter to be used set AppleScript's text item delimiters to " $ID:" -- create the array set theArray to every text item of my tmpLine -- restore the old setting set AppleScript's text item delimiters to oldDelimiters if length of theArray = 2 then set my taskId to item 2 of theArray set my tmpLine to item 1 of theArray end if end parseId --used to get all projects and contexts from a todo.txt line. on getListWithSep(sep) set myList to {} set s to offset of sep in my tmpLine repeat while s > 1 --grab the first part of the line - up to the project start mark set fst to (characters 1 thru (s - 1) of my tmpLine) as string --set the remainder of the line as the part after the start mark set rem to (characters (s + 2) thru -1 of my tmpLine) as string --project name ends with a space (technically any whitespace) set e to offset of " " in rem if e > 0 then --the end was found --set the last part of the line as the part after the project title set lst to (characters e thru -1 of rem) as string set e to e - 1 set myValue to (characters 1 thru e of rem) as string set myList's end to myValue else --else the end is the end of the string set e to -1 set lst to "" set myValue to (characters 1 thru e of rem) as string set myList's end to myValue end if --remove the project string from the line set my tmpLine to fst & lst set s to 0 offset of sep in my tmpLine end repeat return myList end getListWithSep on parseProject() set projects to getListWithSep(" +") set my tprojects to replaceStringInList(projects, "_", " ") end parseProject on parseContext() set contexts to getListWithSep(" @") set my tcontexts to replaceStringInList(contexts, "_", " ") end parseContext on replaceStringInList(inList, fromChar, toChar) set outList to {} -- save delimiters to restore old settings set oldDelimiters to AppleScript's text item delimiters repeat with i in inList -- set delimiters to delimiter to be used set AppleScript's text item delimiters to fromChar set tmpList to every text item of i set AppleScript's text item delimiters to toChar set iName to tmpList as string set outList's end to iName end repeat -- restore the old setting set AppleScript's text item delimiters to oldDelimiters return outList end replaceStringInList on replaceSubString(i, fromChar, toChar) set output to "" -- save delimiters to restore old settings set oldDelimiters to AppleScript's text item delimiters -- set delimiters to delimiter to be used set AppleScript's text item delimiters to fromChar set tmpList to every text item of i set AppleScript's text item delimiters to toChar set output to tmpList as string -- restore the old setting set AppleScript's text item delimiters to oldDelimiters return output end replaceSubString end script if length of taskLine > 0 then tell task to parseLine(taskLine) end if return task end makeTask --given a "task" object, create a corresponding task in OF. on createOmniFocusTask(task) set taskName to taskName of task set con to missing value set taskFlagged to false if length of (tcontexts of task) > 0 then repeat with c in (tcontexts of task) if ("" & c) is equal to "flagged" then set taskFlagged to true else if con is equal to missing value then set con to c end if end if end repeat end if if length of (tprojects of task) > 0 then set prj to first item of (tprojects of task) else set prj to missing value end if set userChoice to display dialog "Add " & taskName & " to OmniFocus?" buttons {"YES", "NO"} if button returned of userChoice is equal to "YES" then tell application "OmniFocus" tell default document set inboxTask to true if prj is not equal to missing value then set theProject to first flattened project where name is prj if theProject is not equal to missing value then set inboxTask to false end if end if if inboxTask is equal to false then tell theProject set t to make new task with properties {name:taskName} end tell else set t to make new inbox task with properties {name:taskName} end if if con is not equal to missing value then set mycontext to (first flattened context where name is con) if mycontext is not equal to missing value then set context of t to mycontext end if end if if taskFlagged is equal to true then set flagged of t to true end if end tell end tell end if end createOmniFocusTask on markDoneInOmniFocus(taskName, taskId) tell application "OmniFocus" tell default document set foundTasks to (flattened tasks whose id is taskId) if number of items in foundTasks is equal to 1 then set completed of (first item in foundTasks) to true else display dialog "Could not identify which task titled \"" & taskName & "\" to mark as complete!" end if end tell end tell end markDoneInOmniFocus --create a todo.txt task from an OmniFocus task on createTaskFromOFTask(ofPriority, ofTask) using terms from application "OmniFocus" --I use priority D-Z allowing for A-C (the ones that get color and are at the top) to be selected manually set priorityString to "DEFGHIJKLMNOPQRSTUVWXYZ" set t to makeTask("") if ofPriority <= length of priorityString then set priority of t to (character ofPriority of priorityString) end if set taskId of t to id of ofTask if (name of ofTask) is not equal to missing value then set taskName of t to name of ofTask end if if (creation date of ofTask) is not equal to missing value then set creationDate of t to convertDateToString(creation date of ofTask) end if if (start date of ofTask) is not equal to missing value then set startDate of t to convertDateToString(start date of ofTask) end if if (due date of ofTask) is not equal to missing value then set dueDate of t to convertDateToString(due date of ofTask) end if if (context of ofTask) is not equal to missing value then set (tcontexts of t)'s end to name of context of ofTask end if if flagged of ofTask then set (tcontexts of t)'s end to "flagged" end if if (containing project of ofTask) is not equal to missing value then set conDoc to (containing project of ofTask) set n to name of conDoc set (tprojects of t)'s end to n end if return t end using terms from end createTaskFromOFTask --convert an AppleScript date object to a string in the format of yyyy-mm-dd on convertDateToString(inDate) if inDate is not missing value then set y to year of inDate set mm to (characters -2 thru -1 of ("00" & (month of inDate as number))) as string set dd to (characters -2 thru -1 of ("00" & (day of inDate))) as string return y & "-" & mm & "-" & dd else return "" end if end convertDateToString on growl(message) tell application "System Events" set isRunning to (count of (every process whose bundle identifier is "com.Growl.GrowlHelperApp")) > 0 end tell if isRunning then tell application id "com.Growl.GrowlHelperApp" -- Make a list of all the notification types -- that this script will ever send: set the allNotificationsList to {"Sync Complete Notification", "Sync Failed Notification"} -- Make a list of the notifications -- that will be enabled by default. -- Those not enabled by default can be enabled later -- in the 'Applications' tab of the growl prefpane. set the enabledNotificationsList to {"Sync Complete Notification"} -- Register our script with growl. -- You can optionally (as here) set a default icon -- for this script's notifications. register as application "Sync with todo.txt" all notifications allNotificationsList default notifications enabledNotificationsList icon of application "Script Editor" notify with name "Sync Complete Notification" title "Sync Complete" description message application name "Sync with todo.txt" end tell end if end growl