Window rules let you apply per-window overrides based on a window's identity.
Rules are declared as [[window_rules]] sections in your config file.
All matching rules are applied, not just the first one. Rules are processed in config order and merged together:
- Scalar fields (
decoration,opacity,position,size,border_width,border_color,border_color_focused,corner_radius,shadow): last-wins — a later rule overrides an earlier one. - Boolean flags (
widget,blur): sticky-on — once set by any matching rule, the flag stays set regardless of later rules. pass_keys:Allis sticky-on;Onlylists are unioned across rules (see pass_keys details).
This lets you compose independent rules for the same window:
# Rule 1: make kitty blur its background
[[window_rules]]
app_id = "kitty"
blur = true
# Rule 2: also make it semi-transparent (blur from Rule 1 is kept)
[[window_rules]]
app_id = "kitty"
opacity = 0.85At least one criterion is required. All specified criteria must match.
| Field | Matches |
|---|---|
app_id |
Wayland app_id (X11 apps via xwayland-satellite arrive with app_id set from WM_CLASS instance, typically lowercase) |
title |
Window title |
driftwm msg state # camera, zoom, and the window inventoryTo get the app ids and titles of all current non-widget windows:
driftwm msg --json state | \
jq '.Ok.State.windows[] | select(.is_widget == false) | {app_id, title}'All match fields support three syntaxes:
| Syntax | Example | Meaning |
|---|---|---|
| Exact string | "kitty" |
Exact match (case-sensitive) |
| Glob | "steam_app_*" |
* matches any sequence of chars |
| Regex | "/^steam_app_\\d+$/" |
Full regular expression (wrap in /…/) |
Multiple * wildcards are allowed in glob patterns: "*terminal*".
Regex patterns use the regex crate (RE2-compatible, no backreferences).
The table below describes how each field behaves on rules matching regular windows (xdg-toplevels). Layer-shell surfaces interpret chrome fields differently — see Layer-shell surfaces below.
| Field | Type | Default | Description |
|---|---|---|---|
position |
[x, y] |
— | Place window at canvas coordinates (window center, Y-up). Output-relative (origin = output center) when pinned_to_screen is set. |
size |
[w, h] |
— | Initial window dimensions in pixels (one-shot: the user/app can resize freely afterwards; pair with widget = true to keep the size locked) |
widget |
bool |
false |
Pin window: immovable, below normal windows, excluded from navigation/alt-tab |
pinned_to_screen |
bool |
false |
Pin to one output's screen space — see Screen-pinned windows |
decoration |
string | inherited | Override decoration mode (see below) |
blur |
bool |
false |
Blur compositor background behind this window |
opacity |
0.0–1.0 |
1.0 |
Window transparency (1.0 = fully opaque) |
border_width |
px | inherited | Border width override. Set to 0 to disable the border even when global width is > 0. Ignored for decoration = "none". |
border_color |
"#rrggbb[aa]" |
inherited | Per-window unfocused border color |
border_color_focused |
"#rrggbb[aa]" |
inherited | Per-window focused border color |
corner_radius |
px | inherited | Per-window corner radius override. Affects content clip, border shape, and shadow. Ignored for decoration = "none". |
shadow |
bool |
inherited | Per-window shadow toggle. Overrides [decorations] shadow. Ignored for decoration = "none". |
output |
string | — | Output name (e.g. "DP-1") this window fullscreens onto — see Fullscreen output |
pass_keys |
bool or ["combo", …] |
false |
Forward keys to the app — see below |
Warning
Blur has real GPU/VRAM cost. Results are cached and only recomputed when
the content behind a window changes, but the cost does not scale down with
zoom — a blurred window is processed at full resolution no matter how far you
zoom out. Many blurred windows, or a few at extreme zoom-out, can stutter and
consume significant VRAM. Prefer enabling blur on a handful of windows over
applying it globally. There's room to improve this further.
| Value | Description |
|---|---|
"client" |
CSD — client draws its own titlebar (default) |
"server" |
SSD — driftwm draws a titlebar with the window title and a close button |
"minimal" |
SSD — no titlebar; shadow, corner clip, and border still apply per [decorations] / per-window rules |
"none" |
Bare client surface — compositor adds zero chrome; per-window border/corner/shadow rules are ignored too |
pinned_to_screen = true lifts a window out of the infinite canvas and fixes it
to one output's screen space: it does not pan or zoom with the camera, and it
renders above normal windows (but below panels / Top & Overlay layer-shell
surfaces). Use it for Picture-in-Picture, video-call toolbars, or any always-on
floating overlay.
- Coordinates are output-relative. When pinned,
positionis measured from the output center (still center-anchored and Y-up):[0, 0]centers the window on the monitor,+Yis up. Droppositionto center it. - Movable and resizable like a normal window — drag the title bar (or
Mod-drag) to move, drag a border to resize. Dragging across monitors reassigns it to that output. Combine withwidget = trueto make it immovable. - Fullscreen round-trips. A fullscreen request (or
Mod+F) temporarily unpins the window to fill the screen; exiting fullscreen re-pins it in place. Any canvas pan/zoom exits fullscreen, just like a normal window. - Off the canvas. Pinned windows are excluded from navigation, alt-tab,
snapping, fit/center actions, and canvas screenshots
(
driftwm msg screenshot). They remain focusable and closable; SSD windows show a small dot in the title bar. - Toggle at runtime with the
toggle-pin-to-screenaction (bound toMod+Tby default), which pins/unpins the focused window in place.
position/size are screen-space. The easiest way is to pin the window live,
drag it where you want, then read the numbers back to bake into a rule:
- Open the window (e.g. start Picture-in-Picture), click it to focus, and press
Mod+Tto pin it. It's now in screen space — drag it anywhere with the mouse and resize to taste. - Once it sits where you want, press
Mod+A(home) to bring the viewport to the canvas origin at zoom 1.0. The pinned window stays put on screen. - Press
Mod+Tagain to unpin. At home the window drops back onto the canvas at the same on-screen spot, and one canvas unit is one screen pixel, so its canvas coords now equal its screen position. - Run
driftwm msg stateand copy the window'sapp_id,title,position, andsize. - Write the rule with those values plus
pinned_to_screen = true(anddecoration = "none"for a chrome-free PiP surface).
[[window_rules]]
title = "Picture-in-Picture"
pinned_to_screen = true
position = [540, -350]
size = [570, 320]
decoration = "none"You have to unpin before reading (step 3): pinned windows live in screen space,
not on the canvas, so — like layer-shell panels — they're deliberately absent
from driftwm msg state and canvas screenshots. If you run top/bottom bars,
Mod+A centers the usable area rather than the raw output, so the result can
sit a little off — nudge position to taste.
On a multi-monitor setup, output chooses which monitor a window fullscreens
onto, by output name (e.g. "DP-1" — find names under outputs.* in
driftwm msg state):
[[window_rules]]
app_id = "steam_app_*"
output = "DP-1"Precedence when a window goes fullscreen: the rule's output wins; otherwise the
output the client itself requested; otherwise the active output (where the
pointer is). An unknown or disconnected output name falls through to the next
choice. output only affects fullscreen — it does not move a windowed or
screen-pinned window.
Layer-shell surfaces (panels, notifications, bars like waybar) have no decoration
mode — the decoration field on a rule matching a layer surface is ignored.
Chrome on layers is field-by-field opt-in: set border_width,
corner_radius, and/or shadow directly on the rule. Layers do not inherit
[decorations] defaults for those three fields — without an explicit value on
the rule, a layer surface has no border, no shadow, and no corner clipping.
border_color_focused is also ignored on layers (the focused / unfocused
distinction is window-only); layers always use border_color.
[[window_rules]]
app_id = "waybar"
widget = true
corner_radius = 10
shadow = true
border_width = 2pass_keys controls which compositor keybindings are forwarded to the focused
window instead of being handled by the compositor:
| Value | Behaviour |
|---|---|
false (or omit) |
Compositor handles all keybindings normally (default) |
true |
All keys forwarded — no compositor shortcuts fire while this window has focus |
["mod+q", "ctrl+q"] |
Only the listed combos are forwarded; all other shortcuts stay active |
VT switching (Ctrl+Alt+F1–F12) always stays in the compositor regardless
of pass_keys.
Key combo syntax is the same as in [keybindings]: mod+key, ctrl+shift+key, etc.
When multiple rules match the same window:
trueis sticky-on: if any rule setspass_keys = true, the result istrue.["combo", …]lists are unioned across all matching rules.trueoverrides a list: if one rule saystrueand another says["mod+q"], the result istrue.
[[window_rules]]
app_id = "my-widget"
position = [0, 0]
widget = true
decoration = "none"[[window_rules]]
app_id = "kitty"
opacity = 0.85
blur = true[[window_rules]]
app_id = "steam_app_*"
pass_keys = trueKeep mod+q and other compositor shortcuts active, but pass ctrl+q to the game:
[[window_rules]]
app_id = "factorio"
pass_keys = ["ctrl+q", "ctrl+s"][[window_rules]]
app_id = "/^steam_app_\\d+$/"
pass_keys = true[[window_rules]]
app_id = "myapp-panel"
size = [400, 800]
position = [960, 0]
widget = true# All three rules below apply to the same kitty window and are merged:
[[window_rules]]
app_id = "kitty"
blur = true # sticky-on: cannot be unset by later rules
[[window_rules]]
app_id = "kitty"
opacity = 0.85 # blur from above is preserved
[[window_rules]]
title = "*nvim*" # title match narrows to nvim windows only
opacity = 1.0 # override opacity for nvim (blur still applies)decoration = "minimal" gives you a titlebar-less window that still participates
in compositor chrome — borders, corner clipping, and shadow all apply. Use it
when you want a widget that isn't fully bare. decoration = "none" is the
opposite: a bare client surface where the compositor adds (and ignores) all
chrome overrides.
[[window_rules]]
app_id = "my-clock"
widget = true
decoration = "minimal"
border_width = 2
border_color = "#5c5c5c"
border_color_focused = "#7aa2f7"
corner_radius = 8
shadow = true[[window_rules]]
app_id = "firefox"
shadow = falseSome apps (cosmic-term, etc.) open small utility windows that share the main app_id but have a generic title:
[[window_rules]]
title = "winit window"
widget = trueEnable debug logging to see which rules matched a window at map time:
RUST_LOG=debug driftwm 2>&1 | grep -i "window rule\|app_id"