Skip to content

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 region
  • count omitted: 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_region moves 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:

ActionDescription
send_to_next_regionMove focused window to next region
send_to_prev_regionMove focused window to previous region
balance_regionsReset 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 the gap option.
  • 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.4

Error handling

Partition returns nil and an error string on invalid input:

-- Partition requires valid splits
local 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.