Skip to content

Chaining modifiers

Combine modifiers to build layout variations. Start with a base layout and layer on transformations.

The chain function

Modifiers.chain applies modifiers in sequence:

local Modifiers = require("layouts.modifiers")
local Magnifier = require("layouts.modifiers.magnifier")
local Mirror = require("layouts.modifiers.mirror")
local Tall = require("layouts.tall")
local myLayout = Modifiers.chain(Tall, {
Mirror.horizontal(),
Magnifier.create({ ratio = 1.3 }),
})

Execution order

Modifiers execute in array order:

  1. Base layout computes geometries
  2. First modifier transforms the result
  3. Second modifier transforms that output
  4. And so on…
-- Order: Tall -> Mirror -> Magnifier
Modifiers.chain(Tall, {
Mirror.horizontal(), -- First: flip positions
Magnifier.create(), -- Second: enlarge focused
})
-- Different order: Tall -> Magnifier -> Mirror
Modifiers.chain(Tall, {
Magnifier.create(), -- First: enlarge focused
Mirror.horizontal(), -- Second: flip (including enlarged window)
})

Order produces different results. In the first example, positions flip before magnification. In the second, magnification happens first, then the enlarged window flips with everything else.

Naming chained layouts

Chained layouts auto-generate names from modifier metadata:

local layout = Modifiers.chain(Tall, {
Mirror.horizontal(),
Magnifier.create(),
})
-- layout.name = "tall+mirror-h+magnify"
-- layout.displayName = "Tall + Mirror H + Magnify"

Override with explicit names:

local layout = Modifiers.chain(Tall, {
Mirror.horizontal(),
Magnifier.create(),
}, {
name = "tall-right-mag",
displayName = "Tall Right + Magnify",
})

Preserving actions

Chained layouts don’t inherit base layout actions by default. To preserve them, pass preserveActions = true:

local layout = Modifiers.chain(Tall, {
Mirror.horizontal(),
}, {
preserveActions = true,
})

preserveActions is only controlled by the options table.

Examples

Mirrored magnified tall

Master on the right with focused window enlarged:

local Modifiers = require("layouts.modifiers")
local Magnifier = require("layouts.modifiers.magnifier")
local Mirror = require("layouts.modifiers.mirror")
local Tall = require("layouts.tall")
local tallRightMag = Modifiers.chain(Tall, {
Mirror.horizontal(),
Magnifier.create({ ratio = 1.4 }),
}, {
name = "tall-right-mag",
displayName = "Tall (Right) + Magnify",
})
spoon.Shoji:configure({
layouts = { tallRightMag },
enabled_layouts = { "tall", "tall-right-mag" },
retile_on_focus = true, -- Update magnifier on focus change
})

BSP with custom gaps

BSP with tighter spacing than global defaults:

local Modifiers = require("layouts.modifiers")
local Gaps = require("layouts.modifiers.gaps")
local BSP = require("layouts.bsp")
local tightBSP = Modifiers.chain(BSP, {
Gaps.add({ inner = 2, outer = 4 }),
}, {
name = "bsp-tight",
displayName = "BSP (Tight)",
preserveActions = true, -- Keep BSP resize/rotate actions
})

Centered layout

Scale and center at 70% of screen size:

local Modifiers = require("layouts.modifiers")
local Centered = require("layouts.modifiers.centered")
local Tall = require("layouts.tall")
local centeredTall = Modifiers.chain(Tall, {
Centered.ratio(0.7),
}, {
name = "tall-centered",
displayName = "Tall (Centered)",
})

Error handling

chain returns nil and an error for invalid input:

-- Empty modifier list
local layout, err = Modifiers.chain(Tall, {})
-- err = "at least one modifier required"
-- Invalid modifier
local layout, err = Modifiers.chain(Tall, { "not a function" })
-- err = "modifier at index 1 must be a function"

Debugging

Add a pass-through modifier to inspect intermediate geometries:

local function debugModifier(geometries, params)
print("Geometries after previous modifier:")
for id, frame in pairs(geometries) do
print(string.format(" %d: x=%d y=%d w=%d h=%d",
id, frame.x, frame.y, frame.w, frame.h))
end
return geometries -- Pass through unchanged
end
local debuggedLayout = Modifiers.chain(Tall, {
Mirror.horizontal(),
debugModifier,
Magnifier.create(),
})