Skip to content

Hooks

Hooks run custom code when specific events occur. Use them for logging, integrations, or extending Shoji’s behavior. This page focuses on patterns and examples. For the canonical list of hook names and arguments, see Hooks reference.

At a glance

  • Hook names and arguments live in the Hooks reference
  • Use hooks = { event = fn } and arrays for multiple callbacks
  • Callbacks run in pcall and are guarded against recursion
  • Examples below show common patterns

Registering hooks

Via configuration

Pass hooks through configuration:

spoon.Shoji:configure({
hooks = {
layout_changed = function(spaceID, layout, prevLayout)
print("Layout changed to: " .. layout)
end,
},
})

At runtime

Register hooks dynamically with spoon.Shoji:on():

spoon.Shoji:on("after_tile", function(spaceID)
print("Tiled space: " .. spaceID)
end)

This is useful for adding hooks after Shoji has started, or from other Spoons.

Multiple callbacks

Multiple callbacks for the same event:

spoon.Shoji:configure({
hooks = {
after_tile = {
function(spaceID) print("Tiled space " .. spaceID) end,
function(spaceID) hs.alert.show("Retiled!") end,
},
},
})

Hook list

For a complete list of hook names and arguments, see Hooks reference.

Hook aliases

Both snake_case and camelCase work:

hooks = { window_created = fn }
hooks = { onWindowCreated = fn } -- equivalent

Error handling

Callbacks are wrapped in pcall, so errors won’t crash Shoji. Errors log to the Hammerspoon console.

Recursion guard

Hooks have a depth limit to prevent infinite loops. If a hook triggers an action that fires the same hook, the chain stops after 10 levels.

Examples

Layout-specific wallpaper

local wallpapers = {
tall = "~/Pictures/tall.jpg",
wide = "~/Pictures/wide.jpg",
fullscreen = "~/Pictures/fullscreen.jpg",
}
spoon.Shoji:configure({
hooks = {
layout_changed = function(_spaceID, layout, _prevLayout)
local wallpaper = wallpapers[layout]
if wallpaper then
hs.screen.mainScreen():desktopImageURL("file://" .. wallpaper)
end
end,
},
})

Log window events

spoon.Shoji:configure({
hooks = {
window_created = function(windowID)
local win = hs.window.get(windowID)
if win then
print("[+] " .. (win:title() or "untitled"))
end
end,
window_destroyed = function(windowID)
print("[-] Window " .. windowID .. " closed")
end,
},
})

Auto-float specific apps

local autoFloatApps = {
["com.apple.finder"] = true,
["com.apple.Preview"] = true,
}
spoon.Shoji:configure({
hooks = {
window_created = function(windowID)
local win = hs.window.get(windowID)
if not win then return end
local app = win:application()
if app and autoFloatApps[app:bundleID()] then
spoon.Shoji.state:toggleWindowFloating(windowID)
end
end,
},
})

Alert on retile

spoon.Shoji:configure({
hooks = {
after_tile = function(spaceID)
hs.alert.show("macOS Space " .. spaceID .. " retiled", 0.3)
end,
},
})