Client Rules
A rule runs when a client maps, before it takes focus. It is the place to say "this app lives on that tag", "dialogs float", or "this window never steals focus", without touching a keybinding.
local kiln = require("kiln")
kiln.rule {
match = { dialog = true },
props = { floating = true },
on = kiln.placement.centered,
}
A rule has up to three parts: selectors (which clients it matches), props (state to apply), and on (a callback for anything a property write cannot express).
The full spec (selector keys, clause fields, props semantics) is in the rules reference. What matters for writing rules that work:
- String values are Lua patterns, and a list means any-of. Escape magic characters: to match the literal class
Blueman-manager, write"Blueman%-manager". - Wayland clients identify by
app_id; X11 clients byclass,instance, androle. An app that can run either way needs both named:
kiln.rule {
match_any = { app = { "Gpick" }, class = { "Gpick" } },
props = { floating = true },
}
propswrites properties;on(c)runs code. Placement lives inon, because a placement helper is just a function of the client. Sending a client to a tag never switches the view to that tag; if you want to follow it, do so inon(c).
A rule with no selector at all matches nothing, not everything. To match every client, use an explicit always-true predicate: match = { fn = function() return true end }.
Ordering: every match applies
Rules are not first-match. Every rule that matches a client applies, in the order the rules were declared, and a later rule overrides an earlier one's props. That makes broad-then-specific stacking natural: a catch-all placement rule first, app-specific overrides after it.
-- Every client: place new floats in free space, and never off screen.
kiln.rule {
match = { fn = function() return true end },
on = function(c)
kiln.placement.no_overlap(c)
kiln.placement.no_offscreen(c)
end,
}
-- App-specific rules declared later win on conflicts.
Rules only apply to normal windows; X11 override-redirect surfaces (menus, tooltips) bypass them entirely. kiln.rule.clear() drops every registered rule, which is what a config reload uses.
Worked example: browser to its own tag
Assuming a tag named "web" exists (created in your screen.on("added") handler):
kiln.rule {
match_any = { app = { "firefox" }, class = { "firefox" } },
props = { tag = "web" },
}
To jump to the browser when it opens, add the follow in on:
kiln.rule {
match_any = { app = { "firefox" }, class = { "firefox" } },
props = { tag = "web" },
on = function(c)
if c.tag ~= nil then c.tag:view() end
end,
}
Worked example: dialogs float, centered
dialog = true matches any client with a parent window (file pickers, confirmation prompts):
kiln.rule {
match = { dialog = true },
props = { floating = true },
on = kiln.placement.centered,
}
Placement helpers compose by being called in sequence, so "centered, but never off screen" is two calls in a custom on:
on = function(c)
kiln.placement.centered(c)
kiln.placement.no_offscreen(c)
end
Worked example: a scratchpad-style terminal
Launch a terminal under a dedicated app id, and give that id a rule that always floats it, keeps it on top, and centers it:
kiln.key {
mods = { "mod" }, key = "grave",
desc = "scratchpad terminal", group = "launch",
press = function() kiln.spawn("foot --app-id=scratchpad") end,
}
kiln.rule {
match = { app = "^scratchpad$" },
props = { floating = true, ontop = true, sticky = true },
on = kiln.placement.centered,
}
sticky = true keeps it visible on every tag of its screen, so it follows you around like a drop-down terminal. The ^...$ anchors keep the pattern from also matching a longer app id.