A small rule engine for the niri Wayland compositor that listens to IPC events and runs user-defined window rules to move, resize, toggle state, or execute commands.
  • Rust 97.7%
  • Shell 1.6%
  • Dockerfile 0.7%
Find a file
Vlad Verestiuc b2a4219585
All checks were successful
CI / arch-image (push) Successful in 5s
CI / ci-image (push) Successful in 6s
CI / build (push) Successful in 21s
CI / test (push) Successful in 17s
CI / dist-artifacts (push) Successful in 13s
CI / arch-package (push) Successful in 29s
CI / release (push) Successful in 9s
Use tag version for Arch package
2026-02-11 22:31:12 +02:00
.forgejo/workflows Align release workflow with tag-based model 2026-02-11 22:12:52 +02:00
ci Optimize CI with cached images 2026-02-08 20:59:20 +02:00
log Add niri-rules config and rule engine 2026-02-04 03:28:07 +02:00
misc/niri-rules-git Use tag version for Arch package 2026-02-11 22:31:12 +02:00
src Format action module imports 2026-02-09 18:11:31 +02:00
tests Document rules and align tests 2026-02-09 15:35:00 +02:00
.gitignore Ignore log directory outputs 2026-02-11 22:15:33 +02:00
AGENTS.md Document rules and align tests 2026-02-09 15:35:00 +02:00
build.rs Add build-time version flag 2026-02-06 18:18:47 +02:00
Cargo.lock Add niri-rules config and rule engine 2026-02-04 03:28:07 +02:00
Cargo.toml Add niri-rules config and rule engine 2026-02-04 03:28:07 +02:00
config.kdl Add niri-rules config and rule engine 2026-02-04 03:28:07 +02:00
INSTRUCTIONS.md Add close-window action and focus tracing 2026-02-09 18:07:40 +02:00
README.md Add close-window action and focus tracing 2026-02-09 18:07:40 +02:00

niri-rules

niri-rules is a small rule engine for the niri Wayland compositor. It listens to the niri IPC event stream and executes user-defined window rules on specific events.

This project is focused on “window-rules, but with actions”: move/size windows and run arbitrary commands when a rule matches.

What It Can Do

  • Subscribe to niri events and evaluate rules.
  • Match windows by app-id, title, and a set of boolean flags.
  • Execute external commands (command).
  • Place a window before/after another matched window (place-before, place-after).
  • Toggle urgent and floating.
  • Move a window to an output (open-on-output).
  • Set window width/height (fixed pixels or proportion of output size).
  • Close a window (close-window).

Requirements

  • niri compositor running and IPC available.
  • niri on $PATH (this tool shells out to niri msg ...).
  • Rust toolchain (to build).

Build And Run

cargo build --release
./target/release/niri-rules

Notes:

  • Default config path is config.kdl (override with --config).
  • Run niri-rules --help to see available CLI options.

Logging

Logging uses tracing.

  • By default logs are printed to stdout.
  • Override log file:
    • --log-file /path/to/file.log
    • NIRI_RULES_LOG_FILE=/path/to/file.log
  • Control verbosity with RUST_LOG (via tracing-subscriber EnvFilter).

Example:

RUST_LOG=niri_rules=debug ./target/release/niri-rules

Configuration (config.kdl)

The configuration format is KDL (parsed with knus).

At the top level you declare one or more window-rule blocks:

window-rule {
  match app-id="^Alacritty$" event="window-created"
  place-before app-id="^zen$"
  window-width { proportion 0.20; }
}

Rule Evaluation Semantics

  • A window-rule must contain at least one action.
  • match ... entries are ORed together:
    • If there are no match entries, the rule matches everything.
    • If there are match entries, at least one must match.
  • exclude ... entries are ORed together:
    • If any exclude matches, the whole rule is rejected.

Events

You can match by event string using the event="..." matcher property. Currently supported events:

  • window-created
  • window-closed
  • window-focused
  • window-blurred

Matchers

match and exclude nodes accept the same set of properties.

match app-id="^Alacritty$" title="fish" event="window-created" is-floating=false
exclude title="scratchpad"

Supported matcher properties:

  • app-id="REGEX": Rust regex pattern matched against the window app_id
  • title="REGEX": Rust regex pattern matched against the window title
  • event="window-created|window-closed|window-focused|window-blurred"
  • is-active=true|false: true if the window is the active window in its workspace
  • is-focused=true|false
  • is-floating=true|false
  • is-urgent=true|false
  • at-startup=true|false: true during the first ~60s after niri-rules starts
  • is-active-in-column=true|false: currently parsed, but not implemented yet (will never match)
  • is-window-cast-target=true|false: currently parsed, but not implemented yet (will never match)

Actions

Actions are the other statements inside a window-rule block. A rule may contain multiple actions; they run in order.

command

Executes an external command. The first argument is the program; the rest are arguments.

command "notify-send" "niri-rules" "matched!"

Environment variables passed to the command (when available):

Focused Window
  • NIRI_RULES_FOCUSED_WINDOW_ID
  • NIRI_RULES_FOCUSED_WINDOW_APP_ID
  • NIRI_RULES_FOCUSED_WINDOW_TITLE
  • NIRI_RULES_FOCUSED_WINDOW_WORKSPACE_ID
  • NIRI_RULES_FOCUSED_WINDOW_WORKSPACE_NAME
  • NIRI_RULES_FOCUSED_WINDOW_WORKSPACE_OUTPUT
Previous Focused Window
  • NIRI_RULES_PREV_FOCUSED_WINDOW_ID
  • NIRI_RULES_PREV_FOCUSED_WINDOW_APP_ID
  • NIRI_RULES_PREV_FOCUSED_WINDOW_TITLE
  • NIRI_RULES_PREV_FOCUSED_WINDOW_WORKSPACE_ID
  • NIRI_RULES_PREV_FOCUSED_WINDOW_WORKSPACE_NAME
  • NIRI_RULES_PREV_FOCUSED_WINDOW_WORKSPACE_OUTPUT

place-before / place-after

Moves the current window so it ends up immediately before/after a target window. The target is described using matcher properties (same shape as match).

place-before app-id="^zen$"
place-after app-id="^zen$"

Notes:

  • This action relies on the windows scrolling layout position (pos_in_scrolling_layout).
  • Under the hood it runs niri msg action move-column-left/right on the focused window.

urgent

Sets or unsets urgent state:

urgent true
urgent false

floating

Moves the window to floating or back to tiling:

floating true
floating false

open-on-output

Moves the window to a specific output:

open-on-output "DP-1"

window-width / window-height

Set window size either as fixed pixels or as a proportion of the windows output size.

window-width  { fixed 480; }
window-height { fixed 320; }
window-width  { proportion 0.333; }

Notes:

  • Proportional sizes require output dimensions from niri msg -j outputs.
  • If the output name can be resolved but the output size isnt known, the action will log an error and no-op.

close-window

Closes the matched window via niri msg action close-window.

close-window

More KDL Examples

Multiple match And exclude

match entries are OR'ed together, exclude entries are OR'ed together.

window-rule {
  match event="window-created" app-id="^Alacritty$"
  match event="window-created" app-id="^kitty$"
  exclude title="scratchpad"

  window-width { fixed 420; }
}

Chain Multiple Actions

window-rule {
  match event="window-created" app-id="^Alacritty$"
  place-before app-id="^zen$"
  floating true
  urgent true
  window-width { proportion 0.20; }
  command "notify-send" "niri-rules" "placed + sized terminal"
}

Use at-startup

window-rule {
  match event="window-created" at-startup=true app-id="^Alacritty$"
  place-before app-id="^zen$"
}

Move To An Output

window-rule {
  match event="window-created" app-id="^Alacritty$"
  open-on-output "DP-1"
}

Fixed Size With Width + Height

window-rule {
  match event="window-created" app-id="^Alacritty$"
  window-width  { fixed 480; }
  window-height { fixed 320; }
}

Command Using Focus Env Vars

window-rule {
  match event="window-focused"
  command "bash" "-lc" "echo focused=$NIRI_RULES_FOCUSED_WINDOW_APP_ID prev=$NIRI_RULES_PREV_FOCUSED_WINDOW_APP_ID"
}

Example: Multiple Rules

window-rule {
  match event="window-focused" app-id="^Alacritty$"
  command "notify-send" "focused alacritty"
}

window-rule {
  match event="window-created" app-id="^Alacritty$"
  place-before app-id="^zen$"
  window-width { proportion 0.20; }
}