Sweets
Configuration

Monitors

Modes, refresh rate, scale, position, transform, adaptive sync, and RGB range

sweets.monitor(name, { ... }) configures one output. Name it by connector, or by what the monitor reports about itself. Any monitor you do not list uses its preferred mode, scale 1, no transform, and automatic placement.

sweets.monitor("DP-1", {
  position = { 0, 0 },
  mode = { 2560, 1440 },
  refresh_hz = 144,
  scale = 1.5,
  transform = "normal",
  enabled = true,
  adaptive_sync = "on_demand",
})

sweets.monitor("eDP-1", {
  enabled = false,
})

Options

FieldTypeValuesIf omitted
positiontable{ x, y }, may be negativeAutomatic
modetable or string{ width, height } in physical pixels, or "preferred", "highest", "highest_refresh""preferred"
refresh_hznumber1 to 1000; needs an explicit mode sizePreferred or highest
scalenumber or string0.1 to 16, in steps of 1/120, or "auto"1
transformstring"normal", "90", "180", "270", and the "flipped-*" variants"normal"
enabledbooleantrue
adaptive_syncboolean or stringfalse/"disabled", true/"always", "on_demand"false
rgb_rangestring"auto", "full", "limited""auto"
backgroundstring#RRGGBB or #RRGGBBAAgeneral.background
focus_at_startupbooleanfalse
gaps, border, windowtableSame as sweets.layout_styleInherited
strutsinteger or table0 to 4096 per edgelayout.struts

Each rule must set at least one field, and each name may appear once per configuration layer. Up to 32 rules are allowed.

Your rule replaces the installed rule for that connector entirely — fields are not merged. Repeat any installed field you want to keep.

HDR, tearing, and custom modes are not configurable.

Naming a monitor

sweets msg outputs

This lists each connector with its make, model, serial, modes, current mode, position, scale, transform, power state, and adaptive-sync capability. Add --json for scripts. Every name below is matched exactly and is case-sensitive.

By connector

sweets.monitor("DP-1", { scale = 1.5 })

Names such as DP-1, HDMI-A-1, and eDP-1. Connector names change when a cable moves ports or a laptop changes dock, and the rule then stops applying.

By description

sweets.monitor({ make = "DEL", model = "S2719DGF" }, { scale = 1.5 })

Give any of make, model, and serial; every field you name must match. The rule follows the monitor between ports and docks.

Use the values sweets msg outputs prints, not the names on the box. make is the three-letter manufacturer code from the monitor's EDID, so Dell reports DEL and LG reports GSM.

Two identical panels usually differ only in serial. Name serial to configure one of them; name make and model to configure both.

A connector rule wins over a description rule for the same monitor. Between two description rules, the first one declared wins. A rule that matches no connected monitor is not an error, but Sweets logs a warning naming it.

Mode and refresh rate

sweets.monitor("DP-1", {
  mode = { 2560, 1440 },
  refresh_hz = 144,
})

An explicit mode must match a size the monitor advertises. Without refresh_hz, Sweets picks the preferred rate for that size, or the highest if none is preferred. Sweets picks the closest advertised rate within 0.5 Hz, so 60 may select 59.94. An unavailable size or rate is rejected and the previous output state is kept.

Instead of a size, mode takes a keyword. A keyword ranks the whole advertised list, so it keeps working after a firmware update changes the modes.

KeywordPicks
"preferred"The mode the monitor prefers. The default.
"highest"The largest mode, whatever the monitor prefers
"highest_refresh"The highest refresh rate, then the largest mode at it
sweets.monitor({ model = "S2719DGF" }, { mode = "highest_refresh" })

refresh_hz needs an explicit size, because a keyword already chooses the rate.

Scale

sweets.monitor("eDP-1", { scale = 1.5 })

Scale must be an exact multiple of 1/120, so 1.25, 1.5, and 2 work but 1.31 is rejected. The logical size is the mode divided by the scale: a 3840 × 2160 monitor at 1.5 becomes 2560 × 1440 logically.

Automatic scale

sweets.monitor("DP-1", { scale = "auto" })

Sweets picks a scale from the monitor's size and resolution, in quarter steps from 1 to 4. It targets the same density GNOME does, so auto usually gives the same number GNOME would.

auto is a sensible starting point, not a preference. A 32-inch 4K monitor gets 1.25; if you prefer 1.5, write the number.

A monitor that reports no physical size stays at 1. Nested and headless outputs always do.

Background

sweets.monitor("DP-1", { background = "#101018" })

The colour behind windows on that monitor. Without it, the monitor uses general.background.

Focus at startup

sweets.monitor({ model = "S2719DGF" }, { focus_at_startup = true })

Selects this monitor when the session starts, and starts the pointer in its middle. Without it, Sweets picks the first output. It applies once: reconnecting a monitor later never moves your selection back.

If several rules set it, the first matching monitor wins.

Gaps, borders, and struts

sweets.monitor("eDP-1", {
  gaps = { inner = 4, outer = 4 },
  border = { width = 1 },
  window = { corner_radius = 6 },
  struts = { top = 0 },
})

Layout appearance for the workspace shown on this monitor. Omitted fields inherit the global values. A workspace rule or layout_style overrides them.

struts replaces layout.struts on this monitor. It does not add to it.

Transform

sweets.monitor("DP-1", { transform = "90" })

90, 270, and their flipped variants swap logical width and height, which positioning accounts for.

Position

position sets the logical top-left corner.

sweets.monitor("DP-1", { position = { 0, 0 }, mode = { 3840, 2160 }, scale = 1.5 })
sweets.monitor("DP-2", { position = { 2560, 0 }, mode = { 2560, 1440 }, scale = 1 })

DP-2 starts after DP-1's 2560 logical pixels. Explicitly positioned monitors must not overlap.

A position is in logical pixels, so it depends on the scale: the same monitor is 2560 wide at scale 1.5 and 3072 wide at 1.25. Changing a scale moves where the next monitor has to start. If the result overlaps, Sweets rejects the configuration and names both rectangles:

configured output `DP-1` at (2560, 0) sized 3072x1728 logical
overlaps configured output `DP-4` at (0, 0) sized 3072x1728 logical

Use those sizes to work out the new positions.

scale = "auto" picks the scale for you, so you cannot know the logical size in advance. Either leave position off and let Sweets place the monitors, or run sweets msg outputs once to read the derived scale and size, then write positions to match.

Monitors without a position are sorted by name and placed left to right at y = 0, starting past the rightmost explicitly placed one. Disabled monitors are ignored by both rules.

Enabling and disabling

sweets.monitor("eDP-1", { enabled = false })

Workspaces, focus, and the pointer move to the remaining monitors. Disabling every usable monitor is rejected. Rules for disconnected monitors are kept and apply when the connector returns.

To turn a monitor off temporarily without changing the configuration:

sweets.bind("MOD+Ctrl+P", "monitor_power", "toggle")

The binding targets the selected monitor and accepts on, off, or toggle. To target a monitor by name instead, use IPC:

sweets msg output-power DP-1 off
sweets msg output-power DP-1 on

The powered field in sweets msg outputs shows the current state.

Adaptive sync (VRR)

"on_demand" keeps the desktop at a fixed refresh rate and switches to variable refresh only for a fullscreen application you opt in:

sweets.monitor("DP-1", {
  mode = { 2560, 1440 },
  refresh_hz = 144,
  adaptive_sync = "on_demand",
})

sweets.window_rule({
  app_id = "steam",
  adaptive_sync = true,
})

The window rule marks eligibility; it does not make the window fullscreen. VRR turns on only while that window is mapped, visible, fullscreen, and actually drawn on the monitor. Leaving fullscreen, switching workspaces, locking, or opening the special workspace returns it to fixed refresh.

true or "always" enables VRR all the time. Some panels flicker slightly on the desktop that way, which is what "on_demand" avoids. false or "disabled" keeps a fixed refresh rate.

VRR requires a vrr_capable connector. A request on hardware that cannot do it is rejected without changing your working setup, and a monitor that cannot restore its configured VRR after resume or hotplug falls back to fixed refresh. Nested sessions are always fixed refresh.

In sweets msg outputs, adaptive_sync_mode stays "on_demand" while adaptive_sync changes as eligible content comes and goes.

RGB range

sweets.monitor("DP-1", { rgb_range = "full" })

Pick the value that makes the monitor match the others:

SymptomTry
Darker, crushed blacks, too much contrast"limited"
Washed out, grey blacks"full"

"full" always sends 0255, and "limited" always sends 16235. Some monitors expect limited range for TV-style modes, whatever the signal says.

Many monitors have their own range setting, often called Black level on HDMI inputs. Check it first: an adapter between the graphics card and the monitor can change the range before the monitor sees it.

"auto" lets the graphics driver choose. Intel drivers pick limited range for TV-style modes, which many 4K monitors use at 60 Hz.

Sweets applies the range at startup, on reload, and when you switch back from another session. Changing it briefly blanks that monitor. The option needs driver support, which Intel has; elsewhere "full" and "limited" are rejected.

Output-management clients

Sweets supports wlr-output-management-unstable-v1, so tools like wlr-randr and kanshi can change modes, scale, transform, position, enablement, and adaptive sync at runtime. Custom modes are not supported.

The protocol only has a VRR on/off switch, so a client can select always-on or fixed refresh but never on-demand. Changing another property leaves an untouched monitor's on-demand setting alone.

A client change is a temporary override — it never edits your Lua files. The next valid reload clears it and reapplies your rules, even if the files did not change. Check for one with sweets msg status, under backend.runtime_output_override.

Reload

sweets --check-config
sweets msg reload

Removing a field restores its default on the next reload — drop scale and the monitor returns to 1. If a new topology cannot be applied, Sweets keeps the one that works.

--check-config validates syntax, types, and ranges. It cannot check modes, overlap, or hardware without a running compositor.

Workspaces on specific monitors

sweets.workspace(1, { monitor = "eDP-1" })
sweets.workspace(4, { monitor = "DP-1" })

Pins survive a disconnect and reclaim the monitor when it returns. See Workspaces.

On this page