Window rules
Match a window, then change what PlasmaZones does with it.
Settings apply everywhere. Rules are how you make an exception: send your terminal to a zone on open, keep a video player out of tiling entirely, drop the gaps on one monitor, or give a single application its own border colour. A rule is a match and a list of actions, and both halves are richer than a per-app list.
Rules live in ~/.config/plasmazones/rules.json and are edited from Settings → Rules. The file is watched, so an external edit takes effect without a restart.
The shape of a rule
A rule has an id, a name, a priority, a match expression, and its actions. The smallest useful one looks like this:
{
"rules": [
{
"id": "0e6b2c1a-8f4d-4a71-9d2e-3c5b7a1f0e44",
"name": "Keep the video player out of tiling",
"enabled": true,
"priority": 10,
"match": { "field": "appId", "op": "equals", "value": "mpv" },
"actions": [ { "type": "excludePlacement" } ]
}
]
}A rule with no actions is dropped at load rather than kept as dead weight, and an unknown field or operator token makes the loader discard that predicate instead of quietly coercing your typo to a default. If a rule seems to do nothing, a misspelled token is the first thing to check.
Matching
A match is either a single predicate, made of a field, an op, and a value, or a composite that nests them. The three composites are all, any, and none. They take a list of child expressions, so matches nest as deep as you need:
"match": {
"all": [
{ "field": "appId", "op": "equals", "value": "firefox" },
{ "any": [
{ "field": "title", "op": "contains", "value": "Picture-in-Picture" },
{ "field": "isTransient", "op": "equals", "value": true }
]},
{ "none": [
{ "field": "isFullscreen", "op": "equals", "value": true }
]}
]
}An omitted match is the always-true catch-all, which is what makes a rule that should apply everywhere easy to write and easy to write by accident.
Operators
| Operator | Behaviour |
|---|---|
equals | Case-insensitive for strings, numeric for numbers, plain true/false for flags. |
contains | Substring, case-insensitive. |
startsWith | Prefix, case-insensitive. |
endsWith | Suffix, case-insensitive. |
regex | Compiled once per predicate and cached, so a regex rule is not a per-window cost. |
appIdMatches | Segment-aware reverse-DNS match, for appId only. |
greaterThan | Numeric only: pid, virtualDesktop, width, height, positionX, positionY, tiledWindowCount. |
lessThan | The same numeric fields as greaterThan. |
Fields
These are the wire spellings, so they can be typed straight into rules.json.
Identity
What the application is. The most common thing to match on.
appIdwindowClassdesktopFilewindowRolepidtitlecaptionNormalwindowType
State
What the window is doing right now.
isStickyisFullscreenisMinimizedisMaximizedisFocusedisTransientisNotificationisModalisFloatingisSnappedisTiledzone
Geometry
Size and position, in pixels. These take the numeric operators.
widthheightpositionXpositionY
Hints and capabilities
What the window asked the compositor for, and what it will allow.
keepAbovekeepBelowskipTaskbarskipPagerskipSwitcherhasDecorationisResizableisMovableisMaximizable
Context
Where and when, rather than which window. These are the fields a context rule matches on.
screenIdvirtualDesktopactivitymodeactiveLayoutscreenOrientationcolorSchemetiledWindowCount
Window rules and context rules
The field list divides in two, and the division decides whether a rule fires at all. Most fields describe a window. A handful describe a context instead: the screen, the virtual desktop, the activity, the current mode and layout, the screen's orientation, and the colour scheme. Those are present even when no window is involved.
That matters because some actions are context actions. Locking a screen's layout or setting its engine mode is a question about a screen, not about a window, and it gets asked during context resolution where no window exists. A predicate over a window field is false when there is no window, so pairing a context action with a window match produces a rule that loads, validates structurally, and never fires. The rule editor flags this combination. When hand-editing, nothing will warn you.
The reverse pairing is fine. A window action with context predicates simply narrows where the action applies, which is how you scope an appearance change to one monitor.
Priority and the cascade
This is the part most worth understanding, because it is not first-rule-wins.
Rules are walked in descending priority, with ties broken by their order in the list. Each action fills a named slot, and the first action to reach a slot keeps it. A slot counts as filled the moment anything lands in it, even an action carrying empty parameters.
So a lower-priority rule is not skipped when a higher-priority one matches. It still fills every slot the winner left empty. A high-priority rule that sets only a border colour and a general-purpose rule that sets gaps and opacity will both apply, with the border colour coming from the first and everything else from the second. Rules layer rather than replace each other.
The exception is the terminal actions, the Exclude family: exclude, excludePlacement, excludeAnimations, and excludeDecorations. These say "stop". Pairing one with ordinary effect actions in the same rule is contradictory enough that validation calls it out.
One rule ships built in, marked managed: the baseline appearance rule. It is pinned to the lowest precedence so anything you write overrides it, and the settings UI will not let you delete or reorder it. Evaluation treats it like any other rule.
Actions
There are over a hundred action types, which is too many to list usefully. They fall into a few families:
- Placement
setEngineMode,disableEngine,snapToZone,float,routeToScreen,routeToDesktop, and the three layout setters for snapping, tiling, and scrolling. - Exclusion The terminal family above, for windows that should be left alone entirely or spared one subsystem.
- Window appearance Borders, opacity and tint, title-bar hiding, and the window layer. These are the per-window equivalents of the
Windowssettings group. - Gaps and geometry Inner and outer gaps, including the per-side variants, scoped to whatever the rule matches.
- Overlay and effects Override the overlay shader, its colours and opacity, or the animation shader, timing, and curve.
- Mode-specific The tiling knobs (master count, split ratio, insert and overflow behaviour) and the large scrolling family covering column width, the tab indicator, and the drop indicator.
- Per-context overrides
lockContext,defaultLayoutAssignment, andsetOsdEnabled, which override a global setting for one screen, desktop, or activity.
The settings app lists every action with its parameters, which is the practical way to discover them. Parameters are validated against the action's schema on load, so an action with a bad parameter is rejected rather than half-applied.
One trap worth knowing
Autotile is spelled two different ways, on purpose, and mixing them up produces a rule that fails silently.
The mode field takes snapping, tiling, or scrolling. It names the placement mode a window is in, and tiling covers autotile.
The setEngineMode action names the engine instead, and spells that middle value autotile.
The two vocabularies are deliberately independent and are not cross-checked. A match leaf written as { "field": "mode", "op": "equals", "value": "autotile" } is structurally valid, loads without complaint, and never equals any stamped mode, so the rule quietly does nothing.
Next
- Settings reference → The global defaults that rules make exceptions to.
- Snapping → Where the global exclusions live, and how they differ from a rule.
- D-Bus scripting → Driving placement from a script when a rule is not the right shape.