Ruby
Article
By Robert Qualls

Fun and Practical Alfred Workflows in Ruby

By Robert Qualls
Help us help you! You'll get a... FREE 6-Month Subscription to SitePoint Premium Plus you'll go in the draw to WIN a new Macbook SitePoint 2017 Survey Yes, let's Do this It only takes 5 min

glance

Alfred is an OS X productivity tool that can turn user-specified commands into actions. These are known as workflows and they can be written in a variety of languages – including Ruby.

In a previous article, I showed how to make an Alfred workflow to generate a list of random numbers for the user to choose, storing the selected number in the clipboard. In this article, let’s look at some more practical use cases, such as:

  1. Showing what’s on a website using screen scraping
  2. Using a JSON API and a symbol keyword for quick currency conversion
  3. Generating OS X calendar events with natural language

If you are new to Alfred, be sure to read over the previous article to get up to speed.

Note: You will need to buy the Alfred Powerpack to use and create workflows.

Alfredo

Previously, we sent information to Alfred by converting a Ruby hash into XML (using the gyoku gem) and then printing it to stdout. In this tutorial we’ll use a gem that simplifies this a bit: Alfredo.

First, install the alfredo gem. As with any workflow gem, be sure to install it for whatever Ruby you are using inside the workflow bash script, usually /usr/bin/ruby:

$ gem install alfredo

Then use it by creating a workflow object, adding items to it, and printing the XML:

require 'alfredo'

workflow = Alfredo::Workflow.new
workflow << Alfredo::Item.new({
  title: "Example",
  arg: "example"
})
workflow.output!

One of the nice things about alfredo is that it will fill in missing workflow item attributes like uid (used to keep track of your behavior for prediction) and autocomplete with intelligent defaults. If you don’t want these attributes, be sure to specify them in the Alfredo::Item constructor.

Screen Scraping

“Screen Scraping” may be a bit of a misnomer. We’re not taking a screenshot and then giving the pixel values to a machine-learning algorithm to get the characters. Although, that probably has plenty of applications.

Fortunately, with websites the process usually works like this:

  1. Send GET request to HTML endpoint (URL)
  2. Parse the HTML response string into a traversable hash
  3. Use the hash

If there’s a lot of JavaScript, then step 1 may require a headless browser with a JS engine.

Let’s make an Alfred workflow that will display the titles on the front page of rubyflow and then open the page that is selected. All we need is a blank workflow with an Inputs->Script Filter node connected to an Actions->Open URL node.

Inside the script filter, set bash as the script type, add a Ruby script to the workflow folder, and then run it with /usr/bin/ruby:

require 'net/http'
require 'nokogiri'
require 'alfredo'

url = "http://www.rubyflow.com"
uri = URI.parse(url)

body = Net::HTTP.get(uri)
html = Nokogiri::HTML(body)

workflow = Alfredo::Workflow.new

html.css(".body h1 a").each do |el|
  if el.text != ""
    workflow << Alfredo::Item.new({
      title: el.text,
      subtitle: "Open this page in your browser",
      arg: url + el["href"]
    })
  end
end

workflow.output!

rubyflow_result

How easy was that?

--ADVERTISEMENT--

Currency Conversion with a JSON API

Workflow keywords don’t need to be words, necessarily. If you want a quick currency converter, create a script filter workflow with your currency symbol as the keyword and then the amount as the argument. Be sure to uncheck “with space” in the script filter node so that you can use amounts like “$100” instead of “$ 100”.

We can use the free currency conversion API provided by Fixer.io.

require "json"
require "net/http"
require "alfredo"

amount = ARGV[0] || 1.0
base = "USD"

amount = amount.to_f
amount = 1.0 if amount == 0.0
url = "http://api.fixer.io/latest?base=#{base}"
uri = URI.parse(url)
body = Net::HTTP.get(uri)
json = JSON.parse(body, symbolize_names: true)

workflow = Alfredo::Workflow.new

json[:rates].each do |rate|
  # rate is in the format [:CUR, float]
  total = rate[1].round(4) * amount
  workflow << Alfredo::Item.new({
    title: total.to_s + " " + rate[0].to_s,
    subtitle: "Copy this value to the clipboard",
    arg: total
  })
end

workflow.output!

Since we’re using the keyword argument here, we access it via ARGV[0] by passing it to the script like

$ /usr/bin/ruby your_script.rb {query}

For the selected value to be copied to the clipboard, be sure to add an Outputs->Copy to Clipboard node with {query} as the text, and connect it to the output of the script filter.

currency_converter_result

A Reminder Workflow

Let’s create a workflow that lets us set OS X calendar notifications based on natural language. For example, we could say rem "pay the light bill on tuesday". Here’s how it’s going to work:

  1. Get the reminder and the time from the keyword argument
  2. Give these to AppleScript in a single string (Alfred seems to only support one
    argument at this time)
  3. Split the string and create an event in Calendar.

We’ll use the nickel gem (https://github.com/iainbeeston/nickel) for time parsing since it can parse messages and times together:

$ gem install nickel

Open up irb to see what kinds of times nickel will parse.

> require 'nickel'
> event = Nickel.parse "pay the light bill on tuesday"
=> message: "pay the light bill", occurrences: [#<Occurrence type: single, start_date: 20151103>]
> event.occurrences.first.start_date.day
=> 3

Note that nickel currently isn’t great at inferring things. You will need to specify AM/PM if you use that format – it works fine with 24 hour times:

> Nickel.parse "Go to the gym at 5:00 pm"
=> message: "Go to the gym", occurrences: []
> Nickel.parse "Go to the gym today at 5:00 pm"
=> message: "Go to the gym", occurrences: [#<Occurrence type: single, start_date: 20151101, start_time: 170000>]

OK, now let’s write the workflow:

require 'nickel'
require 'alfredo'

query = ARGV[0]
parsed = Nickel.parse(query)
workflow = Alfredo::Workflow.new

if parsed != nil && parsed.occurrences.size > 0
  event = parsed.occurrences.first
  reminder = parsed.message
  date = event.start_date || DateTime.new
  time = event.start_time || DateTime.new

  date_string = date.to_date.strftime "%m/%d/%Y"
  time_string = time.to_time.strftime "%I:%M %p"
  time_string[0] = '' if time_string[0] == '0'

  workflow << Alfredo::Item.new({
    title: "Remember \"#{reminder}\" on #{date_string} at #{time_string}",
    arg: "#{reminder};#{date_string} #{time_string}"
  })
end

workflow.output!

AppleScript

Next, we need some AppleScript that will create the Calendar event based on the time processed by nickel. AppleScript gives us the power to automate scriptable OS X application by sending them Apple Events.

To know what scriptable behavior an application supports, we will need to examine its AppleScript dictionary. This can be displayed by opening the AppleScript Editor (found in the Utilities Folder and called Script Editor) and then going to File->Open Dictionary and selecting the desired application.

Looking at the dictionary for Calendar, we can see what kind of properties are needed to create a new event:

calendar_applescript_event

In the workflow node view, add an Actions->Run NSAppleScript node and connect it to the script filter node. Then add the following code:

-- Found at http://erikslab.com/2007/08/31/applescript-how-to-split-a-string/
on split(theString, theDelimiter)
  -- save delimiters to restore old settings
  set oldDelimiters to AppleScript's text item delimiters
  -- set delimiters to delimiter to be used
  set AppleScript's text item delimiters to theDelimiter
  -- create the array
  set theArray to every text item of theString
  -- restore the old setting
  set AppleScript's text item delimiters to oldDelimiters
  -- return the result 
  return theArray
end split

on alfred_script(query)
  tell application "Calendar"
    tell calendar "Home"
      set inputString to query
      set inputArray to my split(inputString, ";")
      set reminder to item 1 of inputArray
      set dateString to item 2 of inputArray
      set newDate to my date (dateString)
      make new event at end with properties {description:reminder, summary:reminder, location:"Event Location", start date:newDate, end date:newDate}
    end tell
  end tell
end alfred_script

Note that we’re getting the argument passed from the script filter node with the on alfred_script argument instead of {query} as in the notification node.

Also, an AppleScript keyword that might not be obvious is my. This tells AppleScript that the context is the top-level script rather than the target of the tell block. We need this because AppleScript will not keep looking if something is not defined specifically in the current context.

Now, out the workflow and it will tell us the date and time that will be added once we’ve closed off the argument in quotes.

reminder_result

In order to receive notifications for calendar events, we need to turn them on in the preferences (CMD-,) for the Calendar application. You can set whether you want the alert to occur at the start of the event or some time before.

calendar_alerts

Nickel can parse whether events occur in an interval like a daily or monthly basis as well as a period between two times. So there’s a lot that can be added to the reminder workflow.

Conclusion

Tools like Alfred have only been around for a few years, so we’re probably just scratching the surface with what’s possible. It may especially be a boon for disabled users who need to associate spoken commands with complex behavior.

Oh, and if you want the workflow pictured at the top of the article, it can be found on GitHub.

Got any great ideas for workflows? Let us know in the comments.

More:
Login or Create Account to Comment
Login Create Account
Recommended
Sponsors
Get the latest in Ruby, once a week, for free.