Skip to content

Working with Hammerspoon

Alex Claman edited this page Feb 27, 2023 · 5 revisions

Tired of wrapping test commands in hs.inspect(…)?

There's an undocumented function to preproccess console input: hs._consoleInputPreparser

You can set this to always wrap your input in hs.inspect with a custom depth:

hs._consoleInputPreparser = function(s) return 'hs.inspect('..s..', { depth = 2 } )' end
hs._consoleInputPreparser = function(s) return 'u.toJson('..s..')' end

First steps with Hammerspoon

If this is your first time experimenting with Hammerspoon, I can recommend zzamboni's fantastic ebook, Getting Started with Hammerspoon.

When you first startup Hammerspoon, you'll see a macOS notification informing you that Hammerspoon couldn’t find a configuration file. To fix this, run the commands below in a terminal window:

❯ touch $HOME/.hammerspoon/init.lua
❯ vim $HOME/.hammerspoon/init.lua

Let's add something to your Hammerspoon config just to confirm that everything is working properly:

hs.hotkey.bindSpec({ { "cmd", "alt", "ctrl" }, "s" },
  function()
    hs.notify.show("Hello from Hammerspoon!", "Everything looks good here, cap'n.", "Next, let's set up stackline.")
  end
)

Add the snippet above to your Hammerspoon config file and save the file. Then, click the 'Hammer' icon in your menu bar and select "Reload Config". Now, when you enter cmd + alt + ctrl, a macOS notification should appear with the message you entered above.

Congrats! Now you know enough about Hammerspoon to setup Stackline. If you'd like to continue learning more Hammerspoon's powers, see this blog post will be your guide.

Hammerspoon's quirks & rough-edges

lua is a unique language. It can be beautiful once you understand it, and perplexing if you don't (as I didn't when first working on Stackline). If, like me, you're not yet comfortable with lua check out the 'Working with lua' page for some quality-of-life tips before reading on.

The sections below represent areas of Hammerpsoon that required research on my part. It's not an exhuastive list of tips for beginners, but it might save you some time.

Async with hs.task

hs.task is async. There are 2 ways to deal with this:

  1. hs.timer.waitUntil(pollingCallback, func)
  2. hs.task.new(…):start():waitUntilUNex()

The 1st polls a callback to check if the expected result of the async task has materialized.

The 2nd makes hs.task behave synchronously.

The docs strongly discourage use of the 2nd approach, but as long as there isn't background work that could be done while waiting (there isn't in the use case I'm thinking of), then it should be slightly faster than polling since the callback will fire immediately when the task completes. It also saves the cycles needed to poll in the first place.

-- Wait until the win.stackIdx is set from async shell script
 hs.task.new("/usr/local/bin/dash", function(_code, stdout, stderr)
   callback(stdout)
 end, {cmd, args}):start():waitUntilExit()

 -- NOTE: timer.waitUntil is like 'await' in javascript
 hs.timer.waitUntil(winIdxIsSet, function() return data end)

-- Checker func to confirm that win.stackIdx is set 
-- For hs.timer.waitUntil
-- NOTE: Temporarily using hs.task:waitUntilExit() to accomplish the
-- same thing
function winIdxIsSet()
    if win.stackIdx ~= nil then
        return true
    end
end 

Drawing things on the screen with hs.canvas

Yet another project has a similar take:

if canvas then
   canvas:delete()
end

Clear any pre-existing status display canvases

     for state, display in pairs(self.displays) do
        if display ~= nil then
            display:delete()
            self.displays[state] = nil
        end
     end

Next: Changelog →