Partition
Partition divides the workarea into regions, each with its own layout. Create IDE-style layouts, presentation modes, or any custom arrangement.
At a glance
- Divide the screen into regions with independent layouts
horizontal= left-to-right,vertical= top-to-bottom- Windows stay in their focused region (region affinity)
- Partitions nest: regions can contain other Partitions
Quick start
Add a two-region layout in five lines:
local Partition = require("layouts.combinators.partition")local Tall = require("layouts.tall")local Column = require("layouts.column")
spoon.Shoji:configure({ layouts = { Partition.horizontal({ { ratio = 0.65, layout = Tall, count = 1 }, { ratio = 0.35, layout = Column }, }, { name = "dev", displayName = "Dev" }), }, enabled_layouts = { "tall", "wide", "dev" },})This gives you a 65/35 split: one main window on the
left, remaining windows stacked on the right. Cycle to
it with cycle_layout_forward or set it directly with
set_layout_dev.
The sections below cover every Partition option in detail.
Horizontal split
Divide left to right:
local Partition = require("layouts.combinators.partition")local Tall = require("layouts.tall")local Column = require("layouts.column")
local devLayout = Partition.horizontal({ { ratio = 0.65, layout = Tall, count = 1 }, { ratio = 0.35, layout = Column },}, { name = "developer", displayName = "Developer",})┌─────────────────────────────────────────┬─────────────────┐│ │ ││ │ 2 ││ │ ││ ├─────────────────┤│ │ ││ │ 3 ││ │ ││ ├─────────────────┤│ 1 │ ││ │ 4 ││ │ ││ ├─────────────────┤│ │ ││ │ ││ │ 5 ││ │ ││ │ │└─────────────────────────────────────────┴─────────────────┘This creates:
- Left 65%: First window in tall layout
- Right 35%: Remaining windows stacked in column
Vertical split
Divide top to bottom:
local Partition = require("layouts.combinators.partition")local Monocle = require("layouts.monocle")local Column = require("layouts.column")
local presentLayout = Partition.vertical({ { ratio = 0.85, layout = Monocle, count = 1 }, { ratio = 0.15, layout = Column },}, { name = "presentation", displayName = "Presentation",})┌───────────────────────────────────────────────────────────┐│ ││ ││ ││ ││ ││ ││ 1 ││ ││ ││ ││ ││ ││ │├───────────────────┬───────────────────┬───────────────────┤│ │ │ ││ 2 │ 3 │ 4 ││ │ │ │└───────────────────┴───────────────────┴───────────────────┘This creates:
- Top 85%: Main presentation window
- Bottom 15%: Speaker notes in column
Window assignment
Each region specifies a preferred window count:
count = N: Initially assign N windows to this regioncountomitted: Assign all remaining windows
Initial distribution
On first layout or after balance_regions, windows are
distributed by declared count values in order. The first
region gets its count windows, then the second, and so on.
The last region (or any region without count) receives
whatever remains.
Region affinity
After initial distribution, region affinity takes over. Each window remembers which region it belongs to:
- New windows join the focused region, not the one with the fewest windows.
send_to_next_region/send_to_prev_regionmoves a window’s affinity permanently — it stays in the new region across retiles.- Closing a window removes it from its region. The remaining windows in that region stay put.
count is the preferred count — the initial distribution
target, not a hard cap. After affinity kicks in, any region
can hold any number of windows.
Partition.horizontal({ { ratio = 0.5, layout = Tall, count = 2 }, { ratio = 0.5, layout = Grid },})Open a new terminal while focused in the left region and it appears there, not in the right.
Region actions
Move windows between regions or rebalance to declared counts:
| Action | Description |
|---|---|
send_to_next_region | Move focused window to next region |
send_to_prev_region | Move focused window to previous region |
balance_regions | Reset regions to declared counts |
Bind them like any other action:
spoon.Shoji:bindHotkeys({ send_to_next_region = { { "cmd", "shift" }, "]" }, send_to_prev_region = { { "cmd", "shift" }, "[" }, balance_regions = { { "cmd", "shift" }, "=" },})These actions work alongside any actions from child layouts (e.g., BSP rotate, ratio adjustments).
Region gap
Control spacing between partition regions with the gap option:
local devLayout = Partition.horizontal({ { ratio = 0.65, layout = Tall, count = 1 }, { ratio = 0.35, layout = Column },}, { name = "developer", displayName = "Developer", gap = 20,})Gap is applied only between regions — outer edges remain flush
with the workarea boundary. This is independent of the global
gap_inner/gap_outer settings, which control gaps within
each child layout.
Gap interaction
Three gap layers affect window placement in a Partition. From outermost to innermost:
┌───────────────────────────────────────────────────────────┐│ gap_outer ││ ┌────────────────────────┐ ┌────────────────────────┐ ││ │ ┌────────────────────┐ │ │ ┌────────────────────┐ │ ││ │ │ │ │ │ │ │ │ ││ │ │ Window 1 │ │ │ │ Window 3 │ │ ││ │ │ │ │ │ │ │ │ ││ │ └────────────────────┘ │ │ └────────────────────┘ │ ││ │ gap_inner │ │ gap_inner │ ││ │ ┌────────────────────┐ │ │ ┌────────────────────┐ │ ││ │ │ │ │ │ │ │ │ ││ │ │ Window 2 │ │ │ │ Window 4 │ │ ││ │ │ │ │ │ │ │ │ ││ │ └────────────────────┘ │ │ └────────────────────┘ │ ││ └────────────────────────┘ └────────────────────────┘ ││ ▲ ││ Partition gap │└───────────────────────────────────────────────────────────┘gap_outer: Space between the screen edge and the tiling area. Applied once around the entire workarea.- Partition
gap: Space between adjacent regions. Set per-Partition via thegapoption. gap_inner: Space between windows within each region. Applied by the child layout.
Nested partition
Partition layouts can contain other Partition layouts:
local Partition = require("layouts.combinators.partition")local Tall = require("layouts.tall")local Grid = require("layouts.grid")local Monocle = require("layouts.monocle")
local ideLayout = Partition.horizontal({ { ratio = 0.6, layout = Tall, count = 2 }, { ratio = 0.4, layout = Partition.vertical({ { ratio = 0.5, layout = Grid, count = 2 }, { ratio = 0.5, layout = Monocle }, })},}, { name = "ide", displayName = "IDE Layout",})This creates:
- Left 60%: 2 main windows (tall layout)
- Right top 20%: 2 windows in grid
- Right bottom 20%: Remaining windows monocle
Nested region cycling
When using send_to_next_region and send_to_prev_region
with nested Partitions, cycling traverses leaf regions
in reading order (left-to-right, top-to-bottom).
Consider the IDE layout above: the outer horizontal Partition has 2 regions, but the right region is itself a vertical Partition with 2 sub-regions. This yields 3 leaf regions:
┌─────────────────────────────────────────┬─────────────────┐│ │ ││ │ ││ │ ││ │ Leaf 2 ││ │ ││ │ ││ │ ││ ├─────────────────┤│ Leaf 1 │ ││ │ ││ │ ││ │ Leaf 3 ││ │ ││ │ ││ │ ││ │ ││ │ ││ │ │└─────────────────────────────────────────┴─────────────────┘Cycling from Leaf 1 goes to Leaf 2, then Leaf 3, then wraps back to Leaf 1. The order follows the visual layout: left region first, then top-right, then bottom-right.
balance_regions resets the outer Partition only; nested
Partitions redistribute independently via their own
balance_regions action.
Ratio normalization
Ratios are normalized to sum to 1.0. These are equivalent:
-- Explicit fractions{ ratio = 0.6 }, { ratio = 0.4 }
-- Any proportional values{ ratio = 3 }, { ratio = 2 } -- Same as 0.6, 0.4{ ratio = 60 }, { ratio = 40 } -- Same as 0.6, 0.4Error handling
Partition returns nil and an error string on invalid input:
-- Partition requires valid splitslocal layout, err = Partition.horizontal({ { ratio = 0, layout = Tall }, -- ratio must be positive})-- err = "ratio at index 1 must be a positive number"State management
Partition maintains state for child layouts. Stateful layouts (like BSP with custom splits) preserve their state when wrapped in Partition. State persists across layout cycles and Hammerspoon restarts.
Common mistakes
Omitting count on non-last splits:
Without count, all remaining windows land in that region.
Only the last split should omit count to collect the
rest.
Confusing Partition gap with gap_inner:
Partition gap controls spacing between regions.
gap_inner controls spacing between windows within each
region. They are independent settings.
Expecting balance_regions to reset nested Partitions:
balance_regions only resets the outer Partition. Nested
Partitions redistribute independently via their own
balance_regions action.
Common errors
“ratio at index N must be a positive number”: A ratio is zero or negative. All ratios must be positive numbers.
Child layout validation errors: The child layout table
is missing name or arrange. Every layout passed to a
split must satisfy the layout protocol.