Sweets
Configuration

General

Modifier key, background, workspaces, and cross-monitor behavior

sweets.general sets global compositor behavior.

sweets.general({
  background = "#12121c",
  mod_key = "SUPER",
  workspace_mode = "shared",
  workspace_count = 10,
  workspace_move_follow = false,
  workspace_back_and_forth = false,
  workspace_wrap = false,
  workspace_skip_empty = false,
  focus_cross_monitor = true,
  move_cross_monitor = true,
  drag_center_cursor = true,
  resize_on_border = false,
})

Options

FieldTypeDefaultDescription
backgroundstring"#06080A"Clear color behind windows, #RRGGBB or #RRGGBBAA
mod_keystring"SUPER"What MOD means in bindings: "SUPER" or "ALT"
workspace_modestring"shared"One workspace set globally, or one per monitor
workspace_countinteger10Usable workspaces, 1 to 10
workspace_move_followbooleanfalseFollow a window after moving it to another workspace
workspace_back_and_forthbooleanfalseRe-selecting the workspace you are on returns to the previous one
workspace_wrapbooleanfalseRelative workspace actions wrap between the first and last number
workspace_skip_emptybooleanfalseRelative workspace actions pass over empty workspaces
focus_cross_monitorbooleantrueDirectional focus may continue onto the next monitor
move_cross_monitorbooleantrueDirectional moves may continue onto the next monitor
drag_center_cursorbooleantrueCenter a tiled window under the pointer when a MOD+drag starts
snaptabledisabledSnap a dragged or resized floating window to nearby edges
resize_on_borderbooleanfalseResize a window by dragging its border, with no modifier held
resize_border_grabinteger8Extra pixels outside the border you can still grab, 0 to 64
insert_hintcolour, gradient, or false"#C27AFF80"Show where a dragged tiled window will land
render_unfocused_fpsinteger15Frame rate for windows kept drawing by render_unfocused, 1 to 60
spawn_origin_placementbooleantrueOpen a launched app on the workspace you launched it from
spawn_origin_timeoutinteger30Seconds a launched app has to open its window, 1 to 600
lid_switchstring"disable_panel"What closing a laptop lid does to the built-in screen
power_keystring"system"Who acts on the power key

Omitted fields keep their installed value. The installed configuration sets background = "#12121c" and insert_hint = false, and leaves the rest at the defaults above.

mod_key

sweets.general({ mod_key = "ALT" })

sweets.bind("MOD+Return", "spawn", "foot")  -- Alt+Return

Explicit Ctrl, Shift, Alt, and Super in a binding are unaffected. See Key bindings.

workspace_mode

shared keeps one workspace per number for the whole session. A workspace is visible on at most one monitor at a time.

  • Selecting a visible workspace focuses its monitor.
  • A hidden workspace with windows returns to its remembered or pinned monitor.
  • An empty workspace opens on the selected monitor.

per_monitor gives every monitor its own set, so two monitors can each have a workspace 1. Number actions apply to the selected monitor.

A pin made with sweets.workspace wins over the selected monitor in both modes.

Changing the mode on reload keeps your windows. per_monitor creates the missing workspaces; shared merges equal numbers.

workspace_count

Requests above the count are ignored, and disabled workspaces disappear from panels. Bindings are independent of the count, so keep your loop in range:

sweets.general({ workspace_count = 5 })

for i = 1, 5 do
  sweets.bind("MOD+" .. i, "workspace", i)
  sweets.bind("MOD+Shift+" .. i, "workspace_move", i)
end

Lowering the count first moves windows off the disabled workspaces onto the last enabled one.

workspace_wrap and workspace_skip_empty control the relative workspace bindings and workspace touchpad swipes. A minimized window keeps its workspace non-empty. A pinned window does not, because it is carried onto every workspace that its monitor shows. Reloading either option affects the next input and does not move anything immediately.

Cross-monitor focus and movement

Sweets always looks for a target on the current monitor first. These settings only decide what happens when there is none in that direction.

focus_cross_monitor lets focus_left, focus_right, focus_up, and focus_down reach the next monitor.

move_cross_monitor lets move_left, move_right, move_up, and move_down carry the focused tiled window there. Floating, maximized, and fullscreen windows never use this fallback, and the explicit move_output_* actions work either way.

Both take effect on the next action; a reload never moves anything by itself.

drag_center_cursor

When enabled, a MOD+left-button drag on a tiled window centers that window under the pointer for the rest of the drag. When disabled, the window keeps the exact point you grabbed.

With animations on, the window slides to the centered position over window_move. The slide never delays the drag — keep moving, reorder, cross monitors, or drop while it runs.

Floating windows and client-requested moves always keep their grab point. A reload affects later drags only.

snap

Snapping pulls a floating window's edges onto the work area and onto the other floating windows on its monitor while you drag or resize it with the pointer.

sweets.general({
  snap = { enabled = true, monitor_gap = 10, window_gap = 10, respect_gaps = false },
})
FieldTypeDefaultDescription
enabledbooleanfalseTurn snapping on
monitor_gapinteger10How close an edge must come to the work area to snap, in pixels. 0 turns this off
window_gapinteger10How close an edge must come to another floating window to snap. 0 turns this off
respect_gapsbooleanfalseLeave your configured gaps instead of snapping flush

An edge snaps to the work area, to another floating window's matching edge, or against another floating window it lies beside. A snapped edge stays put until you drag twice the capture distance away, so it does not flicker at the boundary.

Snapping applies to floating windows only. A tiled MOD+drag is a reorder, so there is nothing for its edges to land on.

The work area excludes panels and struts, so a snapped window never lands under a bar. Size limits set by the application still win. A reload affects later drags only.

resize_on_border

Drag a window's border, or the gap just outside it, to resize it. No modifier key. MOD+right-drag keeps working either way.

sweets.general({
  resize_on_border = true,
  resize_border_grab = 8,
})

A thin border is hard to hit, so resize_border_grab extends the grab area outward by that many pixels. The cursor changes shape when you are on it.

The area never takes a click from a window. Your press goes to whatever is drawn under the pointer first: the application, a menu, a panel. Only when nothing claims the pixel does the nearest window's border area get it, which is why the extra pixels work over the wallpaper and in the gap between two windows.

Fullscreen and maximized windows have no border to grab. With border.width = 0 and resize_border_grab = 0 there is nothing to grab at all.

A tiled window resizes the layout boundary that edge belongs to, exactly as MOD+right-drag does, so nothing happens on an edge its layout cannot move - monocle, or a single tile filling the workspace. Floating windows follow snap. A reload affects later presses only.

insert_hint

While you MOD+drag a tiled window, Sweets shades the spot it will land in if you let go now.

sweets.general({ insert_hint = "#C27AFF80" })
sweets.general({ insert_hint = { "#C27AFF80", "#7AA2F780", angle = 90 } })
sweets.general({ insert_hint = false })  -- no hint

It takes the same colours as a border. What it shows:

Drop targetHint
A tiled layoutThe slot the window will take
An empty workspaceThe whole work area
The middle of a scrolling columnThat column, which the window joins
Near a scrolling column's edgeA bar where the new column opens

The hint appears once the window moves and disappears when you let go. Moving a floating window shows no hint.

spawn_origin_placement

An app started by a spawn binding opens on the workspace you pressed the key on, even if you switched away while it was starting.

sweets.general({
    spawn_origin_placement = true,
    spawn_origin_timeout = 30,
})

It only works for apps that read the XDG_ACTIVATION_TOKEN variable before they show their first window. GTK 4 apps and foot do. Other apps open where you are when their window appears, as before.

SituationWhere the window opens
A window rule sets workspace, monitor or pinWhere the rule says
A dialog with a parent windowWith its parent
The app took longer than spawn_origin_timeoutWhere you are now
The launch workspace no longer existsWhere you are now

The window does not take focus away from the workspace you moved to. Apps started by exec_once are not affected.

render_unfocused_fps

The rate windows named by the render_unfocused window rule keep drawing at while they are on screen nowhere.

sweets.general({ render_unfocused_fps = 15 })

It is a cap from 1 to 60, and it applies to every such window. 15 is enough for a shared window; higher values spend power without being seen, since a paced window is drawn on no monitor. With no window using the rule, nothing is paced and the setting does nothing.

lid_switch

Closing a laptop lid turns the built-in screen off and moves its windows to your other monitors. Opening it brings both back.

sweets.general({ lid_switch = "disable_panel" })
ValueEffect
"disable_panel"The built-in screen is disabled while the lid is shut
"none"The lid never changes your outputs

Built-in means the connector type, so eDP, LVDS, DSI and DPI screens are covered and nothing else is. A monitor rule that disables a screen still wins; the lid can only turn one off.

With no other monitor attached, the built-in screen stays on: a desktop with no screen at all would leave your windows nowhere to go. On most laptops the system suspends a moment later anyway.

A lid switch binding still runs whichever value you choose. A laptop booted or resumed with the lid already shut starts with its screen off.

power_key

Press the power key and the system powers off. "compositor" asks logind to leave the key alone, so your own binding runs instead.

sweets.general({ power_key = "compositor" })
sweets.bind("XF86PowerOff", "spawn", { "veila", "lock" })
ValueEffect
"system"logind keeps the key and powers the machine off
"compositor"Sweets takes the key, so only your binding runs

Bind XF86PowerOff before setting "compositor", or the key does nothing. Sweets warns in its log when it is set with nothing bound.

With "system" a binding on XF86PowerOff still runs, and the machine powers off as well, so the two are rarely useful together.

This needs logind and systemd-inhibit. Without them the key stays with the system and Sweets says so once in its log.

On this page