9.1 KiB
Hammerspoon Configuration
A literate programming file for configuring Hammerspoon.
Introduction
Ever since I got a Mac, I’ve used various tools to script it. With my limited number of applications, my UI needs are simple (I mean, what more do you need once Emacs is in full screen). That said, I’ve been using Hammerspoon for those few needs. While I refer to the standard documentation, I often steal snippets of code from others.
Simple to use. Use hs.execute to run a script, ha.alert.show to print a small message on the screen, and for something longer:
hs.notify.new({title="Hammerspoon", informativeText="Hello World"}):send()
Shortcut Keybindings
I’ve created left and right Meh keys on my Moonlanders:
- Left Meh Key:
C-M-S-s(Control Option/Meta Command Shift) - Right Meh Key:
C-M-S(Control Option/Meta Shift)
To create special key bindings, I can:
----------------------------------------------------------------------
-- Launcher replaces iCanHazShortcuts
hs.hotkey.bind({"alt", "ctrl", "shift"}, "T", function()
hs.application.launchOrFocus("iTerm")
end)
hs.hotkey.bind({"alt", "ctrl", "shift"}, "S", function()
hs.application.launchOrFocus("Slack")
end)
hs.hotkey.bind({"alt", "ctrl", "shift"}, "W", function()
hs.application.launchOrFocus("Spotify")
end)
hs.hotkey.bind({"alt", "ctrl", "shift"}, "F", function()
hs.application.launchOrFocus("Firefox")
end)
hs.hotkey.bind({"alt", "ctrl", "shift"}, "C", function()
-- hs.osascript.applescriptFromFile("~/bin/chrome.scr")
hs.execute("~/bin/chrome.scr")
end)
hs.hotkey.bind({"alt", "ctrl", "shift"}, "B", function()
hs.application.launchOrFocus("Microsoft Outlook")
end)
hs.hotkey.bind({"alt", "ctrl", "shift"}, "Z", function()
hs.application.launchOrFocus("zoom.us")
end)
hs.hotkey.bind({"alt", "ctrl", "shift"}, "A", function()
hs.application.launchOrFocus("Cursor")
end)
hs.hotkey.bind({"alt", "ctrl", "shift"}, "Q", function()
hs.application.launchOrFocus("KeepassXC")
end)
hs.hotkey.bind({"alt", "ctrl", "shift"}, "E", function()
hs.execute("FOR_WORK=yes open -a /Applications/Emacs.app")
end)
-- Special Emacs Guys
-- Right Meh key:
hs.hotkey.bind({"alt", "ctrl", "shift"}, "X", function()
hs.execute("~/bin/emacs-capture")
end)
-- Left Meh key:
hs.hotkey.bind({"cmd", "alt", "ctrl", "shift"}, "X", function()
hs.execute("~/bin/emacs-capture-clock")
end)
hs.hotkey.bind({"alt", "ctrl", "shift"}, "M", function()
hs.execute("~/bin/emacs-capture-meeting")
end)
Zoom
Library extensions to Hammerspoon are called spoons, and most of these are a simple Lua script, init.lua stored in the Spoons subdirectory. We grab these with a git clone, typically. To use the Zoom spoon, clone it:
git clone https://github.com/jpf/Zoom.spoon.git
I noticed that it does its works by calling Zoom’s menus, and with the latest version of Zoom, they changed the case of the menu, meaning that I needed to create a pull request.
From this nice essay, we create a menu bar item that shows the status, as well as allowing me to click it to toggle:
zoomStatusMenuBarItem = hs.menubar.new(true)
zoomStatusMenuBarItem:setClickCallback(function()
spoon.Zoom:toggleMute()
end)
updateZoomStatus = function(event)
hs.printf("updateZoomStatus(%s)", event)
if (event == "from-running-to-meeting") then
zoomStatusMenuBarItem:returnToMenuBar()
elseif (event == "muted") then
zoomStatusMenuBarItem:setTitle("🔴")
elseif (event == "unmuted") then
zoomStatusMenuBarItem:setTitle("🟢")
elseif (event == "from-meeting-to-running") then
zoomStatusMenuBarItem:removeFromMenuBar()
end
end
Now we can load, instantiate it, as well as create a callback loop to call updateZoomStatus:
hs.loadSpoon("Zoom")
spoon.Zoom:setStatusCallback(updateZoomStatus)
spoon.Zoom:start()
And bind a key to the mute ability that works good for both keyboards:
hs.hotkey.bind({"cmd", "alt", "ctrl", "shift"}, "M", spoon.Zoom:toggleMute)
hs.hotkey.bind({"cmd", "alt", "ctrl"}, "M", function()
spoon.Zoom:toggleMute()
end)
Lovely bit of code and shows the true power of Hammerspoon to fix the various app issues.
Automatically Adjust Volume
Leaving my home office and hopping on the train can be socially awkward when my laptop suddenly screams, so if I leave my home, I just readjust the volume and turn it off:
----------------------------------------------------------------------
-- When leaving house, turn off the volume:
wifiWatcher = nil
homeSSID = "Intertubes"
workSSID = "workdaysecure"
hotspotSSID = "Pixie Dust"
lastSSID = hs.wifi.currentNetwork()
function ssidChangedCallback()
newSSID = hs.wifi.currentNetwork()
if newSSID == homeSSID and lastSSID ~= homeSSID then
-- joined our home WiFi network
hs.audiodevice.defaultOutputDevice():setVolume(25)
elseif newSSID == workSSID and lastSSID ~= workSSID then
-- joined our work WiFi network
hs.audiodevice.defaultOutputDevice():setVolume(0)
elseif newSSID == hotspotSSID and lastSSID ~= hotspotSSID then
-- joined our hotspot WiFi network
hs.audiodevice.defaultOutputDevice():setVolume(0)
elseif newSSID ~= homeSSID and lastSSID == homeSSID then
-- departed our home WiFi network
hs.audiodevice.defaultOutputDevice():setVolume(0)
end
lastSSID = newSSID
end
hs.wifi.watcher.new(ssidChangedCallback):start()
Why yes, I’m looking for features to use Hammerspoon.
Clocking out a Task
I want to use Org’s task clocking ability, but I often forget to clock out. This code allows me to clock out whenever my computer goes to sleep … which works well when closing the laptop lid:
function sleepWatch(eventType)
if (eventType == hs.caffeinate.watcher.systemWillSleep) then
if hs.application.find("Emacs") then
hs.execute("/opt/homebrew/bin/emacsclient --socket work --eval '(ha-clock-out)'")
end
end
end
hs.caffeinate.watcher.new(sleepWatch):start()
Not that we could also do something for Wake:
if (eventType == hs.caffeinate.watcher.systemDidWake) then
...
end
Monitors
My company gave me a nice monitor … maybe a little too nice, as I don’t care to shift my neck to the extreme sides (serious first-world problem), so here I can center a window Keeping it my field of view:
----------------------------------------------------------------------
-- Centering a window on the large monitors at Work:
function centerWindow()
local win = hs.window.focusedWindow()
local app = win:application()
local f = win:frame()
-- Magic numbers figured out by trial and error:
f.x = 600
f.y = 30
f.w = 2200
f.h = 1470
if app then
local name = app:name()
-- If the application is Slack, adjust the height
if name == "Slack" or name == "iTerm2" then
f.h = 1200
end
end
win:setFrame(f)
hs.alert.show("Centered Window")
end
hs.hotkey.bind({"cmd", "alt", "ctrl", "shift"}, "Y", centerWindow)
Auto Reload Configuration
Whenever my configuration file is altered (or with a Meh-R key), I reload the configuration:
----------------------------------------------------------------------
-- Automatically reload the Hammerspoon configuration
hs.hotkey.bind({"cmd", "alt", "ctrl", "shift"}, "R", function()
hs.reload()
hs.alert.show("Reloaded Hammerspoon Config")
end)
function reloadConfig(files)
doReload = false
for _,file in pairs(files) do
if file:sub(-4) == ".lua" then
doReload = true
end
end
if doReload then
hs.reload()
end
end
hs.pathwatcher.new(os.getenv("HOME") .. "/.hammerspoon/", reloadConfig):start()