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

OperatorBehaviour
equalsCase-insensitive for strings, numeric for numbers, plain true/false for flags.
containsSubstring, case-insensitive.
startsWithPrefix, case-insensitive.
endsWithSuffix, case-insensitive.
regexCompiled once per predicate and cached, so a regex rule is not a per-window cost.
appIdMatchesSegment-aware reverse-DNS match, for appId only.
greaterThanNumeric only: pid, virtualDesktop, width, height, positionX, positionY, tiledWindowCount.
lessThanThe 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:

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