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.

Register hooks in the configure() call:

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

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

Called after start() completes.

Property Value
Arguments none
started = function()
print("Shoji started")
end

Called after stop() completes.

Property Value
Arguments none
stopped = function()
print("Shoji stopped")
end

Called when screen configuration changes: monitor connected/disconnected or resolution changed. Fires before tiling begins. Use after_tile for post-tiling actions.

Property Value
Arguments none
screen_changed = function()
print("Screen configuration changed")
end

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

Called when a new window is added to tiling. Fires before tiling begins. Use after_tile for post-tiling actions.

Property Value
Arguments windowID (WindowID)
window_created = function(windowID)
print("Window created: " .. windowID)
end

Called when a window is closed or removed from tiling. Fires before tiling begins. Use after_tile for post-tiling actions.

Property Value
Arguments windowID (WindowID)
window_destroyed = function(windowID)
print("Window destroyed: " .. windowID)
end

Called when a window receives focus.

Property Value
Arguments windowID (WindowID)
window_focused = function(windowID)
print("Window focused: " .. windowID)
end

Called when a window is moved. Fires before tiling begins. Use after_tile for post-tiling actions.

Property Value
Arguments windowID (WindowID)
window_moved = function(windowID)
print("Window moved: " .. windowID)
end

Called when a window becomes visible (unhidden or unminimized). Fires before tiling begins. Use after_tile for post-tiling actions.

Property Value
Arguments windowID (WindowID)
window_visible = function(windowID)
print("Window visible: " .. windowID)
end

Called when a window becomes hidden (minimized or hidden). Fires before tiling begins. Use after_tile for post-tiling actions.

Property Value
Arguments windowID (WindowID)
window_hidden = function(windowID)
print("Window hidden: " .. windowID)
end

Called before and after tiling operations.

Called immediately before tiling a space.

Property Value
Arguments spaceID (SpaceID)
before_tile = function(spaceID)
print("About to tile space: " .. spaceID)
end

Called immediately after tiling a space completes.

Property Value
Arguments spaceID (SpaceID)
after_tile = function(spaceID)
print("Finished tiling space: " .. spaceID)
end

Called when layout or window state changes.

Called when a window’s floating state changes.

Property Value
Arguments windowID (WindowID), floating (boolean)
float_toggled = function(windowID, isFloating)
local state = isFloating and "floating" or "tiled"
print("Window " .. windowID .. " is now " .. state)
end

Called when the active layout changes for a space.

Property Value
Arguments spaceID (SpaceID), layout (LayoutName), prev (LayoutName|nil)
layout_changed = function(spaceID, layout, prevLayout)
print("Layout changed: " .. (prevLayout or "none") .. " -> " .. layout)
end

Called when switching to a different desktop/space. Fires before tiling begins. Use after_tile for post-tiling actions.

Property Value
Arguments spaceID (SpaceID)
space_changed = function(spaceID)
print("Switched to space: " .. spaceID)
end

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.

Property Value
Arguments spaceID (SpaceID)
space_initialized = function(spaceID)
-- Set BSP as default layout for new spaces
spoon.Shoji.state:setSpaceLayout(spaceID, "bsp")
end

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.


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.


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.


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

Method Returns Description
getSpaceLayout(spaceID) LayoutName Active layout for the space
getWindowOrder(spaceID) WindowID[] Tiling window IDs in order
getMainRatio(spaceID) number Main area ratio (0.1–0.9)
getMainCount(spaceID) integer Number of main windows
isWindowFloating(windowID) boolean Whether the window is floating
getLayoutState(spaceID) table Layout-specific state (e.g., BSP splits)
Method Description
setSpaceLayout(spaceID, layout) Set the active layout
setMainRatio(spaceID, ratio) Set the main ratio
setMainCount(spaceID, count) Set the main window count
hooks = {
after_tile = function(spaceID)
local layout =
spoon.Shoji.state:getSpaceLayout(spaceID)
local count =
#spoon.Shoji.state:getWindowOrder(spaceID)
print(layout .. ": " .. count .. " windows")
end,
}