hardscrabble 🍫

By Max Jacobson

Psst. Check out my RubyConf 2017 talk, There are no rules in Ruby.

blog posts

composing a Parslet parser from modules and making code climate happy

27 Jul 2014

I’m making very slow and intermittent progress on smashcut, my Fountain screenplay parser written in Ruby. At this rate I’ll have a usable version in a few years or so :smile:. I’m going to talk about it a little bit here and then talk about how a small refactor boosted my Code Climate GPA for the project from 1.9 to 3.56.

(For those who don’t know: Code Climate is a very neat website which analyzes your code and points out improvement areas. They provide free analysis to open source projects like smashcut and paid analysis of private projects. They also maintain a cool blog about code design and techniques at http://blog.codeclimate.com/.)

Today on a train ride I reorganized the smashcut codebase a bit. The heart of the project is a file called fountain_parser.rb which describes the grammar of a Fountain screenplay as a list of rules. The syntax for defining a rule uses a DSL provided by Parslet, a great Ruby gem for exactly this purpose. Here’s the code example from their get started page:

require 'parslet'

class Mini < Parslet::Parser
  rule(:integer) { match('[0-9]').repeat(1) }
  root(:integer)
end

Mini.new.parse("132432")  # => "132432"@0

This is a very simple grammar with only one rule in addition to the required root (which names the rule that is expected to come first). If you were to define a grammar for something more complex, like a screenplay or even a programming language, you could expect there to be many, many more rules for defining specific, small things like operator characters and return charactersand then also abstract things like a character’s monologue or a program’s function.

At some point, your parser class will get quite long. I think this is kind of to be expected and not necessarily a bad thing. But it sure hurts your Code Climate GPA, which drops at the sight of long and complex classes. It also balks at code outside of methods, which is I think unfairly punishing to projects using DSL-style libraries.

It took some figuring out, but it is possible to compose your parser from modules. That example above could be rewritten like this:

require 'parslet'

module NumberRules
  include Parslet
  rule(:integer) { match('[0-9]').repeat(1) }
end

class Mini < Parslet::Parser
  include NumberRules
  root(:integer)
end

Mini.new.parse("132432")  # => "132432"@0

This is pretty nice when you have more than a few rules, and Code Climate rewarded me with a coveted A grade: https://codeclimate.com/github/maxjacobson/smashcut/compare/0cd1d78b...c2668c0e… well, for some of the classes anyway.

EDIT 2015-01-08: Hrmph, that link is dead now. Suffice it to say that the changes from this pull request improved the GPA from 1.9 to 3.56.

An Alfred Workflow for adding the frontmost Firefox tab to OmniFocus with AppleScript

13 Jul 2014

Saw a link about using tools to take long-lasting tabs out of your browser and put them somewhere useful like Evernote or OmniFocus rather than let them tax your mind and memory. I wanted in.

Sadly, there was only support for Chrome and Safari, not Firefox, which I continue to stubbornly prefer. I assumed at first this was simply a reflection of Firefox not being particularly popular among Mac users1 and resolved to patch the gap.

In order to interact with your running Mac applications, people often use AppleScript, a kind of strange programming language that comes with Macs and lets you script or automate user tasks. So whenever I want to close a tab without forgetting that it might matter somehow, I have to do these steps manually:

  • open the omnifocus quick entry panel with the global hotkey
  • write some kind of task title
  • go back to the browser
  • copy the URL
  • go back to the quick entry panel
  • type ⌘+', the keyboard shortcut to open the notes field
  • paste the URL
  • type ⌘+' again to return to the title field
  • press enter twice

Imagine having two dozen tabs open and kind of wanting to do that for each of them? Prime candidate for automation!

Given that this is my first AppleScript, I’m bizarrely confident that I can figure this out. I assume I can just ask Firefox for the list of currently open URLs, write some kind of loop, and then send OmniFocus a bunch of messages (in the background maybe) and have it all just work easily. Pretty quickly I learn that, unlike Safari and Chrome, Firefox exposes nothing to AppleScript. It’s scriptable insoafar as every app is scriptable – you can perform user actions like clicking and typing, but it doesn’t expose anything nicely, and Mozilla’s bugtracker has multiple tickets dating back several years of people saying they wish Firefox supported AppleScript without much movement as far as I can tell.

No worry! I’ll find some clever solution. I open AppleScript Editor and start trying to figure out how to even do any of this.

tell application "FirefoxNightly" # I'm using the nightly build, and I have to specifically say so
  activate # this opens or switches to the application
end tell

a digression about running AppleScripts

You can write small things like this in the AppleScript Editor app and run them from there. This is what I was doing while writing the thing and tinkering. But it’s not a great interface, really, as far as text editors go. Another option is to use the texteditor of your choice and run it from the terminal by writing osascript your_file.scpt. There’s something kind of trippy about running something like this, and seeing your operating system react:

[hardscrabble] [hardscrabble.net] [my-first-applescript branch]
⇥ osascript -e 'display alert "hello world"'
button returned:OK

Later on you can use tools like Alfred to run your scripts from anywhere at any time.

back to struggling with this code

So if Firefox doesn’t expose anything to us, how can we get the list of tabs open? I went down a few dead ends. I spent several minutes trying to make a bookmarks group of the currently opened tabs, then export the bookmarks, then parse the exported file before I realized I was definitely going the wrong way and I definitely didn’t know how to do parse JSON in AppleScript.

After a little more googling I came upon a comment on Bugzilla which provides a way to get the current page’s URL at least. Here’s how:


tell application "FirefoxNightly"
  activate
  delay 2 # sleeps 2 seconds because idk... it doesn't work otherwise
  tell application "System Events"
    tell process "firefox"
      keystroke "l" using {command down} # ⌘+l is the keyboard shortcut to focus on the address bar
      keystroke "c" using {command down} # ⌘+c copies the current URL to the clipboard
      set tabUrl to get the clipboard    # stores the clipboard in a variable
    end tell
  end tell
end tell

display alert tabUrl

Yikes… AppleScript is interesting because it’s an awkward, not fun language but also super powerful if you’re a kind of nerdy Mac power user, or maybe even developer who wants to harness the power of GUI applications in their project. Fortunately the next major version of OS X, Yosemite, has a JavaScript API for doing all the same stuff. This might be my last AppleScript too, because of that.

Now that I have the URL I want to do something better than display an alert. Some googling points me to a 2012 MacSparky blog post about AppleScripting with OmniFocus. The language is still awkward (set theProject to first flattened project where its name = "Finance"), but this is significantly better than Firefox because it exposes an API at all. Since then, there’s been a major 2.0 update to OmniFocus, but I guess they made an effort to maintain consistency because these examples still work. I don’t really blame Mozilla because they’re maintaining applications across every operating sytem that will support them and are very consistent. In fact I wish there existed OmniFocus for Linux but, like, lol. Everyone compromises!

OK so I can get the current URL and add a task to OmniFocus. A little more digging and this is what I came up with:

tell application "FirefoxNightly"
  activate
  set taskTitle to name of front window

  tell application "System Events"
    tell process "firefox"
      keystroke "l" using {command down}
      keystroke "c" using {command down}
      set taskNote to get the clipboard
    end tell
  end tell
end tell

tell application "OmniFocus"
  tell quick entry
    open
    make new inbox task with properties {name:taskTitle, note:taskNote}
  end tell
end tell

It’s kind of a dinky version of what I originally wanted to make. It’s only for the current tab, and when it’s done the quick entry panel is still open (by design: so I can still edit the details).

I packaged it as an Alfred Workflow here so I can invoke it from wherever by typing ⌘+space and then just writing tab.

I wonder if this is useful to other people!

  1. Recently on the Accidental Tech Podcast I was surprised to hear the hosts be super harsh to Firefox. This tweet makes the argument that Firefox isn’t relevant anymore and I don’t really know if that’s true, but I think it should be.