Keybindings
kiln.key{} is how input reaches your config. One call registers one chord,
and the same call carries the description that the hotkeys popup reads back.
Prerequisites
A running kiln and a config you can edit (see Basics). Snippets assume the usual header:
local kiln = require("kiln")
local key, button = kiln.key, kiln.button
1. Your first binding
key { mods = { "mod" }, key = "d",
press = function() kiln.spawn("fuzzel") end }
One call, one chord. mods is a list of modifier names, key is the key
name, press runs on the press edge. Reload (or paste this through
scripts/kiln-eval) and mod+d spawns a launcher.
Bound chords are consumed by the compositor: the press never reaches the focused client, so a binding cannot half-leak into a terminal.
2. Mods and kiln.modkey
Modifier names are ctrl, alt, shift, super. The special name "mod"
resolves through kiln.modkey at bind time, so the whole config retargets
with one line:
kiln.modkey = "super"
Set it before your key{} calls. Matching is exact: { "mod" } does not fire
while shift is also held, so { "mod" } and { "mod", "shift" } on the same
key are two distinct bindings.
3. Press and release
Both handlers are optional and independent:
key { mods = { "mod" }, key = "v",
press = function() kiln.spawn("pactl set-source-mute @DEFAULT_SOURCE@ 0") end,
release = function() kiln.spawn("pactl set-source-mute @DEFAULT_SOURCE@ 1") end }
A push-to-talk key: reload, hold mod+v, and the mic is live for exactly as
long as the key is down. A release-only binding works too; the chord is still
consumed on the press edge so the release reliably arrives.
4. desc and group: the hotkeys popup
key { mods = { "mod" }, key = "d", desc = "app launcher", group = "launch",
press = function() kiln.spawn("fuzzel") end }
desc and group change nothing about dispatch. They are recorded in the
binding registry, which kiln.key.all() returns, and the default config's
mod+s cheat sheet is built entirely from that registry.
Reload, then press mod+s. Your binding is on the sheet: a launch group
with an "app launcher" row, next to everything the default config declared.
Press mod+s again to dismiss it. Every binding you annotate documents
itself this way. The popup ships in the stdlib (kiln.hotkeys), and the
same registry powers a custom one; see the
hotkeys popup guide.
5. Ranges
A key of the form "1-9" or "a-z" expands into one binding per value, and
the handler receives the value: digits arrive as numbers, letters as letters.
key { mods = { "mod" }, key = "1-9", desc = "view tag", group = "tag",
press = function(i)
local t = screen.focused.tags[i]
if t then t:view() end
end }
key { mods = { "mod", "ctrl" }, key = "a-z", desc = "mark", group = "demo",
press = function(letter)
kiln.notify { title = "pressed", message = letter }
end }
Reload and press mod+ctrl+m. A notification pops up with the letter you
pressed; try a few others and the message follows the key. The registry keeps
each range as one entry, so the cheat sheet shows a single 1-9 view tag
row instead of nine.
6. Client verbs with kiln.focused
Handlers that act on "the focused client" all need the same guard. kiln.focused(fn)
wraps a function to run with client.focus as its first argument, and to do
nothing when no client is focused:
key { mods = { "mod" }, key = "q", desc = "close client", group = "client",
press = kiln.focused(function(c) c:close() end) }
key { mods = { "mod" }, key = "f", desc = "fullscreen", group = "client",
press = kiln.focused(function(c) c:toggle_fullscreen() end) }
Reload, focus a window, and press mod+f: it fills the screen; again and it
tiles back. Press mod+q on an empty tag and nothing happens, quietly: that
is the guard doing its job, not an error being swallowed.
c:close() is the polite close (the client is asked to quit). c:kill()
sends SIGKILL. This is the opposite of AwesomeWM, where c:kill() is the
polite one.
7. Mouse bindings
kiln.button{} is the same shape with a button number instead of a key, plus
an on field naming where the press must land: "client" (the default) for
presses on a client surface, "root" for presses on empty workarea. The
handler receives the client (or nil on the root).
button { mods = { "mod" }, button = 1, press = function(c) c:grab_move() end }
button { mods = { "mod" }, button = 3,
press = function(c) c:grab_resize_nearest() end }
button { mods = {}, button = 2, on = "root",
press = function() kiln.spawn(os.getenv("TERMINAL") or "foot") end }
The first two are the default config's move and resize drags: hold mod and
drag with the left button and the window follows the pointer; drag with the
right button and it resizes from whichever corner is nearest. Buttons: 1
left, 2 middle, 3 right. A plain unbound click on a client simply focuses it.
Presses on bar cells are handled by the cells' own on_press handlers, not
by button{}; see the bar tutorial.
8. Clearing and re-registering
kiln.key.clear() drops every key binding, kiln.button.clear() every mouse
binding. kiln.reload() calls both before re-running your config, which is
why editing the file and reloading never duplicates anything.
Re-running a key{} call over IPC is safe: rebinding a chord replaces its
registry row in place, so the cheat sheet never shows duplicates. Still,
scripts/kiln-eval 'require("kiln").reload()'
is the cleanest way to apply binding edits from a file. (require("kiln")
returns the same module your rc holds as kiln; the local itself is not
visible to the socket.)
Complete example
local kiln = require("kiln")
local key, button = kiln.key, kiln.button
kiln.modkey = "super"
key { mods = { "mod" }, key = "Return", desc = "terminal", group = "launch",
press = function() kiln.spawn(os.getenv("TERMINAL") or "foot") end }
key { mods = { "mod" }, key = "d", desc = "app launcher", group = "launch",
press = function() kiln.spawn("fuzzel") end }
key { mods = { "mod" }, key = "q", desc = "close", group = "client",
press = kiln.focused(function(c) c:close() end) }
key { mods = { "mod" }, key = "space", desc = "toggle floating", group = "client",
press = kiln.focused(function(c) c.floating = not c.floating end) }
key { mods = { "mod" }, key = "1-9", desc = "view tag", group = "tag",
press = function(i)
local t = screen.focused.tags[i]
if t then t:view() end
end }
key { mods = { "mod", "shift" }, key = "1-9", desc = "move to tag", group = "tag",
press = kiln.focused(function(c, i)
local t = screen.focused.tags[i]
if t then c.tags = { t } end
end) }
key { mods = { "mod" }, key = "Escape", desc = "quit", group = "system",
press = kiln.quit }
button { mods = { "mod" }, button = 1, press = function(c) c:grab_move() end }
button { mods = { "mod" }, button = 3,
press = function(c) c:grab_resize_nearest() end }