How to Create an OmniFocus Project Using OmniJS

So much faster I thought it hadn’t worked.

by Colter Reed
2:07 read (647 words)
by Colter Reed
2:07 read (647 words)

OmniFocus has been scriptable from the beginning. In fact, the idea of OmniFocus was born out of a collection of AppleScripts for OmniOutliner. I started writing AppleScripts to automate OmniFocus as soon as I got it.

In 2014, Apple introduced JavaScript for Automation (JXA), which lets you write scripts for macOS using JavaScript instead of AppleScript. It’s a language that is much more widely known, but the underlying technology is the same: the script sends a series of AppleEvents to the app. An event lets you send and instruction to an app or retrieve information from it. Each event has overhead and it adds up quickly.

Enter OmniJS. The OmniGroup has been adding a built-in JavaScript engine to each of its apps, and OmniFocus is no exception. OmniJS lets you run a script directly in the application itself with very little overhead. It’s in the early stages, and the details will undoubtedly change before it’s finished, but you can start playing with it now.

OmniJS is blindingly fast compared to JXA. I recently rewrote a script that I use regularly. As a JXA script, it took about 45 seconds to run. As an OmniJS script, you’ll miss it if you blink. (Seriously, the first time I got it to run, I didn’t think it had actually done anything.)

Here’s how to create a simple project using OmniJS.

Pay Rent
 - Write out check
 - Drop off rent check

Aside: Yes, this example is simple enough that you could use TaskPaper to create the task and be done with it. If that’s an option, go for it. Always use the simplest tool that could possibly work.

Before we get started, you’ll want to enable OmniJS in OmniFocus. That link will open OmniFocus and prompt you to enable OmniJS. You’ll probably need to quit and reopen OmniFocus after you do.

Let’s create that project using OmniJS.

function OmniMain()
{
    //
    // Calculate the date strings we need
    //
    var date = new Date()                        // Start with today
    date.setDate(1)                              // Go to the first…
    date.setMonth(date.getMonth() + 1)           // …of next month
    var month_name = Intl.DateTimeFormat('en-us', { month: 'long' }).format(date)
    
    date.setDate(0)                              // Go to the first of the previous (this) month
    date = Calendar.current.startOfDay(date)     // OmniJS extension to go to midnight
    var due_date = new Date(date)
    due_date.setHours(17)
    
    var defer_date = new Date(date)
    defer_date.setDate(defer_date.getDate() - 6) // Go back 6 more days (1 week total)
    defer_date.setHours(6)

    // 
    // Create the project
    //
    var project = new Project("Pay " + month_name + " Rent")
    project.task.dueDate = due_date
    project.task.deferDate = defer_date
    project.task.sequential = true
    new Task("Write out rent check", project)
    new Task("Drop of rent check", project)
}

//
// Run the script in OmniFocus
//
var scriptAsString = OmniMain.toString() + "\nOmniMain()"
var omniAutomationURL = encodeForOmniAutomation("OmniFocus",scriptAsString)

var app = Application.currentApplication()
app.includeStandardAdditions = true
app.openLocation(omniAutomationURL)


//
//  Based on encodeForOmniAutomation()[1] from omni-automation.com,
//  refactored to separate the responsibilities of creating the
//  url and encoding the script body.
//
//  I’ve also replaced the split/join pairs with replace,
//  since it’s going to be faster in modern JS engines.
//
//  [1] https://omni-automation.com/jxa-applescript.html
//
function encodeForOmniAutomation(omniAppName, scriptCode)
{
    appName = omniAppName.toLowerCase()
    urlOpening = appName + ":///omnijs-run?script="
    var encodedScript = encodeScriptBody(scriptCode)
    return urlOpening + encodedScript
}

function encodeScriptBody(scriptCode)
{
    var encodedScript = encodeURIComponent(scriptCode)
    encodedScript = encodedScript.replace("'","%27")
    encodedScript = encodedScript.replace("(","%28")
    encodedScript = encodedScript.replace(")","%29")
    encodedScript = encodedScript.replace("!","%21")
    encodedScript = encodedScript.replace("~","%7E")
    encodedScript = encodedScript.replace(".","%2E")
    encodedScript = encodedScript.replace("_","%5F")
    encodedScript = encodedScript.replace("*","%2A")
    encodedScript = encodedScript.replace("-","%2D")
    return encodedScript
}

Compare this to the script that will create the same project using dynamic TaskPaper.

Because we’re now setting the dates as Date objects and not strings, the date calculations are more involved. We need to make sure the time is set correctly, too. I used OmniJS’s Calendar class to help zero out the hours, minutes, and seconds. I didn’t use DateComponents because it seemed like overkill, although it’s probalby more technically correct.

OmniJS is a huge step forward for scripting the OmniGroup’s apps. In addition to faster scripts, you can also write plugins that add custom menu items. It’s all cross-platform, so you can develop an automation on your Mac and invoke it from your iPhone. Someday, we’ll probably get OmniJS on the Apple Watch, too…

Question: What possibilities does OmniJS open up for you? Share your thoughts in the comments, on Twitter, LinkedIn, or Facebook.