Useful notifications in iTerm2


For some reason, I've found that in my career a lot of the work I do ends up involving slow REPL loops: I make a change, run a command, wait for it to finish, and repeat. While I'm sitting there waiting, it's easy to be distracted by things, and I want my task to bring itself back to my attention when it's ready. This desire has led me on a multi-year quest to tailor my notifications perfectly, and in this post I'm going to share what I've come up with.

Here are the features I want my notifications to have:

  • I should be able to activate them from anywhere in the terminal: in bash, from the ruby console, over SSH, or anywhere else I find myself (in the terminal).
  • They should be subtle so they don't get too annoying, but noticeable enough that I don't miss them.
  • The should only fire when I want them: too many notifications will cause me to ignore all of them.

The early days

Back in 2015 or so, my company was writing scrapers to extract detailed, structured information from hundreds of websites. Each website was different and since we needed the information to be structured, we had to write a different scraper for each one. A single site could be scraped in about 5 minutes, and the data only changed a few times each year. Still, the sites would change often enough that I frequently found myself sitting in the office until late at night, mostly waiting for these scrapers to break so I could make one small fix, and repeat. Five minutes for each change really adds up when you have to repeat the entire scraper to test a single-line change.

Fortunately, writing scrapers is pretty easy technically, so I didn't need to be 100% focused as I was working on them. That meant that while I was waiting on the latest change I'd made to a scraper, I was able to focus on other high-value activities like shooting for a high score in Peggle Nights.

Screenshot of Peggle Nights

But sometimes, when I was planning a tough shot on a high-scoring round, I could forget that I was still at the office waiting for my scraper to crash so I could fix it. That's why I wanted a simple way to get notified when it was waiting for me. My first-ever attempt at building a notifications system used a feature built-in to Mac OS: the say command.

long_running_command; say "done"

This command uses the system's text-to-speech to play through your computer speakers. The scrapers I was writing were mostly in Ruby, so if I were in IRB I could still get the notification:

my_scraper.run_main rescue ex = $!; `say done`

This will play the notification even if the scraper fails, and preserve the raised exception in the ex variable. Neat!

I used this solution for a few months, but I always felt that the text-to-speech was a little unpleasant. I decided to switch out the say command for the built-in audio file player, afplay, and I used one of the default sounds that comes with Mac OS.

function ding {
  afplay /System/Library/Sounds/Glass.aiff
long_running_command; ding

That makes this sound, which is a little more subtle than a robotic voice shouting at me.

These two tricks got me along just fine for years. I felt like I had solved the problem and didn't need to think about it any more. Well, except when I started a command that I thought would be fast but it instead took an hour to run. But I developed a trick for that: if you press ^Z (Ctrl-Z) to pause the job, then you can use fg; ding to resume it and ding when it finishes. There. Now it's perfect.

Improving on perfection

One day I learned about iTerm2 shell integration. This provides a bunch of neat features, and one of them is about notifications! It's not very well documented, but if you use Edit ► Marks and Annotations ► Alerts ► Alert on Next Mark, or the much-easier-to-remember Option-Command-A, you'll get a native notification when the currently running command finishes. I installed the shell integration and to this day I use this feature more than anything else that it provides.

Fast forward to 2020 and I've changed roles again. This time, I find myself automating cloud server provisioners, and waiting for the machines to come up isn't fast. Sadly, the iTerm shell integration has to be installed on each server, and my perfect notification scripts don't work over SSH. Realizing that perfection is a journey and not a destination, I set out to improve my notifications once again. Except this time, I was going to go industrial-strength.

When I was learning about the iTerm2 shell integration, I also found out about a bunch of other features that iTerm2 supports. If you want to follow along with me, read over that post and install, iterm_notify, iterm_badge, and iterm_bounce on your computer. These new features had already made my notifications more powerful and SSH-compatible, but I had set my sights beyond a simple "ding!" sound. I wanted a smart notification system that would "ding" when the command completed, but "bonk" when it failed. But first I needed to find the right sounds to use.

The Legend of Zelda: Ocarina of Time

The first sound that came to mind was the iconic "Hey! Listen!" from Ocarina of Time. If you played this game, then you probably know exactly how that sounds. I found a gallery of sounds from the Zelda games and searched for some appropriate ones. Eventually I settled on these two, for my "good" and "bad" sounds:

The first thing to do is wrap them up in scripts to make them easy to use. I put them in my sound library (which is listing in and wrote these:

function ding {
    iterm_sound OOT_PressStart

function bonk {
    iterm_sound MM_Tatl_Alarm.wav

Now I can run my provisioning script like this:

./ && ding || bonk

If the script succeeds, the && ding will run, resulting in a pleasant chime. Otherwise the || bonk sound will run and I'll hear a nervous fairy. But I can do better! This is too much to type, and I want to leverage all of my notification channels, not just sound. So I wrote this function:

function sound_status {
    local last_status=$?
    test $last_status -eq 0 && ding || bonk
    if test -z "$@"; then
        iterm_notify "Command finished with status $last_status"
        iterm_notify "$@ finished with status $last_status"
    return $last_status

This function has 3 important qualities:

  1. It automatically chooses the sound to play.
  2. It also shows a system notification, with an optional custom message.
  3. It passes on the exit code, so you can run it in a pipeline.

For example, I can use it like this:

./; sound_status "" && ssh myhost

Because it passes on the exit status, the && ssh will only happen when the command succeeds, just like if the ; sound_status hadn't been there at all.

The little things in life

This notification script feels great: simple, only when requested, and with just a bit of "fun" flavor. But there is another area where I think that a subtle audio cue would help streamline my workflow. Sometimes I need to type 3 or 4 quick commands back-to-back. Has this ever happened to you?

$ git commit -m "respond to feedback"
no changes added to commit (use "git add" and/or "git commit -a")
$ git push
Everything up-to-date

I run these commands, see that it succeeded, shut my laptop, and head out to lunch. Later that day I wonder why nobody has reviewed my code, only to find that the second command had failed because I never staged those files, so I never pushed any changes at all. If I had been more careful, this wouldn't have happened. As a substitute for being more careful, I can have the computer tell me when my commands fail!

When I want is a subtle ping to tell me that something bad happened, every time a command fails. A lot of times this notification will be unnecessary. For example, if make fails with a compile error, I don't really need a sound to tell me about that. So I wanted a "negative" sound that wasn't too distracting. I eventually settled on this one.

Can you recognize it? It's a sound you'll have heard pretty often if you've ever played Factorio: the notification that a building you own has been destroyed. In the game, you'll often hear this sound altering you of an attack while you're working on some other task. Generally, it indicates a problem, but not necessarily an urgent one. I thought it would be perfect. Since I'm a user of Fish shell, I rigged up the notification like this:

# Saved to ~/.config/fish/conf.d/
if test -z $beep_command
  set beep_command "printf \a"
set beep_primed ""
function beep_preexec --on-event fish_preexec
  set -g beep_primed 1
function beep_postexec --on-event fish_postexec
  set -l last_status $status
  if test ! -z $beep_primed -a ! -z $argv[1] -a $last_status -ne 0
    eval $beep_command
  set -g beep_primed ""

There's a few things worth mentioning here:

  • To set the beep command, I used set -U beep_command "iterm_sound Factorio/alert-destroyed.wav". This sets a "universal variable" in fish, so it's permanent and system-wide. This lets me change the sound easily whenever I want.
  • The fish_postexec command runs even if you didn't run a command (pressed enter at an empty prompt), so we use the -z $argv[1] to filter that out.
  • The beep_primed variable is needed to ensure that the sound only happens once when the command fails. This is necessary to avoid some awkward situations that come up when you press Ctrl-C to cancel a command.

So that's where I am today. I am very happy with how my notifications work and I'm glad I was able to add a unique bit of flavor to them by sampling from some nostalgic video games. Thanks for reading! If you want to set up something like this and are having problems, feel free to reach out to me.

A picture of Ryan Patterson
Hi, I’m Ryan. I’ve been a full-stack developer for over 15 years and work on every part of the modern tech stack. Whenever I encounter something interesting in my jobs, I write about it here. Thanks for reading this post, I hope you enjoyed it!
© 2020 Ryan Patterson. Subscribe via RSS.