OmniFocus Progress Chart Part 1: Extract & Save Data

Measured progress motivates me. I want to see how many things are in my OmniFocus library and if it’s trending larger or smaller. My desktop has been home to a list of today’s completed OmniFocus tasks for a long time, and now it’s home to a pair of charts: remaining items over the last 30 days, and items completed per day for the last 30 days.

The charts are created in 3 steps, and I am breaking these up into two posts:

  1. Extract a snapshot of the status of the database every day, (this post).
  2. Use that snapshot to create a text-based chart, (next post).
  3. Show the chart on my desktop, (next post).

Steps 1 & 2 are handled by a Javascript for Automation application written in Script Editor. Step 3 is handled by GeekTool.

Extracting & Saving the Database Status

There are a few ways to build a program that interacts with other programs on a Mac. For the completed task list I used Automator, but I don’t like editing Java- or Apple- script in the tiny Automator windows, so I tried out Script Editor. Script Editor lets us create an app bundle with somewhat modularized code, but isn’t the greatest IDE and crashes more than it should. Save your work often.

Applescript is foreign to me so I opted for Javascript for Automation, aka JXA, Apple’s, (maybe abandoned?), attempt to get a Javascript version of Applescript running. It works ok, but documentation of how it actually works, and what parts of modern Javascript are supported, is hard to find.

To extract the data we can use OmniFocus’s flattenedTasks list, which gets a flattened list of all tasks in the database, then we can filter that list of tasks by status, creation date, completion date, and so on:

// Get the "document" that we need to work with.
const ofdoc = Application('OmniFocus').defaultDocument;

// Get a list of all tasks in the DB.
const tasks = ofdoc.flattenedTasks;
// now tasks.length is the total number of tasks in the DB
// (this will change a lot when you archive old tasks)

// Filter using the .whose method.
const remainingTasks = tasks.whose({
      effectivelyCompleted: false,
      effectivelyDropped: false
});

// ... etc. for completed tasks, and tasks completed/added/dropped in the past day.

Filtering with whose is pretty slow so this can’t be run every few seconds, (it takes several seconds to run), but for now I’m only updating daily so it’s fine if it’s slow.

I tried iterating over the tasks and checking the effectivelyCompleted property on each one, thinking it would be faster, but effectivelyCompleted has to send a message to OmniFocus to get a response, and doing that for my entire library is much slower than a single .whose() call.

Once all the tasks are filtered the current stats put into a Javascript object, which is .push()‘ed onto the end of a Javascript array of all the stats and stored on disk as a JSON file.

Modularizing the code was a hurdle.

I tried using module exports & imports but they don’t work. It seems like there is an import() function available but I can’t find what it does. It’s hard to see what Javascript/ECMAScript features are supported in JXA. Some documentation says that it uses the same Javascript engine as Safari, but if that was true when it was released it doesn’t appear to be true now.

Even without exports & imports the Release Notes say we can import libraries into our script like so:

// Imports from StatsProcessor.scpt - supposedly. Also, where is StatsProcessor.scpt?
const StatsProcessor = Library("StatsProcessor");

The 10.11 Release Notes say that we can put our library scripts into the ‘Contents/Library/Script Libraries’ directory of the App we’re making. I couldn’t find a way to do this, or even see the Script Libraries folder in Script Editor, but once I moved everything around with finder the library was recognized and the import worked. But this code crashed:

const StatsProcessor = Library("StatsProcessor");
StatsProcessor.anyPublicMethod();

Not only did it crash, but it made Script Editor crash as well. Calling any method on an imported library caused a crash. Apparently we need to use a “compiled” script – a .scptd file – for libraries. This isn’t documented anywhere that I can find, and there seems to be no way to change between a .scpt and .scptd file, at least not using a GUI.

There’s also no documentation for that a Library actually is and what’s available when it’s imported. From what I can see any global function declared in the imported file is available as a method on the imported object.

Once the modularization was working it was relatively straightforward to figure out where to store the data, (answer: In ~/Library/Application Support/OmniFocusStats), and save it as desired.

Next up: Using the data to make & display a chart.

Show the OmniFocus Tasks You Did Today on your Desktop

This morning Ken Case retweeted this:

Using Automator, not TextExpander

I’ve wanted a solution to show me what I accomplished in a day for a while, but I don’t use TextExpander. Luckily Colter Reed’s script to log today’s completed OmniFocus tasks is Javascript for Automation, which can be run with the built-in OSX app, Automator, so I built an Automator workflow that runs the script, (I just pasted Colter Reed’s script into an Automator “Run Javascript” action, then made one tweak when I ran into a small problem). If you’ve never used Automator before don’t worry – I hadn’t either! Here’s my Automator workflow file, (it’ll need to be unzipped). It returns a bunch of text, which we’ll deal with next.

Put the Results on your Desktop with GeekTool

GeekTool can take text from a shell script and display it on your desktop, and you can set it to refresh that text however often you want. I created a new shell Geeklet with GeekTool with the following script:

Automator Runner ~/Developer/Scripts/omnifocus-completed.workflow | sed -e 's/^"//' -e 's/"$//' -e 's/\\"/"/g'

For some reason the Automator Runner returns the text with quotation marks around it, so the parts after the pipe remove the quotation marks, (using instructions from Stackoverflow).

When choosing your refresh interval keep in mind that the script grabs focus from OmniFocus when it runs, which will be annoying if you are working in OmniFocus and you have a short refresh interval. I also don’t know how much battery this will eat for those of us on laptops.

With some font & colour tweaking, my desktop now looks like this:

omnifocus-completed-desktop-screensho

Update (October 9, 2015): This morning a bunch of tasks I did last night, but before midnight, were still on my desktop, so I went debugging. It looks like OmniFocus, (at least my version, which is a pre-release test version), is exposing the completedDate value to Javascript as a UTC date/time, but with a Timezone offset set, so things done in the evening might remain on the list of “today’s” completed tasks, (this might reverse on the other side of the world, with things you do in the morning not appearing). To fix this I’ve modified the startOfDay() function to take the Timezone Offset into account. I assume this is a bug and will be temporary, so I’m not updating the downloadable Automator action above. Here’s the updated startOfDay() function:


function startOfDay() {
// The day started at midnight this morning
    var d = new Date(),
    hours = 0,
    minutes = 0;
    
    if( d.getTimezoneOffset() !== 0 ) {
        hours = Math.round( d.getTimezoneOffset() / 60 );
        minutes = d.getTimezoneOffset() % 60;
    }
    
    d.setHours( hours );
    d.setMinutes( minutes );
    d.setSeconds( 0 );
    
    return d;
}

Update (November 3, 2015): I used quotation marks in a task name, and they came out escaped on my desktop. I’ve updated the command line for the geeklet to strip the slashes from before double quotation marks;