Skip to content

Hooks reference

Callbacks for responding to lifecycle and window events. This page is the canonical list of hooks and signatures. For guidance and examples, see Hooks guide.

Usage

Register hooks in the configure() call:

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

Both snake_case and camelCase are supported:

hooks = {
window_created = function(windowID) end, -- snake_case
onWindowCreated = function(windowID) end, -- camelCase (alias)
}

Lifecycle hooks

Called when Shoji starts, stops, or screen configuration changes.

started

Called after start() completes.

PropertyValue
Argumentsnone
AliasesonStart, onStarted
started = function()
print("Shoji started")
end

stopped

Called after stop() completes.

PropertyValue
Argumentsnone
AliasesonStop, onStopped
stopped = function()
print("Shoji stopped")
end

screen_changed

Called when screen configuration changes: monitor connected/disconnected or resolution changed.

PropertyValue
Argumentsnone
AliasonScreenChanged
screen_changed = function()
print("Screen configuration changed")
end

Window hooks

Called when windows are created, destroyed, or change visibility.

window_created

Called when a new window is added to tiling.

PropertyValue
ArgumentswindowID (number)
AliasonWindowCreated
window_created = function(windowID)
print("Window created: " .. windowID)
end

window_destroyed

Called when a window is closed or removed from tiling.

PropertyValue
ArgumentswindowID (number)
AliasonWindowDestroyed
window_destroyed = function(windowID)
print("Window destroyed: " .. windowID)
end

window_focused

Called when a window receives focus.

PropertyValue
ArgumentswindowID (number)
AliasonWindowFocused
window_focused = function(windowID)
print("Window focused: " .. windowID)
end

window_moved

Called when a window is moved.

PropertyValue
ArgumentswindowID (number)
AliasonWindowMoved
window_moved = function(windowID)
print("Window moved: " .. windowID)
end

window_visible

Called when a window becomes visible (unhidden or unminimized).

PropertyValue
ArgumentswindowID (number)
AliasonWindowVisible
window_visible = function(windowID)
print("Window visible: " .. windowID)
end

window_hidden

Called when a window becomes hidden (minimized or hidden).

PropertyValue
ArgumentswindowID (number)
AliasonWindowHidden
window_hidden = function(windowID)
print("Window hidden: " .. windowID)
end

Tiling hooks

Called before and after tiling operations.

before_tile

Called immediately before tiling a space.

PropertyValue
ArgumentsspaceID (number)
AliasonBeforeTile
before_tile = function(spaceID)
print("About to tile space: " .. spaceID)
end

after_tile

Called immediately after tiling a space completes.

PropertyValue
ArgumentsspaceID (number)
AliasesonAfterTile, onTilingComplete
after_tile = function(spaceID)
print("Finished tiling space: " .. spaceID)
end

State change hooks

Called when layout or window state changes.

float_toggled

Called when a window’s floating state changes.

PropertyValue
ArgumentswindowID (number), floating (boolean)
AliasesonFloatToggled, onWindowFloatToggled
float_toggled = function(windowID, isFloating)
local state = isFloating and "floating" or "tiled"
print("Window " .. windowID .. " is now " .. state)
end

layout_changed

Called when the active layout changes for a space.

PropertyValue
ArgumentsspaceID (number), layout, prev (LayoutName|nil)
AliasonLayoutChanged
layout_changed = function(spaceID, layout, prevLayout)
print("Layout changed: " .. (prevLayout or "none") .. " -> " .. layout)
end

space_changed

Called when switching to a different desktop/space.

PropertyValue
ArgumentsspaceID (number)
AliasonSpaceChanged
space_changed = function(spaceID)
print("Switched to space: " .. spaceID)
end

Multiple callbacks

Pass an array to register multiple callbacks for the same hook:

hooks = {
after_tile = {
function(spaceID)
print("First callback for space: " .. spaceID)
end,
function(spaceID)
print("Second callback for space: " .. spaceID)
end,
},
}

Callbacks run in order. If one fails, the others still execute.


Error handling

Hooks run inside pcall. A failing hook logs an error but does not crash Shoji or prevent other hooks from running:

hooks = {
after_tile = function()
error("This error is logged, not thrown")
end,
}

Check the Hammerspoon console (Cmd+Alt+C) for hook errors.


Recursion limit

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

For example, calling retile() from after_tile triggers another after_tile. This continues up to 10 times before stopping with a warning.