rb-appscript → MacRuby

A few nights ago, I upgraded to ruby 2.0.0-dev through rbenv. Loading up a script, I got the following:

1
LoadError: cannot load such file -- rb-appscript

Not too surprising - a bunch of gems needed to get installed, so I quickly fired up the install script. Except…

1
2
3
4
> gem i rb-appscript
Building native extensions.  This could take a while...
ERROR:  Error installing rb-appscript:
    ERROR: Failed to build gem native extension.

This had been coming for a while. The developers of rb-appscript have said that the project has been abandoned, which is usually a good time to leave the ship.

Next step was to find a replacement. As far as I’m aware, the big popular base that everyone likes for getting ruby to play nice with OS X these days is MacRuby, which (it appears) can do all kinds of voodoo when it comes to interacting with .nib files and reading Core Data. I’m not into all that right now, though - what I really wanted was a way to interact with OS X programs, get data out of them, and use said data to my own nefarious purposes.

One common use here is to get data out of OmniFocus - for example, for my ongoing project to set up a personal kanban (part two is in the works, and will get published at some point, I swear). Here’s something simple to fetch some data from OmniFocus using rb-appscript:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env ruby

require 'jcache'
require 'rb-appscript'
require 'date'
include Appscript

exit unless app('System Events').processes[its.name.eq 'OmniFocus'].get.size > 0

cache = JCache::Cache['of-cache']

of = app('OmniFocus').default_document
today = Date.today

cache['done'] = of.flattened_tasks[its.completed.eq(true).and(its.completion_date.eq(today))].get.map{ |t| t.name.get }
cache['flagged'] = of.flattened_tasks[its.completed.eq(false).and(its.blocked.eq(false).and(its.flagged.eq(true)))].get.map{ |t| t.name.get }

cache.save

The change over to MacRuby is relatively painless - the main difference I noticed is that everything that was once snake_case is now camelCase, 50% of your usual get calls can be removed, and you need to require "rubygems" once again:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/local/bin/macruby

require 'rubygems'
require 'jcache'
require 'date'

framework 'ScriptingBridge'
def app str
  SBApplication.applicationWithBundleIdentifier(str)
end

exit unless app('com.apple.systemevents').processes.any?{ |p| p.name == 'OmniFocus' }

cache = JCache::Cache['of-cache']

of = app('com.omnigroup.omnifocus').defaultDocument
today = Date.today
ft = of.flattenedTasks
cache['done'] = ft.select{ |t| t.completed && t.completionDate.get.to_date == today}.map(&:name)
cache['flagged'] = ft.select{ |t| t.flagged &&!t.blocked &&!t.completed }.map(&:name)

cache.save

There is one interesting bug I found - I have no idea if the problem is on MacRuby’s end, or the scripting bridge, or the program itself, or buried somewhere in my system:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/local/bin/macruby

framework 'ScriptingBridge'
app = SBApplication.applicationWithBundleIdentifier('com.omnigroup.omnifocus')
first_project = app.defaultDocument.projects[0]

# This works
num_tasks = first_project.rootTask.tasks.size
  # => 20

# But this doesn't...
first_project.rootTask.tasks.each{ |t| puts t.name }
  # => [SBObject classForCode:]: unrecognized selector sent to instance 0x400178dc0

The equivalent in AppleScript works fine:

1
2
3
4
5
6
7
8
9
10
tell application "OmniFocus"
  tell default document
    set firstProject to first project
    set theTasks to tasks of root task of firstProject
    display alert (length of theTasks as string)
    repeat with theTask in theTasks
      display alert (name of theTask as string)
    end repeat
  end tell
end tell

For the moment, I’m using the following workaround:

1
2
3
4
5
6
7
8
9
10
11
class Task
  @tasks = {}

  def self.load
    @tasks = omnifocus.defaultDocument.flattenedTasks.select{|t|!t.completed && t.tasks.empty? }.group_by{ |t| (cp = t.containingProject.get) && cp.id }
  end

  def self.[] p
    @tasks[p.id]
  end
end