Sweets
Configuration

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 },
})
FieldTypeDefaultDescription
defaultstring"tile"Initial layout for each workspace
masterinteger50Master area size in percent, 10 to 90
master_countinteger1Windows in the master area, 1 to 16
master_positionstring"left""left", "right", "top", or "bottom"
new_windowstring"master"Insert new tiled windows at "master" or "stack"
reorder_rationumber0.3Drag depth needed to reorder, 0.1 to 0.9
floating_stepinteger50Keyboard floating move and resize step, 1 to 1024
gaps_stepinteger5Runtime inner or outer gap adjustment, 1 to 4096
cyclearrayall layoutsOrder used by the layout-cycle actions
strutsinteger or table0Margin 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

NameArrangement
tileOne master area beside a stack of the rest
monocleEvery window fills the work area; only the focused one shows
centeredA centered master with the rest split across both sides
gridAn even grid, filled row by row
columnsOne weighted full-height column or full-width row per window
deckA master beside a shared stack slot showing its focused member
dwindleEach new window recursively splits the remaining area
scrollingColumns on a strip wider than the screen, scrolled to the focused one

Exceptions to master, master_count, and master_position:

  • monocle ignores both, and tiled resize does nothing. Floating windows stay visible above it, and fullscreen still works.
  • grid ignores all three. A short final row expands across the width.
  • columns ignores master and master_count. right and bottom reverse the visual order.
  • centered reads left/right as a vertical master column and top/bottom as a horizontal row. The exact side is remembered when you switch layouts.
  • deck supports all three, but only the master-to-stack divider resizes. Its masters always share the master area equally.
  • dwindle uses master for the outer split. Deeper splits start at half and resize independently. It ignores master_count.
  • scrolling ignores 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",
})
FieldTypeDefaultDescription
column_widthnumber0.5Share of the screen up to 1, from 0.05; whole logical pixels above 1
column_widthsarray{ 0.333, 0.5, 0.667, 1.0 }Widths cycle_column_width steps through, one to eight different widths
resize_stepnumber0.1One resize_grow or resize_shrink step, 0.01 to 0.5 of the screen
focusstring"fit"Where focusing puts a column: "fit", "center", or "center_on_overflow"
follow_min_visiblenumber0.0How much of a window must already be on screen for the pointer to scroll to it. 0 never scrolls
wrap_focusbooleanfalseWhether focus wraps at the ends of the strip
single_columnstring"fill"A lone column: "fill", "center", or "start"
narrow_stripstring"center"Columns narrower than the screen: "center" or "start"
directionstring"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

focusResult
"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.

ActionResult
focus_left, focus_rightFocus the neighbouring column's most recently used window
focus_up, focus_downFocus within the column
move_left, move_rightMove a lone window's column one place; move a stacked window out into a new column on that side
move_up, move_downReorder the window within its column
consume_or_expel_left, consume_or_expel_rightStack a lone window into the neighbouring column; move a stacked one out
move_column_left, move_column_rightMove the whole column one place
focus_column_first, focus_column_lastFocus 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

ActionResult
resize_grow, resize_shrinkWiden or narrow the focused column by resize_step
resize_grow_vertical, resize_shrink_verticalTrade height with a neighbour in the same column
cycle_column_width, cycle_column_width_reverseStep through column_widths
toggle_full_widthFill the screen with the focused column, or return it to its width
expand_columnWiden 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 goResult
Over the middle of a columnThe window joins it, above or below the window under the pointer
Within 30% of a column's left or right edgeThe window becomes a new column on that side
Between columns or past the endThe 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:

ActionResult on "down" or "up"
focus_up, focus_down, move_up, move_downWork between columns
focus_left, focus_right, move_left, move_rightWork within the column
resize_grow_vertical, resize_shrink_verticalChange the column's size
resize_grow, resize_shrinkTrade 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.

LayoutResizes
tileThe master split, or a neighboring pair of masters or stack windows
centeredThe master width or height, a pair of masters, or one side stack
gridThe focused cell against its neighboring column or row
columnsNeighboring bands along the running axis
deckThe master-to-stack divider only
dwindleThe focused window's nearest split
monocleNothing

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 },
})
FieldTypeFalls back toRange
border.widthintegersweets.border.width064
gaps.innerintegersweets.gaps.inner04096
gaps.outerinteger or tablesweets.gaps.outer04096 per edge
gaps.smartbooleansweets.gaps.smart
window.corner_radiusinteger or corner tablesweets.window.corner_radius01024

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,
})
FieldTypeDefaultDescription
innerinteger8Pixels between tiled windows, 0 to 4096
outerinteger or table8Margin inside the work area, 0 to 4096 per edge
smartbooleanfalseDrop 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",
})
FieldTypeDefaultDescription
widthinteger2Border width in pixels, 0 to 64; 0 disables
smartbooleanfalseHide the border when exactly one tiled window is mapped
focusedstring or table"#C27AFF"Focused color
unfocusedstring or table"#333338"Unfocused color
urgentstring or table"#F7768E"Unfocused window asking for attention
unresponsivestring 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",
})
FieldTypeDefaultDescription
stopslist of strings1 to 8 colors, spaced evenly along the gradient
angleinteger0Direction 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 },
})
FieldTypeDefaultDescription
corner_radiusinteger or table0One radius for every corner, or a table naming the round ones; 0 to 1024 each
opacitynumber or table1.0Content 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")
FieldTypeDefaultDescription
dimstring"#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.

On this page