Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Window & Client Rules

Jay supports powerful, reactive rules for controlling how clients connect and how windows behave. Rules are defined in the [[clients]] and [[windows]] arrays in your configuration file.

Client Rules

Client rules operate on Wayland clients (processes). They are defined with [[clients]] entries:

[[clients]]
match.exe = "/usr/bin/firefox"
action = { type = "exec", exec = ["notify-send", "Firefox connected"] }

Structure

Each client rule can have the following fields:

name
A name for cross-referencing this rule from other rules.
match
A ClientMatch specifying which clients this rule applies to.
action
An action to run when a client starts matching.
latch
An action to run when a client stops matching.
capabilities
Wayland protocol access granted to matching clients.
sandbox-bounding-capabilities
Upper bounds for protocols available to child sandboxes.

Client Match Criteria

All client match criteria are constant over the lifetime of a client. If no fields are set, all clients are matched. If multiple fields are set, they are implicitly AND-combined.

sandboxed
Whether the client is sandboxed (true/false).
sandbox-engine / sandbox-engine-regex
The sandbox engine (e.g. org.flatpak).
sandbox-app-id / sandbox-app-id-regex
The app ID provided by the sandbox.
sandbox-instance-id / sandbox-instance-id-regex
The instance ID from the sandbox.
uid
The user ID of the client.
pid
The process ID of the client.
is-xwayland
Whether the client is Xwayland (true/false).
comm / comm-regex
The client’s /proc/pid/comm value.
exe / exe-regex
The client’s /proc/pid/exe path.
tag / tag-regex
The connection tag of the client.

Granting Privileges

Jay splits Wayland protocols into unprivileged and privileged. By default, applications only have access to unprivileged protocols. This means that tools like screen lockers, status bars, screen-capture utilities, and clipboard managers will not work unless you explicitly grant them the necessary privileges.

See the Protocol Support table in the Features chapter for the full list of protocols and whether they are privileged.

There are three ways to grant privileges, from simplest to most fine-grained.

1. Grant all privileges via privileged = true (exec) or jay run-privileged

The simplest approach gives a program access to all privileged protocols. This is appropriate for trusted tools like screen lockers where you don’t want to think about which specific protocols they need.

In the config, set privileged = true in the exec table:

on-idle = {
    type = "exec",
    exec = {
        prog = "swaylock",
        privileged = true,
    },
}

From the command line, use jay run-privileged:

~$ jay run-privileged waybar

Both methods connect the program to a privileged Wayland socket that grants access to all privileged protocols.

2. Grant capabilities via connection tags

Connection tags let you combine the CLI with client rules for precise control. You tag a program at launch time, then write a client rule that matches the tag and grants specific capabilities.

First, launch the program with a tag – either from the command line:

~$ jay run-tagged bar waybar

Or from the config using the tag field in an exec action:

[shortcuts]
alt-w = {
    type = "exec",
    exec = {
        prog = "waybar",
        tag = "bar",
    },
}

Then write a client rule that matches the tag and grants capabilities:

[[clients]]
match.tag = "bar"
capabilities = ["layer-shell", "foreign-toplevel-list"]

This way, only the specific instance you launched with the tag receives the privileges – other programs with the same binary name do not.

Available capability values: none, all, data-control, virtual-keyboard, foreign-toplevel-list, idle-notifier, session-lock, layer-shell, screencopy, seat-manager, drm-lease, input-method, workspace-manager, foreign-toplevel-manager, head-manager, gamma-control-manager, virtual-pointer.

Default capabilities: unsandboxed clients receive layer-shell and drm-lease. Sandboxed clients receive only drm-lease. If any client rule matches, its capabilities replace the defaults entirely. If multiple rules match, their capabilities are unioned together, but the defaults are not included unless a matching rule also grants them.

3. Grant capabilities via client match rules

Client rules can also match programs by properties like their executable name instead of a tag. This is convenient when you always want a given program to have certain capabilities, regardless of how it was launched:

[[clients]]
match.comm = "waybar"
capabilities = ["layer-shell", "foreign-toplevel-list"]

# Vim 9.2 uses the data-control protocol for seamless wayland integration.
[[clients]]
match.comm = "vim"
match.sandboxed = false
capabilities = "data-control"

# Older versions use wl-copy and wl-paste.
[[clients]]
match.any = [
    { comm = "wl-copy" },
    { comm = "wl-paste" },
]
match.sandboxed = false
capabilities = "data-control"

Note

Client match criteria like comm, exe, and pid are checked when a client connects. Any process with a matching name receives the specified capabilities. If you need to restrict privileges to programs you launch yourself, use connection tags (method 2) instead.

Bounding capabilities (sandboxes)

Capabilities can never exceed the client’s bounding capabilities. Use sandbox-bounding-capabilities on a client rule to set the upper bound for protocols available to sandboxes created by that client:

[[clients]]
match.comm = "flatpak-portal"
sandbox-bounding-capabilities = ["drm-lease", "layer-shell"]

Window Rules

Window rules operate on individual windows. They are defined with [[windows]] entries:

[[windows]]
match.app-id = "org.gnome.Nautilus"
initial-tile-state = "floating"

Structure

Each window rule can have the following fields:

name
A name for cross-referencing this rule from other rules.
match
A WindowMatch specifying which windows this rule applies to.
action
An action to run when a window starts matching.
latch
An action to run when a window stops matching.
initial-tile-state
"floating" or "tiled" – force the initial tile state.
auto-focus
true/false – whether the window gets focus on map. Without a matching rule, newly mapped windows always receive focus (except for Xwayland override-redirect windows such as menus and tooltips, which bypass the normal mapping path).

The initial-tile-state and auto-focus fields are ad-hoc properties. They are evaluated synchronously during the mapping process (before the window is first displayed), unlike action which runs asynchronously after mapping. If multiple rules match and any sets auto-focus to false, the window will not be focused.

Window Match Criteria

If no fields are set, all windows are matched. If multiple fields are set, they are implicitly AND-combined. Without a types criterion, rules only match client-created windows (XDG toplevels and X windows).

types
Window type mask: none, any, container, xdg-toplevel, x-window, client-window.
client
A nested ClientMatch – matches the window’s owning client.
title / title-regex
The window title.
app-id / app-id-regex
The XDG app ID.
floating
Whether the window is floating.
visible
Whether the window is visible.
urgent
Whether the window has the urgency flag.
focused
Whether the window has keyboard focus.
fullscreen
Whether the window is fullscreen.
just-mapped
true for one compositor iteration after the window maps.
tag / tag-regex
The XDG toplevel tag.
x-class / x-class-regex
The X11 class (X windows only).
x-instance / x-instance-regex
The X11 instance (X windows only).
x-role / x-role-regex
The X11 role (X windows only).
workspace / workspace-regex
The workspace the window is on.
content-types
Content type mask: none, any, photo, video, game.

Combining Criteria

All match objects (both ClientMatch and WindowMatch) support the same logical combinators.

AND (Multiple Fields)

Multiple fields in one match table are implicitly AND-combined:

[[windows]]
match.title = "VIM"
match.app-id = "Alacritty"

This matches only windows whose title is VIM and whose app ID is Alacritty.

OR (Array of Matchers)

An array of match objects matches if any element matches:

[[windows]]
match.any = [
    { title = "chromium" },
    { title = "spotify" },
]

NOT (Negation)

[[windows]]
match.not.title = "firefox"

ALL (Explicit AND)

[[windows]]
match.all = [
    { title-regex = "chro" },
    { title-regex = "mium" },
]

EXACTLY (N of M)

Match if exactly N of the listed criteria match:

[[windows]]
match.exactly.num = 1
match.exactly.list = [
    { title = "VIM" },
    { client.sandboxed = true },
]

Cross-referencing Rules by Name

Rules can reference other rules by name:

[[windows]]
name = "spotify-windows"
match.client.sandbox-app-id = "com.spotify.Client"

[[windows]]
match.name = "spotify-windows"
action = "enter-fullscreen"

Reactive Behavior

Rules are re-evaluated dynamically whenever any referenced criterion changes. For example, if a rule matches on title, it is re-checked every time the window title changes.

  • action fires each time a window transitions from not-matching to matching.
  • latch fires each time a window transitions from matching to not-matching.

just-mapped

If you only want a rule to fire once when a window first appears, add just-mapped = true to the match:

[[windows]]
match.title = "VIM"
match.just-mapped = true
action = "enter-fullscreen"

This is similar to the initial-title criterion found in some other compositors.

Loop Protection

Rules can trigger each other. For example, one rule could fullscreen a window while another exits fullscreen on the same condition, creating a loop. Jay prevents such loops from locking up the compositor by capping action callbacks at 1000 iterations before yielding to other work. However, such loops will still cause 100% CPU usage and will likely cause affected clients to be killed, since they won’t be able to receive Wayland messages fast enough.

Practical Examples

Force an App to Start Floating

[[windows]]
match.app-id = "pavucontrol"
initial-tile-state = "floating"

Move a Specific App to a Workspace

[[windows]]
match.client.sandbox-app-id = "com.spotify.Client"
action = { type = "move-to-workspace", name = "3" }

Float Splash Screens Without Stealing Focus

[[windows]]
match.any = [
    { title = "GIMP Startup", app-id = "gimp" },
    {
        title = "splash",
        x-class-regex = "^jetbrains-(clion|rustrover)$",
    },
]
initial-tile-state = "floating"
auto-focus = false

Run a Command When a Window Appears

[[windows]]
match.app-id = "firefox"
match.just-mapped = true
action = {
    type = "exec",
    exec = ["notify-send", "Firefox window opened"],
}

Grant Protocol Access to a Trusted App

[[clients]]
match.comm = "swaylock"
capabilities = ["session-lock", "layer-shell"]

[[clients]]
match.comm = "waybar"
capabilities = ["layer-shell", "foreign-toplevel-list"]

Suppress Focus Stealing for Chromium Screen-Share Windows

[[windows]]
match.title-regex = 'is sharing (your screen|a window)\.$'
match.client.comm = "chromium"
initial-tile-state = "floating"
auto-focus = false

Introspection

Jay provides several ways to discover the property values you need for writing rules.

jay tree

Interactively select a window and print its properties:

~$ jay tree query select-window

Example output:

- xdg-toplevel:
    id: 258ae697663a1b8abc7e4da9570ad36f
    pos: 1920x36 + 1920x1044
    client:
      id: 15
      uid: 1000
      pid: 2159136
      comm: chromium
      exe: /usr/lib/chromium/chromium
    title: YouTube - Chromium
    app-id: chromium
    workspace: 2
    visible

jay clients

Inspect the client owning a window:

~$ jay clients show select-window

The control center (opened with alt-c by default, or jay control-center) includes a Window Search pane where you can search and filter windows using composable criteria – helpful for experimenting with match expressions before putting them in your config.

See spec.generated.md for the full specification of WindowRule, WindowMatch, ClientRule, ClientMatch, and all available actions.