Notifications, Automation, and Text-to-Speech

Recently I’ve been giving a lot of thought to notifications. Given that I spend so much time trying to communicate with my Mac, trying to get it to do whatever it is I’m thinking, it’s amazing how poorly my Mac manages to communicate with me. Mac OS X has no built-in notification system, so most Mac applications deal with notifications in one of three ways:

  • Issue notifications with a somewhat popular third-party utility for notifications, Growl. It provides simple overlay notifications, so it’s functionality is limited to being in the way or being ignored.
  • Issue their own equally broken notifications.
  • Omit notifications entirely.

In short, the Mac is a terrible platform for receiving notifications.

Here’s a digression: Before owning an Android phone, I might have said that notifications aren’t a solvable problem, but the Android mobile operating system’s notifications are a delight. They provide instantaneous notifications by flashing text in the status bar, followed by a persistent entry in the “window shade” pull-down notification queue. Android notifications provide immediate notifications like an overlay might, without being in the way or being permanently dismissed like an overlay. It’s a good solution.

Unfortunately, the prospects of a workable notification system arriving on the Mac look dim. Apple hasn’t yet attempted an OS X notification system, but it has dabbled in notifications on iOS. Although Apple’s iOS Notification Center is superficially similar to Android’s notification bar, it lacks a persistent inbox of notifications. Despite improvements, iOS still largely insists that notifications be dealt with on first appearance, rather than at a time of the user’s choosing.

Given the sorry state of notifications on the Mac, I’m left doing a bunch of wasteful things in terms of time and attention, just to know what’s going on. I spend a lot of time repeatedly checking on things. But I have found one area where I can get my computer to notify me in a useful way.

Lately, I’ve been on an automation kick, trying to collapse many-staged tasks into single, fire-and-forget tools. For example, to deploy the WebFaction documentation, I used to 1) log into the machine where the docs are hosted 2) update the version control checkout 3) run the build script and 4) enter my password at the appropriate prompts. Now I’ve managed to wrap all of that into a single command with Fabric, a tool for automating SSH activity.

While I’ve managed to eliminate the part where I pay attention to what’s going on, it still takes a minute or two to actually finish running. I hate checking on this process though, because it feels like I’m not actually getting the productivity gains I expect from automation. So I came up with this goofy scheme to announce the start and completion of Fabric tasks by text-to-speech. It’s is harder to ignore a loud, computer-generated voice than a visual notification (Air France 447 notwithstanding), but it also doesn’t get in the way. It’s not exactly what I want from my computer, but it’s what I can get.

How to Make Your Mac Read Your Fabric Tasks Aloud

This section will be most interesting to people who spend time with the terminal on the Mac. It’s okay to stop reading here, if finer implementation details don’t interest you.

To make this work, I exploit two newish features of my terminal application, iTerm2: triggers and coprocesses. Triggers let you fire off scripts and other activity upon the appearance of certain regular expressions in the terminal. Coprocesses receive terminal contents as standard input (and their standard output can be used as input back to the terminal, though I don’t use this particular feature).

To read aloud my Fabric tasks, I simply start a script on the appearance of the words Executing task, then call out to OS X’s say command at the appropriate points (and error conditions). Here’s a coprocess script that captures the Fabric output and says the relevant bits:

(you can also see/download this code as a gist)

#!/usr/local/bin/python2.7

# use as a trigger.
# Trigger regex: Executing task '(.*)'$
# Coprocess command: $HOME/bin/fabsay.py \1

import subprocess
import sys

def incoming():
    while True:
        yield raw_input()

def say(phrase):
    subprocess.call(['say', phrase])

def main():
    task_name = sys.argv[1]
    say('Starting {}'.format(task_name))

    for line in incoming():
        if 'Done.' in line:
            say('{} finished.'.format(task_name))
            sys.exit(0)

        if 'Aborting.' in line:
            say('{} failed.'.format(task_name))
            sys.exit(1)

        if 'Executing task' in line and task_name not in line:
            say('{} finished.'.format(task_name))
            task_name = line[
                line.index('Executing task') + len('Executing task') + 1:
                -1
            ]
            say('Starting {}'.format(task_name))

        if 'Stopped.' in line:
            sys.exit(0)

if __name__ == '__main__':
    main()

To set up the trigger:

  1. Start iTerm2.
  2. In the menu bar, click iTerm2 –> Preferences.
  3. Click Profiles.
  4. Select a profile.
  5. Click Advanced.
  6. In the Triggers section, click the Edit button.
  7. Click the + button to create a new entry.
  8. In the Regular Expression field, enter Executing task '(.*)'$.
  9. In the Action field, click to select Run Coprocess....
  10. In the Parameters field, enter the path to the script, followed by \1.
  11. Click the Close button.

And you’re done! Enjoy the dulcet tones of Apple’s Alex announcing, “Deploy finished.”