Fun and Practical Alfred Workflows in Ruby
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:
- Showing what’s on a website using screen scraping
- Using a JSON API and a symbol keyword for quick currency conversion
- 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:
- Send GET request to HTML endpoint (URL)
- Parse the HTML response string into a traversable hash
- 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!
How easy was that?
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.
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:
- Get the reminder and the time from the keyword argument
- Give these to AppleScript in a single string (Alfred seems to only support one
argument at this time) - 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:
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.
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.
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.