Making MacOS Great Again

By mcs94, Thu 11 April 2019, modified Sun 14 April 2019, in category Macos

automation, macos

Contents

MacOS has some interesting restrictions out of the box compared to Windows 10 and Gnome/KDE Desktop Environments. A good example of this, resizing windows using the keyboard, is a common feature nowhere to be seen on MacOS. Which is bit of a bummer.

However, there is hope. Enter Hammerspoon!

hammerspoon logo

Hammerspoon is an interesting automation application for macOS. In their own words it's a tool for powerful automation of OS X. It allows you to interact with the operating system using LUA code. The config is stored in a .lua file and modular bits of functionality (called spoons in the Hammerspoon vernacular) can be bolted on.

I came across it while looking for a decent way to resize my application windows using hotkeys but it's ended up doing a little more than that...

Installing Hammerspoon

Hammerspoon may be installed from their website or via brew.

brew cask install hammerspoon

Once installed, you'll find the configuration file in ~/.hammerspoon. There will be a few directories and a file called init.lua.

├── Spoons
├── modules
├── init.lua

init.lua may be used to store your configuration while you are getting started. Once you've shoved in a bit of code it may feel unwieldy, at which point you might want to start breaking out bits into separate files and calling them in the init.lua. This repository has an example of this.

Replacing Caffeine

Caffeine is a nifty little application which can stop your Mac going to sleep. Hammerspoon can replace it and many other small helper tools. Below is a snippet of lua you can drop into your Hammerspoon config which will do the same job. This snippet was shamelessly copied from Hammerspoon's Getting Started Guide.

caffeine = hs.menubar.new()
function setCaffeineDisplay(state)
    if state then
        caffeine:setTitle("Awake")
    else
        caffeine:setTitle("Sleepy")
    end
end
function caffeineClicked()
    setCaffeineDisplay(hs.caffeinate.toggle("displayIdle"))
end
if caffeine then
    caffeine:setClickCallback(caffeineClicked)
    setCaffeineDisplay(hs.caffeinate.get("displayIdle"))
end

Now, instead of the words Awake and Sleepy, lets use some nice looking icons:

local iconAwake = hs.image.imageFromPath("~/.hammerspoon/assets/awake.pdf")
local iconSleep = hs.image.imageFromPath("~/.hammerspoon/assets/sleepy.pdf")

local caffeine = hs.menubar.new()

function setCaffeineDisplay(state)
    if state then
        caffeine:setIcon(iconAwake)
        caffeine:setTooltip("Awake - go grab a coffee")
    else
        caffeine:setIcon(iconSleep)
        caffeine:setTooltip("Sleepy - gonna push zzz's")
    end
end

function caffeineClicked()
    setCaffeineDisplay(hs.caffeinate.toggle("displayIdle"))
end

if caffeine then
    caffeine:setClickCallback(caffeineClicked)
    setCaffeineDisplay(hs.caffeinate.get("displayIdle"))
end
menubar

That's much better.

Muting Your Mac on Wake

There is nothing worse than having your laptop wake up and blare out tunes in front of your work colleagues. Use this snippet to ensure that never happens again...

function muteOnWake(eventType)
    if (eventType == hs.caffeinate.watcher.systemDidWake) then
        local output = hs.audiodevice.defaultOutputDevice()
        output:setMuted(true)
    end
end
caffeinateWatcher = hs.caffeinate.watcher.new(muteOnWake)
caffeinateWatcher:start()

Change the Input Source when a USB keyboard is plugged in

Okay, we are getting a bit fruiter now. My work keyboard has a few buttons in odd places. Everytime I plug it in I have to change my input source to a customised one to handle the discrepancy. This gets old quick, so lets get Hammerspoon to do it for us.

First things first, we need to know the vendor ID and product ID of the keyboard:

# Return our USB device information
system_profiler SPUSBDataType

That will return something like the below:

usb-return

The following lua code will allow you to change your input source language when this keyboard is added or removed. Note we are calling another application, the excellent keyboardswitcher cli to do this.

function configureKeyboard(data)
    local keyboardSwitcher = '/usr/local/bin/keyboardSwitcher'
    local isKeyboardAffected = data.vendorID == 0x0483 and data.productID == 0x4001
    if isKeyboardAffected and data.eventType == "added" then
        hs.notify.new({title="Work Keyboard", informativeText="Welcome to work!"}):send()
        os.execute(keyboardSwitcher .. ' select "British - PC"')
    end
    if isKeyboardAffected and data.eventType == "removed" then
        hs.notify.new({title="Work keyboard", informativeText="removed"}):send()
        os.execute(keyboardSwitcher .. ' select British')
    end
end

local keyboardWatcher = hs.usb.watcher.new(configureKeyboard)
keyboardWatcher:start()

Toggle a VPN Connection

Lastly, but not least, we have a mix of LUA and applescript which allows us to toggle a VPN connection via a hotkey.

local vpnscript = string.format([[
set vpn_name to "'Cambridge VPN'"
tell application "System Events"
    set rc to do shell script "scutil --nc status " & vpn_name
    if rc starts with "Connected" then
        do shell script "scutil --nc stop " & vpn_name
        display notification "VPN " & vpn_name & " disconnected!"
    else
        do shell script "scutil --nc start " & vpn_name
        display notification "VPN " & vpn_name & " connected!"
    end if
end tell
]], "AppleScript")

hs.hotkey.bind(hyper, "H", function()
    hs.osascript.applescript(vpnscript)
end)

You may have noticed that the hotkey in the above snippet talks about hyper. This is a shortcut key composed of several other keys:

local hyper = {"shift", "ctrl", "alt", "cmd"}

In my setup I've remapped CAPS LOCK to all these keys using Karabiner-Elements so the above hotkey is actually triggered by pressing caps_lock and h. You could also remap CAPS LOCK using hammerspoon should you wish.