Window Rules
Match XDG and XWayland windows and select opening behavior or live appearance.
sweets.window_rule({ ... }) matches the retained initial identity of a managed
window. One declaration can choose how the window opens, select live root or
popup appearance, or do both. Opening properties include placement, floating
geometry, focus, state, stacking, adaptive-sync eligibility, and corner radius.
sweets.window_rule({ app_id = "org.wezfurlong.wezterm", workspace = 2 })
sweets.window_rule({ title = "Picture-in-Picture", floating = true, focus = false })
sweets.window_rule({ backend = "xwayland", app_id = "steam", monitor = "DP-1" })
sweets.window_rule({ app_id = "foot", opacity = 0.88, blur = true })
sweets.window_rule({ app_id = "org.gnome.Nautilus", popups = { opacity = 0.9, blur = true, corner_radius = 18 } })
sweets.window_rule({
title = "Picture-in-Picture",
size = { 480, 270 },
position = "top-right",
stacking = "above",
corner_radius = {
top_left = 18,
top_right = 18,
bottom_right = 8,
bottom_left = 8,
},
})Rules are optional. Windows that do not match a rule use the normal Sweets and client opening policy, full compositor opacity, and no compositor-forced blur.
Layer-shell surfaces use their own namespace/current-layer identity. Configure
panels, launchers, notification surfaces, and wallpapers with
sweets.layer_rule({ ... }).
Matchers
A rule must contain at least one matcher and at least one opening or appearance property. When a rule contains several matchers, all of them must match the same window.
| Field | Type | Matches |
|---|---|---|
backend | string | Exactly "xdg" or "xwayland" |
app_id | string | XDG app ID for a native window, or X11 class for an XWayland window |
title | string | Initial window title |
dialog | boolean | Whether the initial protocol metadata identifies a dialog |
transient | boolean | Whether the window initially has a parent or transient-for relationship |
String matching is exact and case-sensitive. Sweets does not interpret
substrings, globs, regular expressions, or Lua callbacks. app_id and title
cannot be empty.
For example, this rule matches only native XDG dialogs from the named application:
sweets.window_rule({
backend = "xdg",
app_id = "org.example.Editor",
dialog = true,
floating = true,
})XWayland app_id matching uses the X11 class, not the X11 instance. Restrict
the rule with backend = "xwayland" when a native application may use the
same identifier.
Finding window identities
List mapped windows and their IDs:
sweets msg windowsThen inspect the immutable opening identity and resolved rule policy for one window:
sweets msg window-rule WINDOW_IDThe rule query reports the initial backend, app ID, title, X11 class and
instance when applicable, dialog and transient facts, the accepted config
generation, the effective policy, and any diagnostics. Add --json before the
command when you need machine-readable output.
Titles commonly contain document names or other changing text. Prefer
app_id rules and use title only when the initial title is stable.
Opening properties
Matcher fields select a window. The remaining fields contribute its opening policy:
| Field | Type | Effect when specified |
|---|---|---|
workspace | integer | Open on numbered workspace 1 through 10 |
monitor | string | Open on the connected monitor's currently visible workspace |
floating | boolean | true forces floating; false forces tiling |
focus | boolean | false suppresses automatic opening focus |
size | table | Set floating content size with { width, height } |
position | string or table | Set a floating anchor or { x, y } coordinates |
maximized | boolean | Set the initial maximized state |
fullscreen | boolean | Set the initial fullscreen state |
adaptive_sync | boolean | Allow this view to activate on-demand VRR while fullscreen |
stacking | string | Select "normal", "above", or "below" |
corner_radius | integer or table | Override sweets.window uniformly, or with a complete named four-corner table; each value is 0 through 1024 |
Omitting a property leaves the normal compositor or client decision in place.
A property explicitly set to false still participates in rule merging.
Without an explicit floating value, Sweets automatically floats a native XDG
window with a fixed-size axis. It also floats one whose initial maximum size is
smaller than its prospective tile on either axis. The comparison uses the
workspace selected by opening rules. Set floating = false to keep a matching
window tiled despite those initial constraints.
When such a window also receives automatic keyboard focus, Sweets confirms its activated XDG state after focus delivery. Synchronous confirmation dialogs are therefore visible immediately without an extra focus or resize action.
Opening properties resolve once before the first configure or map. Reloading them affects only unresolved future windows; relaunch an existing application to test an opening-policy edit.
Live appearance properties
These properties use the same matcher and ordered rule declaration:
| Field | Type | Effect when specified |
|---|---|---|
opacity | number | Whole-content opacity from 0.0 through 1.0 |
blur | boolean | Force or suppress compositor blur for the matching window |
blur_mode | string | Override the global mode with "optimized" or "scene" |
popups | table | Descendant XDG popup opacity, blur, and blur_mode |
Appearance properties resolve from the retained initial identity whenever a
valid configuration reload is accepted. They update already-open managed
windows without moving, resizing, restacking, or refocusing them. Blur still
requires the global sweets.blur
master switch.
sweets.window_rule({
app_id = "foot",
workspace = 2, -- opening: applies after relaunch
opacity = 0.88, -- live: applies on valid reload
blur = true, -- live
blur_mode = "optimized",
})Popup appearance
The nested popups table styles every XDG popup owned by the matched native
window, including nested menus and tooltips.
opacity is absolute. It does not multiply the owning window's opacity.
Client alpha still multiplies the configured popup value.
sweets.window_rule({
app_id = "org.gnome.Nautilus",
popups = {
opacity = 0.9,
blur = true,
blur_mode = "optimized",
corner_radius = 18,
},
})Popup blur requires the global blur master. blur = false vetoes client popup
blur. true supplies whole-popup blur when no non-empty client region exists.
An exact non-empty client region wins. blur_mode selects "optimized" or
"scene" for either form.
Popup opacity affects client content only. The backdrop remains fully filtered, so translucency does not mix sharp source pixels back into the menu.
corner_radius clips the backdrop effect to the popup's visual corners. It
accepts the same uniform integer or four-corner table as the window radius,
defaults to 0, and does not clip client pixels or popup input. Wayland does
not expose the radius, so configure it to match the application or toolkit.
Omitting popup blur preserves the window veto and mode for client requests.
Root blur = true never forces popup blur.
The popup tree remains mapped and interactive even at 0.0. This policy does
not affect XWayland override-redirect menus, input-method popups, ordinary
subsurfaces, or menus drawn inside the application's root surface.
Rule order and merging
Every matching rule contributes properties in declaration order. A later rule replaces only the properties it specifies; omitted properties remain from earlier matching rules.
sweets.window_rule({
dialog = true,
floating = true,
focus = false,
})
sweets.window_rule({
app_id = "org.example.Editor",
dialog = true,
focus = true,
size = { 900, 700 },
})A matching editor dialog receives floating = true from the first rule, then
the second rule replaces focus and adds size. Installed rules run first;
user rules append afterward. Rules also retain their declaration order across
files loaded with sweets.include.
A later rule cannot clear an earlier property. Keep placement rules disjoint:
adding monitor does not remove an inherited workspace, and the effective
combination is invalid.
Workspace and monitor placement
workspace selects a logical numbered workspace using the active shared or
per-monitor workspace model and any configured workspace pin. A hidden target
can receive the new window without being made visible.
monitor selects the numbered workspace currently visible on that connector.
Connector names are exact and case-sensitive; find them with:
sweets msg outputsA rule cannot select both workspace and monitor. If the named monitor is
not connected, Sweets keeps the window on its normal fallback workspace and
reports monitor_unavailable through sweets msg window-rule WINDOW_ID.
A generic placement rule does not detach a transient window from its parent
workspace. To move dialogs or transients deliberately, make the placement rule
explicit by including a dialog or transient matcher:
sweets.window_rule({
app_id = "org.example.Editor",
transient = true,
workspace = 3,
})Rules target numbered workspaces only. They do not select the special workspace directly.
Floating size and position
size and position are floating-only. Either property implies
floating = true when floating is omitted. Combining geometry with
floating = false is invalid.
Every size or coordinate component follows the same unit rule:
- Values greater than
0through1are fractions of the target work area. - Values greater than
1through16384are logical pixels.
For example, { 0.5, 600 } means half the work-area width and 600 logical
pixels high. Exactly 1 means the full corresponding work-area dimension, not
one logical pixel. Zero, negative, non-finite, and larger values are invalid.
A position pair measures the window's top-left corner from the work area's top-left:
sweets.window_rule({
app_id = "mpv",
size = { 960, 540 },
position = { 40, 0.1 },
})position also accepts one of nine anchors:
center
top-left top top-right
left right
bottom-left bottom bottom-rightRequested sizes respect client minimum and maximum hints. Oversized windows and
coordinates are clamped into the target work area. When size is combined
with maximized or fullscreen state, it becomes the floating geometry available
after restoring the window.
Focus, state, and stacking
focus = false prevents the new window from taking automatic keyboard focus.
focus = true permits the normal opening focus path, but it cannot override
X11 focus intent, session lock, an interactive layer surface, a popup grab, or
other higher-priority focus policy.
maximized and fullscreen set their respective initial states. A resolved
policy cannot enable both at once. Maximized uses the usable work area;
fullscreen uses the full output.
adaptive_sync = true opts the immutable opening decision into on-demand VRR.
It does not make the window fullscreen and has no effect on outputs configured
for fixed refresh or always-on VRR. On an output using monitor
adaptive_sync = "on_demand", Sweets enables VRR only while this view is
mapped, visible, fullscreen, unobscured by compositor modal policy, and
confirmed by the rendered primary-output assignment. A later matching
adaptive_sync = false rule removes an earlier opt-in. Reloading rules affects
future unresolved views; relaunch an existing application to test a change.
stacking assigns a persistent class within Sweets' managed stacking order:
"above" stays ahead of normal windows, "below" stays behind them, and
"normal" uses ordinary stacking. The field works for tiled and floating
windows.
corner_radius overrides the global sweets.window radius for this resolved
window. A scalar applies to every corner. A table requires exactly top_left,
top_right, bottom_right, and bottom_left; zero keeps the corresponding
corner square. Later matching rules replace the complete earlier radius rather
than merging individual corners, while omission leaves the earlier value in
place. Fullscreen and edge-to-edge tiled presentations produced by smart gaps
remain square. The retained rule radii return when fullscreen exits or the
smart outer margin returns. Windows without an override continue to follow
valid global radius reloads.
Invalid combinations
These effective property combinations are invalid:
workspacetogether withmonitor.floating = falsetogether withsizeorposition.maximized = truetogether withfullscreen = true.
When one rule contains a conflict, configuration validation rejects it. A
conflict can also be created only after several rules match the same identity;
that cannot be detected without a window to resolve. In that case Sweets uses
the normal protocol opening policy for that window, logs a warning, and reports
the conflict in sweets msg window-rule WINDOW_ID.
One resolved configuration can retain up to 128 window rules across opening-only, appearance-only, and combined declarations. Each string can contain up to 256 UTF-8 bytes, and all retained matcher and opening-policy strings together can use up to 32 KiB.
Reload behavior
Sweets resolves one immutable decision from the window's initial protocol metadata before its first configure or map. Later title or app-ID changes do not run the rules again. The decision also remains attached across unmap and remap, including its corner-radius override.
A valid reload atomically replaces the complete rule set. Opening properties apply to windows that have not yet resolved and never move, resize, restack, or refocus existing windows. Appearance properties re-resolve all managed windows from their retained initial identity. Reload and relaunch the application when testing opening properties; appearance edits apply on reload:
sweets --check-config
sweets msg reloadIf validation fails, Sweets keeps the last valid rule set active. Remember that
--check-config can validate each rule and its static limits, but an
identity-dependent conflict across multiple matching rules appears only when a
window resolves them.
For one migration period, the old sweets.rule({ ... }) opening-only entry
point and sweets.appearance_rule({ ... }) appearance-only entry point remain
accepted. Either deprecated spelling produces one bounded warning. Use
sweets.window_rule({ ... }) for all new and updated configuration.