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,
},
})

Lifecycle hooks

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

started

Called after start() completes.

PropertyValue
Argumentsnone
started = function()
print("Shoji started")
end

stopped

Called after stop() completes.

PropertyValue
Argumentsnone
stopped = function()
print("Shoji stopped")
end

screen_changed

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

PropertyValue
Argumentsnone
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 (WindowID)
window_created = function(windowID)
print("Window created: " .. windowID)
end

window_destroyed

Called when a window is closed or removed from tiling.

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

window_focused

Called when a window receives focus.

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

window_moved

Called when a window is moved.

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

window_visible

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

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

window_hidden

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

PropertyValue
ArgumentswindowID (WindowID)
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 (SpaceID)
before_tile = function(spaceID)
print("About to tile space: " .. spaceID)
end

after_tile

Called immediately after tiling a space completes.

PropertyValue
ArgumentsspaceID (SpaceID)
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 (WindowID), floating (boolean)
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 (SpaceID), layout (LayoutName), prev (LayoutName|nil)
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 (SpaceID)
space_changed = function(spaceID)
print("Switched to space: " .. spaceID)
end

space_initialized

Called when a space is tiled for the first time (no prior state). Fires once per space, before reconcileWindowOrder creates the space entry. Use this to set per-space defaults (layout, ratio, main count) before the first tile applies them.

PropertyValue
ArgumentsspaceID (SpaceID)
space_initialized = function(spaceID)
-- Set BSP as default layout for new spaces
spoon.Shoji.state:setSpaceLayout(spaceID, "bsp")
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 is blocked at depth 10 (the 10th nested callback does not execute).

For example, calling retile() from after_tile triggers another after_tile. The chain continues until depth 10, where it is blocked with a warning.


State queries for hooks and scripts

Hooks and scripts can query Shoji’s state via spoon.Shoji.state. These are the most useful methods for hook callbacks and IPC scripting:

Reading state

MethodReturnsDescription
getSpaceLayout(spaceID)LayoutNameActive layout for the space
getWindowOrder(spaceID)WindowID[]Tiling window IDs in order
getMainRatio(spaceID)numberMain area ratio (0.1–0.9)
getMainCount(spaceID)integerNumber of main windows
isWindowFloating(windowID)booleanWhether the window is floating
getLayoutState(spaceID)tableLayout-specific state (e.g., BSP splits)

Writing state

MethodDescription
setSpaceLayout(spaceID, layout)Set the active layout
setMainRatio(spaceID, ratio)Set the main ratio
setMainCount(spaceID, count)Set the main window count

Example

hooks = {
after_tile = function(spaceID)
local layout =
spoon.Shoji.state:getSpaceLayout(spaceID)
local count =
#spoon.Shoji.state:getWindowOrder(spaceID)
print(layout .. ": " .. count .. " windows")
end,
}