rb-appscript → MacRuby

Published
2013-01-30
Tagged

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
> gem i rb-appscript
2
Building native extensions.  This could take a while...
3
ERROR:  Error installing rb-appscript:
4
    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
#!/usr/bin/env ruby
2
3
require 'jcache'
4
require 'rb-appscript'
5
require 'date'
6
include Appscript
7
8
exit unless app('System Events').processes[its.name.eq 'OmniFocus'].get.size > 0
9
10
cache = JCache::Cache['of-cache']
11
12
of = app('OmniFocus').default_document
13
today = Date.today
14
15
cache['done'] = of.flattened_tasks[its.completed.eq(true).and(its.completion_date.eq(today))].get.map{ |t| t.name.get }
16
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 }
17
18
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
#!/usr/local/bin/macruby
2
3
require 'rubygems'
4
require 'jcache'
5
require 'date'
6
7
framework 'ScriptingBridge'
8
def app str
9
  SBApplication.applicationWithBundleIdentifier(str)
10
end
11
12
exit unless app('com.apple.systemevents').processes.any?{ |p| p.name == 'OmniFocus' }
13
14
cache = JCache::Cache['of-cache']
15
16
of = app('com.omnigroup.omnifocus').defaultDocument
17
today = Date.today
18
ft = of.flattenedTasks
19
cache['done'] = ft.select{ |t| t.completed && t.completionDate.get.to_date == today}.map(&:name)
20
cache['flagged'] = ft.select{ |t| t.flagged &&!t.blocked &&!t.completed }.map(&:name)
21
22
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
#!/usr/local/bin/macruby
2
3
framework 'ScriptingBridge'
4
app = SBApplication.applicationWithBundleIdentifier('com.omnigroup.omnifocus')
5
first_project = app.defaultDocument.projects[0]
6
7
# This works
8
num_tasks = first_project.rootTask.tasks.size
9
  # => 20
10
11
# But this doesn't...
12
first_project.rootTask.tasks.each{ |t| puts t.name }
13
  # => [SBObject classForCode:]: unrecognized selector sent to instance 0x400178dc0

The equivalent in AppleScript works fine:

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

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

1
class Task
2
  @tasks = {}
3
4
  def self.load
5
    @tasks = omnifocus.defaultDocument.flattenedTasks.select{|t|!t.completed && t.tasks.empty? }.group_by{ |t| (cp = t.containingProject.get) && cp.id }
6
  end
7
8
  def self.[] p
9
    @tasks[p.id]
10
  end
11
end