Capture Tasks from Emacs to OmniFocus
Last week I had an urge to both see if it’s possible to send tasks from emacs to OmniFocus and to test Claude’s ability with elisp. Most of my productivity needs are handled analog but some go into OmniFocus (right now). I use orgmode for notes and tasks related to those notes but not as my task management system. While the quick capture in OmniFocus works just fine I thought i’d just experiment a little and see what’s possible - because with emacs anything is possible, right?
First Iteration
My first iteration with Claude prompts didn’t do too bad. Claude wanted to use the orgmode capture templates which I don’t really need and it cause issues with destroying my splits. So I modified it a little and simply use the modeline for prompts. It worked well but using the URL Scheme would trigger the OmniFocus capture dialog anyway. I could not find anything their docs so I asked on their slack and a user informed me of the autosave
param which solved that issue. Now it silently sent my task with notes to my inbox in the database.
(defun gn/omnifocus-capture-original ()
"Capture a task with optional note to OmniFocus via URL scheme"
(interactive)
(let* ((task-name (read-string "Task name: "))
(task-note (read-string "Note (optional): "))
(encoded-name (url-hexify-string task-name))
(encoded-note (if (string-empty-p task-note)
""
(concat "¬e=" (url-hexify-string task-note))))
(omnifocus-url (concat "omnifocus:///add?name=" encoded-name encoded-note "&autosave=true")))
(call-process "open" nil nil nil omnifocus-url)
(message "Task sent to OmniFocus: %s" task-name)))
(global-set-key (kbd "C-c o") 'gn/omnifocus-capture)
Robustified
I like that the orgmode capture templates will capture the location in a file in the notes so I came up with this robustified version of the function to prompt if I want to save the location and also to use the date picker for due dates.
(defun gn/omnifocus-capture ()
"Capture a task with optional note, due date, and location in file to OmniFocus via URL scheme"
(interactive)
(let* ((task-name (read-string "Task name: "))
(task-note (read-string "Note (optional): "))
;; Use org-read-date with empty default - just press Enter to skip
(due-date (org-read-date nil nil nil "Due date (Enter to skip): " nil nil t))
(due-date-param (if (or (not due-date) (string-empty-p due-date))
""
(concat "&due=" (url-hexify-string due-date))))
;; Ask about location only if in a file
(capture-location (and (buffer-file-name)
(string= "y" (read-string "Include current location? (y/N): " "n"))))
(location-info (when capture-location
(format "File: %s, Line: %d"
(file-name-nondirectory (buffer-file-name))
(line-number-at-pos))))
(full-note (if location-info
(if (string-empty-p task-note)
location-info
(concat task-note "\n\nLocation: " location-info))
task-note))
(encoded-name (url-hexify-string task-name))
(encoded-note (if (string-empty-p full-note)
""
(concat "¬e=" (url-hexify-string full-note))))
(omnifocus-url (concat "omnifocus:///add?name=" encoded-name encoded-note due-date-param "&autosave=true")))
(call-process "open" nil nil nil omnifocus-url)
(message "Task sent to OmniFocus: %s%s%s"
task-name
(if (string-empty-p due-date) "" (concat " (due: " due-date ")"))
(if location-info " [with location]" ""))))
Conclusion
Claude did ok with this experiment. Not fully working code but close enough for my less-than-stellar elisp chops to get it working. I don’t know if i’ll really make use of this going forward but it was a fun experiment and i’m keeping it in my config just in case. The nice to have feature of this is to quickly grab the location in a file and send to OmniFocus without interupting the context of what I’m working on.
If I don’t stick with OmniFocus and move to Things3 in the future, I’m confident this can be quickly modified to work with it’s URL Schemes.
I’m sure my elisp is not up to par so feel free to call me out it on Mastodon or email.