Hammerspoon: Automating Airpods and Brave Browser

hammerspoon logo

I use Hammerspoon to enhance my OSX with custom UI and automations, while also creating "monotasking" deep work environments to keep me focused.

As I continue to document my Hammerspoon configuration, I'm going to expand on hyper.lua and dive into a couple of modules with simple functionality I access with hyper: brave.lua and airpods.lua.

Brave.lua: automating my browser

I have been using the Brave Browser for a while… I wanted to not give Google any more information than it already has… I wanted to use Firefox and not give WebKit more market share, but Firefox doesn't have automation hooks.1 🤷🏻‍♂️

The brave.lua module is pretty simple, and has two parts: The first, a couple of JXA scripts for manipulating the tabs of the brave browser. The second, some helper methods for accessing parts of the configuration table I provide in init.lua.

To set up brave.lua, you require it and pass in any configuration you have setup.

config = {}
config.domains = {
  ['twitter.com'] = {
    url = 'twitter.com',
    tags = {'#distraction', '#socialmedia'}

local brave = require('brave')

Configuring the domains is optional, but makes using some of the functions easier.

Now you have access to brave's functions. The original reason I wrote it is because I often end up using Google Meet. Unlike Zoom, Meet usually runs in a browser tab. I had become used to pressing HYPER+z to focus Zoom to unmute when someone mentioned me, and I wanted the same functionality in Google Meet.

hyper:bind({}, 'z', nil, function()

My new "jump to video button" in action. F19 here is the hotkey that triggers my Hyper modal. No matter which window, which tab, it always focuses the right window and the right tab. 👍

This is a cool UX hack… my muscle memory is used to pressing this key to "jump to the video chat," and now it is consistent no matter which app I'm using.

In fact, my current iteration supports the same key for either zoom, google meet, or google hangouts…

-- Jump to google hangout or zoom
hyper:bind({}, 'z', nil, function()
  if hs.application.find('us.zoom.xos') then

(From init.lua)

One key, and I always focus the video window when someone asks me a question. 👌

brave.jump() uses some JXA that I believe I adapted from some AppleScript I found somewhere and rewrote. You can read the whole thing here. If nothing else, this highlights Hammerspoon's power to run random scripts based on your environment or input.

I use the exact same technique for a couple other things… I have a key that either focuses Figma.app, the Figma.com website, or Lucidchart (because sometimes I forget I use that.) It's basically a "show me my visual design" button. It's great!

There are some additional features in brave.lua that will be fleshed out in later posts.


This is a small trick but I use it all the time. When I started on this, there was not a good way to connect your AirPods to your computer with a single keypress. I think there's at least three menubar applications now on the App Store, but mine is free!

I found some AppleScript by @daGrevis that connects AirPods, wired it up in a lua module, require and specify the name of my AirPods, and connected it to my Hyper modal layer described earlier. That simple.

local airpods = require("airpods")

hyper:bind({}, 'a', nil, function()
  local ok, output = airpods.toggle('Evan’s AirPods')
  if ok then
    hs.alert.show("Couldn't connect to AirPods!")

Bonus: Snip browser highlight to Drafts as a Simple Markdown Zettelkasten

A lot of my simple markdown zettelkasten starts as something that I'm reading in a browser. While Drafts.app provides a lovely integrated share function if you are using Safari, if you are using Brave (for reasons shared above!) you have to do something different.

When Drafts.app opened up a simple AppleScript api, I could finally do this. This requires enabling an option in Brave: View -> Developer -> Allow Javascript from Apple Events.

-- Snip current highlight in Brave
hyper:bind({}, 's', nil, function()
    -- stolen from: https://gist.github.com/gabeanzelini/1931128eb233b0da8f51a8d165b418fa

    if (count of theSelectionFromBrave()) is greater than 0 then
      set str to "tags: #link\n\n" & theTitleFromBrave() & "\n\n> " & theSelectionFromBrave() & "\n\n[" & theTitleFromBrave() & "](" & theCurrentUrlInBrave() & ")"

      tell application "Drafts"
        make new draft with properties {content:str, tags: {"link"}}
      end tell
    end if

    on theCurrentUrlInBrave()
      tell application "Brave Browser" to get the URL of the active tab in the first window
    end theCurrentUrlInBrave

    on theSelectionFromBrave()
      tell application "Brave Browser" to execute front window's active tab javascript "getSelection().toString();"
    end theSelectionFromBrave

    on theTitleFromBrave()
      tell application "Brave Browser" to get the title of the active tab in the first window
    end theTitleFromBrave
  hs.notify.show("Snipped!", "The snippet has been sent to Drafts", "")

(from init.lua)

Here's what the script builds if I highlight a paragraph in this article.

tags: #link

Hammerspoon: Automating Airpods and Brave Browser

> When Drafts.app opened up a simple AppleScript api, I could finally do this. This requires enabling an option in Brave: View -> Developer -> Allow Javascript from Apple Events.

[Hammerspoon: Automating Airpods and Brave Browser](http://evantravers.com/articles/2020/06/11/hammerspoon-automating-airpods-and-brave-browser/)

I keep these in a Workspace in Drafts to process later, either saving to disk or sending to friends.

  1. That I can find… If you know of any, let me know! 

  • 2022-06-08 11:31:29 -0500
    Rename articles

  • 2020-09-28 09:20:43 -0500
    Change repo name

    I decided to change the name of my hammerspoon configuration repo to
    `hammerspoon-config` to avoid confusion... it's not the actual
    hammerspoon executable, it's a configuration for that executable.

  • 2020-06-18 14:26:02 -0500
    Move everything to CST

    Don't know why I didn't do that before. It caused _no_ end of

  • 2020-06-14 15:51:50 -0500
    Update articles with series and series partials

    After the refactor, needed to move the series metadata to the actual

  • 2020-06-11 15:42:55 -0500
    Fix typo, thank you @megalithic!

  • 2020-06-11 14:22:00 -0500
    Automating brave and airpods