Layout
Tiling layouts, gaps, borders, window corners, and the special workspace
sweets.layout
Selects the tiling algorithm and controls master sizing, new-window placement, drag reordering, and cycling.
sweets.layout({
default = "tile",
master = 50,
master_count = 1,
master_position = "left",
new_window = "master",
reorder_ratio = 0.3,
floating_step = 50,
gaps_step = 5,
cycle = { "tile", "monocle", "centered", "grid", "columns", "deck", "dwindle", "scrolling" },
struts = { left = 48, right = 48 },
})| Field | Type | Default | Description |
|---|---|---|---|
default | string | "tile" | Initial layout for each workspace |
master | integer | 50 | Master area size in percent, 10 to 90 |
master_count | integer | 1 | Windows in the master area, 1 to 16 |
master_position | string | "left" | "left", "right", "top", or "bottom" |
new_window | string | "master" | Insert new tiled windows at "master" or "stack" |
reorder_ratio | number | 0.3 | Drag depth needed to reorder, 0.1 to 0.9 |
floating_step | integer | 50 | Keyboard floating move and resize step, 1 to 1024 |
gaps_step | integer | 5 | Runtime inner or outer gap adjustment, 1 to 4096 |
cycle | array | all layouts | Order used by the layout-cycle actions |
struts | integer or table | 0 | Margin no window may claim, 0 to 4096 per edge |
Every workspace keeps its own layout state. Changing one does not affect another.
Struts
Struts reserve a margin on each screen edge that no window can use — a camera
notch, a bezel, or simply space off the screen edge. Write one number for all
four edges, or a table naming any of top, bottom, left, and right. An
omitted side is 0.
sweets.layout({ struts = { top = 32 } })Struts are taken after a panel's reserved space, so they add to it rather than replace it. A monitor rule can set its own. They do not move layer-shell surfaces such as bars, and clients cannot see or claim them. Negative values are rejected. A margin too large for an output is reduced so the tiling area keeps at least one pixel.
Layouts
| Name | Arrangement |
|---|---|
tile | One master area beside a stack of the rest |
monocle | Every window fills the work area; only the focused one shows |
centered | A centered master with the rest split across both sides |
grid | An even grid, filled row by row |
columns | One weighted full-height column or full-width row per window |
deck | A master beside a shared stack slot showing its focused member |
dwindle | Each new window recursively splits the remaining area |
scrolling | Columns on a strip wider than the screen, scrolled to the focused one |
Exceptions to master, master_count, and master_position:
monocleignores both, and tiled resize does nothing. Floating windows stay visible above it, and fullscreen still works.gridignores all three. A short final row expands across the width.columnsignoresmasterandmaster_count.rightandbottomreverse the visual order.centeredreadsleft/rightas a vertical master column andtop/bottomas a horizontal row. The exact side is remembered when you switch layouts.decksupports all three, but only the master-to-stack divider resizes. Its masters always share the master area equally.dwindleusesmasterfor the outer split. Deeper splits start at half and resize independently. It ignoresmaster_count.scrollingignores both. Resizing changes column widths and heights instead. See Scrolling.
Scrolling
The scrolling layout opens each tiled window as a column on a strip. The strip
can be wider than the screen, and each workspace scrolls to show the focused
column.
sweets.layout({ default = "scrolling" })
sweets.scrolling({
column_width = 0.5,
column_widths = { 0.333, 0.5, 0.667, 1.0 },
resize_step = 0.1,
focus = "fit",
follow_min_visible = 0.0,
wrap_focus = false,
single_column = "fill",
narrow_strip = "center",
direction = "right",
})| Field | Type | Default | Description |
|---|---|---|---|
column_width | number | 0.5 | Share of the screen up to 1, from 0.05; whole logical pixels above 1 |
column_widths | array | { 0.333, 0.5, 0.667, 1.0 } | Widths cycle_column_width steps through, one to eight different widths |
resize_step | number | 0.1 | One resize_grow or resize_shrink step, 0.01 to 0.5 of the screen |
focus | string | "fit" | Where focusing puts a column: "fit", "center", or "center_on_overflow" |
follow_min_visible | number | 0.0 | How much of a window must already be on screen for the pointer to scroll to it. 0 never scrolls |
wrap_focus | boolean | false | Whether focus wraps at the ends of the strip |
single_column | string | "fill" | A lone column: "fill", "center", or "start" |
narrow_strip | string | "center" | Columns narrower than the screen: "center" or "start" |
direction | string | "right" | Which way the strip grows: "right", "left", "down", or "up" |
- A new window opens as the column after the focused one. So does a window moved here from another workspace or monitor.
- A workspace moved to a monitor of another size keeps its focused column on screen.
- Closing a column you are not looking at leaves the view in place.
- A column partly off the screen is cut at the screen edge. It never appears on another monitor.
Where focus puts a column
focus | Result |
|---|---|
"fit" | Scroll only as far as needed. A column already on screen does not move the view |
"center" | Put the column in the middle of the screen |
"center_on_overflow" | Fit while the column and the one the view is anchored to both fit; center once they do not |
No policy scrolls past either end of the strip, so an end column stops there.
A column wider than the screen lines its left edge up with the screen's.
center_window centers the focused column whatever focus says.
Clicking a partly visible window scrolls it into view when you let go of the button. Dragging a window does not.
Moving the pointer onto a partly visible window focuses it without scrolling.
Raise follow_min_visible to scroll instead, once that share of the window is
already on screen. 0.4 is a good starting point.
With wrap_focus, focus_left and focus_right continue from one end of the
strip to the other. A monitor on that side still wins, and focus up or down out
of a column still goes to the next monitor rather than wrapping.
Columns
A column can hold several windows stacked top to bottom.
| Action | Result |
|---|---|
focus_left, focus_right | Focus the neighbouring column's most recently used window |
focus_up, focus_down | Focus within the column |
move_left, move_right | Move a lone window's column one place; move a stacked window out into a new column on that side |
move_up, move_down | Reorder the window within its column |
consume_or_expel_left, consume_or_expel_right | Stack a lone window into the neighbouring column; move a stacked one out |
move_column_left, move_column_right | Move the whole column one place |
focus_column_first, focus_column_last | Focus the first or last column |
sweets.bind("MOD+Comma", "consume_or_expel_left")
sweets.bind("MOD+Period", "consume_or_expel_right")
sweets.bind("MOD+Ctrl+Shift+Left", "move_column_left")
sweets.bind("MOD+Ctrl+Shift+Right", "move_column_right")
sweets.bind("MOD+Home", "focus_column_first")
sweets.bind("MOD+End", "focus_column_last")Each action scrolls the column it acts on into view. The column actions do
nothing on other layouts. At the end of the strip or column, directional
actions continue to the next monitor when focus_cross_monitor or
move_cross_monitor allows it.
Column sizes
| Action | Result |
|---|---|
resize_grow, resize_shrink | Widen or narrow the focused column by resize_step |
resize_grow_vertical, resize_shrink_vertical | Trade height with a neighbour in the same column |
cycle_column_width, cycle_column_width_reverse | Step through column_widths |
toggle_full_width | Fill the screen with the focused column, or return it to its width |
expand_column | Widen the column into the space the other fully visible columns leave |
sweets.bind("MOD+Ctrl+R", "cycle_column_width")
sweets.bind("MOD+Ctrl+Shift+R", "cycle_column_width_reverse")
sweets.bind("MOD+Ctrl+F", "toggle_full_width")
sweets.bind("MOD+Ctrl+E", "expand_column")- Widening a column pushes the columns after it along. It takes no width from its neighbours.
- A column keeps the width you give it. Columns you have not resized follow
column_width, including after a reload. - A window stacked into a column takes that column's width.
- A window's own minimum and maximum size limit its column. An action that cannot change any size does nothing.
- A lone column with
single_column = "fill"always fills the screen.
The column_width window rule opens a
window at its own width.
Dragging and resizing with the mouse
A MOD+left drag lets go of a window by where the pointer is:
| Where you let go | Result |
|---|---|
| Over the middle of a column | The window joins it, above or below the window under the pointer |
| Within 30% of a column's left or right edge | The window becomes a new column on that side |
| Between columns or past the end | The window becomes a new column at the nearest gap |
The edges count from what is on screen, so a column mostly scrolled away is joined by dropping over its visible part. This works the same when the drag comes from another monitor. The dropped window scrolls into view.
A MOD+right drag resizes the column under the pointer:
- The right half drags the column's right edge. Its left edge stays put.
- The left half drags its left edge. The right edge stays put and the view scrolls with the drag, until the start of the strip is reached.
- In a column of several windows, the upper or lower half moves the divider to the window above or below.
Direction
The rest of this section describes the default direction = "right". Other
directions turn the strip:
"left"starts at the right edge of the screen and grows left."down"and"up"stack columns vertically. Windows in a column sit side by side.
sweets.scrolling({ direction = "down" })cycle_master_position turns the strip on the current workspace. It goes
right, down, left, up, and cycle_master_position_reverse goes back. The turn
lasts until reset_layout, which restores the configured direction.
sweets.bind("MOD+I", "cycle_master_position")
sweets.bind("MOD+Shift+I", "cycle_master_position_reverse")On a vertical strip, the arrow directions swap roles:
| Action | Result on "down" or "up" |
|---|---|
focus_up, focus_down, move_up, move_down | Work between columns |
focus_left, focus_right, move_left, move_right | Work within the column |
resize_grow_vertical, resize_shrink_vertical | Change the column's size |
resize_grow, resize_shrink | Trade size with a neighbour in the column |
consume_or_expel_left, move_column_left and the other _left column
actions always work toward the first column, whatever the direction.
The mouse rules above turn with the strip. On "left", swap left and right.
On "down" or "up", read left and right as the edges facing the start and
end of the strip, and upper and lower as left and right.
Cycling layouts
sweets.bind("MOD+W", "cycle_layout")
sweets.bind("MOD+Shift+W", "cycle_layout_reverse")Without cycle, the order is
tile → monocle → centered → grid → columns → deck → dwindle → scrolling. Restrict or
reorder it freely:
sweets.layout({ cycle = { "tile", "centered", "monocle" } })The list needs at least one entry and no duplicates. Both actions wrap. If the active layout is not in the list, forward cycling picks the first entry and reverse picks the last.
Select one layout directly with the layout action:
sweets.bind("MOD+G", "layout", "grid")Master position
Rotate the active workspace at runtime:
sweets.bind("MOD+I", "cycle_master_position")
sweets.bind("MOD+Shift+I", "cycle_master_position_reverse")Centered toggles between its two axes. Monocle and grid have no orientation, so these do nothing there.
Master count
master_count puts more than one window in the master area of tile,
centered, and deck. The masters split the master area along the stack, so
with master_position = "left" they sit one above another.
sweets.layout({ master_count = 2 })
sweets.bind("MOD+bracketright", "add_master")
sweets.bind("MOD+bracketleft", "remove_master")add_master and remove_master change the count on the current workspace
only. They stop at one master and at the number of tiled windows, so every
press changes the layout. With every window a master, the master area fills
the whole screen. centered keeps its masters in the middle column.
A reload leaves a count you changed this way alone. reset_layout returns to
the configured master_count.
Master swap
Send the focused window to the master slot, or bring it back:
sweets.bind("MOD+Shift+Return", "swap_with_master")A window that is already master swaps with the stack member you focused most recently, so the same key undoes it. With several masters, a stack window swaps with the master you focused most recently. Floating windows are unaffected.
Stack rotation
Roll the whole stack by one so the next window takes the master slot:
sweets.bind("MOD+Shift+J", "rotate_next")
sweets.bind("MOD+Shift+K", "rotate_prev")
sweets.bind("MOD+Return", "focus_master")Rotation keeps the stack's order where a swap scrambles it, and it leaves focus alone. A minimized window keeps its place. On a scrolling workspace it rolls whole columns along the strip.
focus_master jumps to the master you used most recently from anywhere in the
stack, and from a master to the stack member you used most recently.
new_window
"master" makes each new tiled window the master. "stack" appends it after
the existing windows. Floating windows and windows returning from floating
ignore this. A reload affects later windows only.
reorder_ratio
How far a MOD+left drag must move into a neighbor before they swap. Lower
reorders sooner. Sampled when the drag begins. The scrolling layout places a
drop by the pointer instead.
Moving and resizing floating windows
sweets.bind("MOD+Ctrl+Left", "move_floating_left")
sweets.bind("MOD+Ctrl+Right", "move_floating_right")
sweets.bind("MOD+Ctrl+Up", "move_floating_up")
sweets.bind("MOD+Ctrl+Down", "move_floating_down")
sweets.bind("MOD+Ctrl+equal", "resize_floating_grow")
sweets.bind("MOD+Ctrl+minus", "resize_floating_shrink")
sweets.bind("MOD+Ctrl+Shift+equal", "resize_floating_grow_vertical")
sweets.bind("MOD+Ctrl+Shift+minus", "resize_floating_shrink_vertical")Each action travels one layout.floating_step. Moving stays on the window's
own output and always leaves a strip on screen; use move_output_* to cross to
another monitor. Resizing keeps the top-left corner fixed and respects the
window's size hints and any min_size or max_size window rule.
These actions do nothing on a tiled, maximized, or fullscreen window. Changing
floating_step affects later presses only.
Keyboard resizing
sweets.bind("MOD+equal", "resize_grow")
sweets.bind("MOD+minus", "resize_shrink")
sweets.bind("MOD+Shift+equal", "resize_grow_vertical")
sweets.bind("MOD+Shift+minus", "resize_shrink_vertical")resize_grow and resize_shrink work horizontally; the _vertical variants
work vertically.
| Layout | Resizes |
|---|---|
tile | The master split, or a neighboring pair of masters or stack windows |
centered | The master width or height, a pair of masters, or one side stack |
grid | The focused cell against its neighboring column or row |
columns | Neighboring bands along the running axis |
deck | The master-to-stack divider only |
dwindle | The focused window's nearest split |
monocle | Nothing |
When a layout can only be resized along one axis, the horizontal actions follow
that axis. This covers a two-window tile with one master, a tile where every
window is a master, deck's divider, and columns' running boundaries.
MOD+right-button drag resizes the divider under the pointer. On a floating
window it resizes the window itself.
Resetting
sweets.bind("MOD+U", "reset_layout")
sweets.bind("MOD+Shift+U", "reset_all_layouts")reset_layout restores the active layout's proportions, master count, orientation, and runtime
gap override on the visible workspace from the latest configured values, and equalizes window
weights. On scrolling it returns every column to column_width and even
heights. reset_all_layouts does the same everywhere. Neither changes the
selected layout, window order, focus, or floating geometry. Monocle clears a
runtime gap override and otherwise remains a no-op.
sweets.layout_style
Overrides appearance for tiled windows while one layout is active.
sweets.layout_style("tile", {
border = { width = 2 },
gaps = { inner = 4, outer = 12, smart = true },
window = { corner_radius = 8 },
})
sweets.layout_style("monocle", {
border = { width = 0 },
window = { corner_radius = 0 },
})| Field | Type | Falls back to | Range |
|---|---|---|---|
border.width | integer | sweets.border.width | 0–64 |
gaps.inner | integer | sweets.gaps.inner | 0–4096 |
gaps.outer | integer or table | sweets.gaps.outer | 0–4096 per edge |
gaps.smart | boolean | sweets.gaps.smart | — |
window.corner_radius | integer or corner table | sweets.window.corner_radius | 0–1024 |
Every field is optional and falls back to the global value. Each layout may be styled once per configuration layer.
Styles apply to tiled windows only, and fullscreen windows stay square and borderless. A matching window rule overrides the layout radius.
The same border, gaps, and window sections work on a
monitor and a
workspace. The order is
global, monitor, workspace, then layout_style, and the last one set wins.
sweets.gaps
sweets.gaps({
inner = 8,
outer = 8,
smart = true,
})| Field | Type | Default | Description |
|---|---|---|---|
inner | integer | 8 | Pixels between tiled windows, 0 to 4096 |
outer | integer or table | 8 | Margin inside the work area, 0 to 4096 per edge |
smart | boolean | false | Drop the outer margin on all four edges when one tiled window is visible |
outer takes one number for all four edges, or a table naming any of top,
bottom, left, and right. An omitted side is 0, not the global value.
sweets.gaps({ inner = 8, outer = { top = 4, bottom = 12, left = 8, right = 8 } })The installed configuration enables smart. A window presented edge-to-edge by
smart gaps is also square — the configured rounding returns with the margin.
Monocle counts as one visible window. Floating windows get no gaps and are not
counted; fullscreen uses the whole output.
Use gaps_grow and gaps_shrink to adjust the selected workspace temporarily.
They change inner by default; pass "outer" to change all four outer sides
together. Each press moves by layout.gaps_step, clamps at 0 and 4096, and
starts from the currently resolved global, monitor, workspace, and layout-style
value. toggle_gaps hides both inner and outer gaps and restores the exact
runtime adjustment on the next press. reset_layout and every accepted reload
clear these temporary overrides without changing the stored configuration.
sweets.border
sweets.border({
width = 4,
smart = false,
focused = "#C27AFF",
unfocused = "#333338",
urgent = "#F7768E",
unresponsive = "#E0AF68",
})| Field | Type | Default | Description |
|---|---|---|---|
width | integer | 2 | Border width in pixels, 0 to 64; 0 disables |
smart | boolean | false | Hide the border when exactly one tiled window is mapped |
focused | string or table | "#C27AFF" | Focused color |
unfocused | string or table | "#333338" | Unfocused color |
urgent | string or table | "#F7768E" | Unfocused window asking for attention |
unresponsive | string or table | "#E0AF68" | Window that stopped responding |
The installed configuration uses width = 4. Colors take #RRGGBB or
#RRGGBBAA.
Gradients
Any of the four colors can be a table of 1 to 8 color stops instead of one
string, with an optional angle:
sweets.border({
width = 4,
focused = { "#C27AFF", "#7AA2F7", angle = 45 },
unfocused = "#333338",
})| Field | Type | Default | Description |
|---|---|---|---|
| stops | list of strings | — | 1 to 8 colors, spaced evenly along the gradient |
angle | integer | 0 | Direction in degrees, 0 to 359 |
The angle is measured clockwise from left-to-right: 0 runs the first stop to
the last across the window, 90 runs it down the window, and 180 runs it
right to left. The gradient covers the whole window, so it stays continuous
around the corners. A table with one stop is the same as that color on its own.
Gradients crossfade like any other border color when focus changes.
The border is drawn inside the window's outer rectangle. Fullscreen windows never have one. Smart borders count tiled windows only; floating windows keep theirs. Unresponsive beats focused, focused beats urgent, and urgent beats unfocused. Focusing a window clears its urgent state.
A window rule can
override any of these fields for the windows it matches, or remove the border
with border = false.
sweets.window
sweets.window({
corner_radius = 12,
opacity = { focused = 1.0, unfocused = 0.92 },
})| Field | Type | Default | Description |
|---|---|---|---|
corner_radius | integer or table | 0 | One radius for every corner, or a table naming the round ones; 0 to 1024 each |
opacity | number or table | 1.0 | Content opacity, or a table naming focused and unfocused |
Set corners individually with a table. An omitted corner is square:
sweets.window({
corner_radius = {
top_left = 16,
top_right = 16,
bottom_right = 8,
bottom_left = 8,
},
})The installed configuration uses a uniform 12. A corner set to 0 stays
square.
Rounding applies to managed Wayland and normal XWayland windows. Fullscreen windows, windows made edge-to-edge by smart gaps, popups, panels, input-method windows, override-redirect X11 windows, cursors, and drag icons are all square. Clicking still uses the full rectangle, so corners remain clickable.
A window rule corner_radius overrides
this for one window; 0 disables its rounding. See
Visual Effects for opacity and
animations.window_opacity to
crossfade it.
sweets.special
Configures the dimmer behind the private special workspaces.
sweets.special({
dim = "#000000A6",
})
sweets.bind("MOD+S", "special_toggle")
sweets.bind("MOD+Shift+S", "special_move")
sweets.bind("MOD+M", "special_toggle", "music")| Field | Type | Default | Description |
|---|---|---|---|
dim | string | "#000000A6" | Dimmer color; the alpha sets the strength |
special_toggle shows or hides one on the selected monitor. special_move
sends the focused window there without revealing it. Both take an optional
name, so you can keep several separate scratchpads; see
Workspaces.
Each keeps its own layout, focus, and floating state while hidden. Only one is shown at a time, and the dimmer is shared. While shown it is modal on its host monitor only — other monitors stay interactive. New windows open in it unless a rule sends them elsewhere, and transients follow their parent. It hides when its last window closes.
A special workspace is a workflow, not a security boundary. Use session lock when content must be protected.