Custom MacOS Notifications

04 Nov 2022
7 minute read

I wanted Jekyll to notify me when it had completed building my site. Getting it to work the way I wanted involved patching together a bunch of almost-solutions from different places.

A monster jumps out of a colorful box, holding a sign.
A monster jumps out of a colorful box, holding a sign. Image by Dall-E.

This blog is built using Jekyll and it works pretty well for my needs. Most people who use Jekyll as a blogging platform do it in conjunction with GitHub. It’s easy to set it up that way, and all you have to do is make a new git project and then every time you push your changes up to GitHub it will rebuild your site for you.

That’s nice, but I wanted a different setup. Running your blog on GitHub imposes some limitations on you. Mostly, you can’t have your own plugins, and you can’t have custom gems either. I wanted to have access to both of those. Also, in general, I like to own my own content. I don’t want my content to be controlled by any other site. So I host my own Jekyll blog.

My Blogging Setup

In my setup, I have a local copy of my blog on my MacBook. I edit posts there, tweaking them until I’m happy, and then I push the blog up to the production server when I want to publish. It’s a workflow that works pretty well for me.

But there’s been one annoyance that’s been bothering me, and I only recently solved that. While I’m editing the blog, there’s a cycle of Edit–Save–Reload Browser–Check and Evaluate. I edit my posts in a text editor (Sublime Text if you’re interested), and when I save it, Jekyll notices that files have been changed and rebuilds the site. That step takes time, about ten seconds. What’s worse is that the window running Jekyll and my web browser are on two different virtual screens. So I have to spend those ten seconds either watching Jekyll compile (boring waste of time) or spamming the Refresh button on my web browser to check if the rebuild is complete.

It would be nice if Jekyll could tell me it was done rebuilding, so I could instead have that notification pop up whereever I am, and then I’d know I could refresh and check the latest edit. To do that, I’d need to leverage the Notification Manager.

MacOS Notifications

Some MacOS notifications

The Notification API on MacOS will pop up a little bot in the corner for a few seconds, and then fade away. That’s exactly what I would like. It’s possible to bring this up from the command line like so:

$ osascript -e 'display notification "Site build complete" with title "Jekyll"'

This runs the osascript tool which is the command line invocation for AppleScript. Now we need to have Jekyll make this call when it’s done building the site each time.

The way to tell Jekyll to do things is via hooks. Specifically, we want the :site, :post_write hook. Defining a hook with that type will enable code to be run after every time Jekyll builds the site.

In the _plugins directory, create notifier.rb

module Jekyll
  Hooks.register :site, :post_write do |site, output|
    system %Q!osascript -e 'display notification "Site build complete" with title "Jekyll"'!
  end
end

You don’t need to edit your Gemfile; just placing the file in the _plugins directory is enough for jekyll to find it (you will have to restart your jekyll server if it’s running, to pick up the plugin). Now whenever jekyll detects a file modification and rebuilds the site, it will execute this one line and a notification will appear.

A first pass at displaying a notification toast from Jekyll.

Adding an icon

This is pretty good for a first attempt. It’s functional. But the icon is a little annoying. That’s the icon of the Script Editor, since technically osascript is the program that’s displaying the notification. But that seems a little generic. What I’d like is to display a Jekyll icon instead.

Adding an icon to a MacOS Notification popup is a little tricky. The issue is that the display notification command in osascript doesn’t have an argument that lets you set the icon. So we have to go about it in a different way.

When you create a script using Script Editor, that app gives you a couple options when saving a script. Normally you save script files as Script, but if you change that to Application then Script Editor creates a little mini-app for you, with an icon you can double-click to launch the script. Then we can give that app its own icon, and that icon will display in the Notifications the app sends.

File format options for saving a script

I downloaded a Jekyll icon from a free icons site, created a one-line script in Script Editor, then saved that script as an Application in the _plugins directory.

A one line script to display a notification

Since this app is only going to be used by Jekyll, I felt it was appropriate to install it directly in Jekyll’s directory structure. But you will also need to tell Jekyll to ignore this app in terms of which files it monitors to see if it needs to rebuild the site. You can do that in your _config.yml file:

# Excluded items can be processed by explicitly listing the directories or
# their entries' file path in the `include:` list.
#
exclude:
  - .sass-cache/
  - .jekyll-cache/
  - gemfiles/
  - Gemfile
  - Gemfile.lock
  - node_modules/
  - vendor/bundle/
  - vendor/cache/
  - vendor/gems/
  - vendor/ruby/
  - _plugins/NotifyJekyll.app/

To set the icon, open the Info window for the new app and drag the icon image to the upper right.

Finder info for the Notify app

There’s one other hitch. For some reason, if you invoke this app from the command line (eg open NotifyJekyll) it will silently fail. I’m not sure why. However, we can invoke the app binary directly. Apps in MacOS are just directories, and if we poke through the NotifyJekyll.app directory we just created, we’ll find the file NotifyJekyll.app/Contents/MacOS/applet. This is the executable that actually gets run, and we can invoke it directly from our script like so:

module Jekyll
  Hooks.register :site, :post_write do |site, output|
    this_dir = File.dirname(__FILE__)
    system "#{this_dir}/NotifyJekyll.app/Contents/MacOS/applet"
  end
end

The plugin now assumes that we’ve saved the NotifyJekyll app in the _plugins directory, and it invokes the applet binary in that app package. Now when we edit the blog and Jekyll recompiles, we get a notification like this:

MacOS popup with Jekyll icon

Looking good! But we can do even better.

Automatically reloading in Safari

As long as we’re using AppleScript to display the notification, we might as well use the same script to tell Safari to reload the page. Instead of waiting for the notification to pop up and then pressing cmd-R, we get the notification and then the page will reload on its own. The only difficulty is, what if we don’t currently have the page open? We’ll have the script check for that and only reload if it’s showing our blog. The address for the blog when Jekyll is serving it locally is localhost:4000, so we can check for that

display notification "Site build complete" with title "Jekyll"

tell application "Safari"
  set theURL to URL of current tab of window 1
  if theURL contains "localhost:4000" then
    set URL of current tab of window 1 to theURL
  end if
end tell

And that’s the complete system. Now I can edit my blog as I like on my MacBook, and everything automatically updates. When it looks the way I want, I use git to push it up to the public server.

Tagged with

Comments and Webmentions


You can respond to this post using Webmentions. If you published a response to this elsewhere,

This post is licensed under CC BY 4.0