hamacs/hammerspoon.org
2025-12-02 17:08:12 -08:00

9.1 KiB
Raw Permalink Blame History

Hammerspoon Configuration

A literate programming file for configuring Hammerspoon.

Introduction

Ever since I got a Mac, Ive 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, Ive 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

Ive 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 Zooms 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, Im looking for features to use Hammerspoon.

Clocking out a Task

I want to use Orgs 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 dont 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()