Powerful Hammerspoon

I have used hammerspoon for a long time, recently I found that the new version of hammerspoon supported Lua plugins called “Spoons”.

So I deceided to rewrite my hammerspoon config file with spoons.

I hope this post can help beginners to get started with hammerspoon (and spoons).

Let's getting started.

First, install hammerspoon, you can easily install it using homebrew:

brew cask install hammerspoon

And make sure these files and folders exists:

~/.hammerspoon
├── Spoons
├── modules
├── init.lua

Let's download our first spoon http://www.hammerspoon.org/Spoons/ReloadConfiguration.html.

Download it and double click to unzip and install it, it will be installed in the Spoons folder.

Then launch the hammerspoon console and input

hs.loadSpoon("AClock")
spoon.AClock:toggleShow()

Bingo! We just finish the hammerspoon version of "hello world" :)

You can see the clock display on the screen.

Now let's start your first config script, edit the init.lua file:

hs.loadSpoon("AClock")
hyper = {'cmd', 'alt'}

hs.hotkey.bind(hyper, 't', function() spoon.AClock:toggleShow() end)

save it and reload config. Then try to press cmd+option+t, the clock should appear again.

Windows manager

Now we can dive into a more complex task: windows manager.

Let's first define our hyper keys.

In modules/config.lua :

hyper = {'cmd', 'alt'}
hyperShift = {'alt', 'cmd', 'shift'}

Don't forget to download and install the winwin plugin: http://www.hammerspoon.org/Spoons/WinWin.html.

In init.lua we should load spoons and require other config files:

hs.loadSpoon("AClock")
hs.loadSpoon("WinWin")

require "modules/config"
require "modules/window"

and let's create a file named winodw.lua in modules folder to save the config about windows management.

In modules/window.lua

spoon.WindowScreenLeftAndRight:bindHotkeys({
  screen_left = { hyperShift, "Left" },
  screen_right= { hyperShift, "Right" },
})

spoon.WindowHalfsAndThirds:bindHotkeys({
  left_half   = { hyper, "Left" },
  right_half  = { hyper, "Right" },
  top_half    = { hyper, "Up" },
  bottom_half = { hyper, "Down" },
})

Thanks to these two awesome spoons, it's really easy to conifg our window movement.

Press cmd+opiton+[arrowkey] or cmd+option+shift+[arrowkey] to move windows in one minitor or around monitors.

I think these exmples are enough for beginners to use hammerspoon, but till now it seems that hammerspoon is just a windows management tool.

So next I will talk about layout, which makes hammerspoon more powerful than other window management tools.

Layouts

Chances are that you are using a MacBookPro with at least one external monitor.

I have two, so let's first find out their names.

In hammerspoon, insert these script:

> hs.screen.allScreens()[1]:name()
Color LCD
> hs.screen.allScreens()[2].name()
2340
> hs.screen.allScreens()[3].name()
LG ULTRAWIDE

Now we get monitors' names. We can modify modules/config.lua file to save them.

-- other config
macbook_monitor = "Color LCD"
main_monitor = "LG ULTRAWIDE"
second_monitor = "2340"

Let's come back to layout, so what is layout?

When you want to keep serveral apps open all the time, and have their windows arranged in a particular way, you can use the hs.layout extension

With multi-window layouts, you can easily setup your environments.

Let's say I place my three monitors(including the macbook screen) horizontally: Color LCD on the left, 2340 on the right, and LG ULTRAWIDE in the middle.

And I need three layouts for difference tasks.

Reading layout

There are so many ways to access to information, I want to open them at one time.

left: Emacs middle: Chrome and iBooks right: Email and Telegram

In modules/layout.lua file:

local reading_layout= {
  {"Emacs",         nil, macbook_monitor, hs.layout.maximized, nil, nil},
  {"Google Chrome", nil, main_monitor,    hs.layout.right50,   nil, nil},
  {"iBooks",        nil, main_monitor,    hs.layout.left50,    nil, nil},
  {"Telegram",      nil, second_monitor,  hs.layout.left50,    nil, nil},
  {"Mail",          nil, second_monitor,  hs.layout.right50,   nil, nil},
}

hs.hotkey.bind(hyper, '1', function()
  hs.application.launchOrFocus('Emacs')
  hs.application.launchOrFocus('Google Chrome')
  hs.application.launchOrFocus('iBooks')
  hs.application.launchOrFocus('Telegram')
  hs.application.launchOrFocus('Mail')

  hs.layout.apply(reading_layout)
end)

Reload config and press cmd+option+1, Woo, every window is on it's place.

Coding layout

When I am working, I need terminal, browser, editor, and database GUI client. Of course I need to communicate with my colleagues (I work remotely).

left: Terminal middle: Emacs and Chrome right: Station(IM) and TablePlus(Database GUI client)

local coding_layout= {
  {"Terminal",      nil, macbook_monitor, hs.layout.maximized, nil, nil},
  {"Google Chrome", nil, main_monitor,    hs.layout.left50,    nil, nil},
  {"Emacs",         nil, main_monitor,    hs.layout.right50,   nil, nil},
  {"Station",       nil, second_monitor,  hs.layout.left50,    nil, nil},
  {"TablePlus",     nil, second_monitor,  hs.layout.right50,   nil, nil},
}

hs.hotkey.bind(hyper, '2', function()
  hs.application.launchOrFocus('Terminal')
  hs.application.launchOrFocus('Google Chrome')
  hs.application.launchOrFocus('Emacs')
  hs.application.launchOrFocus('Station')
  hs.application.launchOrFocus('TablePlus')

  hs.layout.apply(coding_layout)
end)

Writing layout

Writing posts is difference from writing code, I don't want to be interrupted and I need music.

left: IINA(Media Player) middle: Emacs right: Chrome

local writing_layout= {
  {"Emacs",         nil, main_monitor,    hs.layout.maximized, nil, nil},
  {"Google Chrome", nil, second_monitor,  hs.layout.maximized, nil, nil},
  {"IINA",          nil, macbook_monitor, hs.layout.maximized, nil, nil},
}

hs.hotkey.bind(hyper, '3', function()
  hs.application.launchOrFocus('Google Chrome')
  hs.application.launchOrFocus('Emacs')
  hs.application.launchOrFocus('IINA')

  hs.layout.apply(writing_layout)
end)

Reacting events

These part of functions has nothing to do with windows management, they are very useful.

bring all the window front

Application like Finder may have multiple windows on every secreen. With this simple config, when you active any Finder window, all the other windows will be actived too.

function applicationWatcher(appName, eventType, appObject)
  if (eventType == hs.application.watcher.activated) then
    if (appName == "Finder") then
      -- Bring all Finder windows forward when one gets activated
      appObject:selectMenuItem({"Window", "Bring All to Front"})
    end
  end
end
appWatcher = hs.application.watcher.new(applicationWatcher)
appWatcher:start()

connect wifi

I have two routers in my house. In some rooms, the signal of router A is stronger, while in other rooms, router B is better. So I can config like this to make sure I can connect to the stronger one.

bedroomSSID = "MyBedroomNetwork"
studySSID = "MyStudyNetwork"
SSID = hs.wifi.currentNetwork()

hs.hotkey.bind(hyperShift, '9', function()
  if SSID ~= bedroomSSID then
    hs.wifi.associate(bedroomSSID, "myPassPhrase")
  end
end)

hs.hotkey.bind(hyperShift, '8', function()
  if SSID ~= studySSID then
    hs.wifi.associate(studySSID, "myPassPhrase")
  end
end)

caffeinate on the menu bar

I used to type caffeinate -t 99999 to make OS awake. But now I can create a menubar by the following config:

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

Conclusion

Hammerspoon is a powerful tool allowing you to have powerful effects on your system by writing Lua scripts. By the way, you can learn Lua by reading other's hammerspoon config files.

The more you know about hammerspoon, the more you can control your MacOS.

I have used hammerspoon for a long time, recently I found that the new version of hammerspoon supported Lua plugins called “Spoons”.

So I deceided to rewrite my hammerspoon config file with spoons.

I hope this post can help beginners to get started with hammerspoon (and spoons).