Skip to main content

Theming

kiln has no theme file, no theme format, and no style engine. It has a table.

1. What kiln.theme is

kiln.theme is one plain Lua table of colors and metrics. Everything the stdlib draws (the focus ring, the bar, taglist cells, titlebars, menus, tooltips, notifications, the prompt) reads it at declare time, on every dirty frame. Assign to the table, mark the screen dirty, and the next frame wears the new values.

Because it is a plain table (not a signal-emitting object), writing to it does not redraw by itself. In your rc that never matters (the first frame comes after the config runs); at runtime you follow a write with kiln.dirty().

2. The keys

The full list with defaults lives in the theme variable reference. The groups:

  • Palette: bg, fg, accent, and friends. accent works hardest: focus ring, selected tag, focused tasklist entry, caret, selection.
  • Metrics: sizes and spacing, from font_size and gap to bar_height and border_width.
  • Titlebars and popups: the titlebar_*, menu_*, prompt_*, and notification_* keys.

3. Edit theme values in your rc

The default config does exactly this, near the top, applying a whole palette at once (it ships three: gruvbox, catppuccin, nord, with the chosen name persisted to ~/.config/kiln/theme):

local kiln = require("kiln")
local th = kiln.theme

kiln.modkey = "super"
th.bg = "#1e1e2e"
th.accent = "#cba6f7"

Any key you do not set keeps its default. Colors are "#rrggbb" or "#rrggbbaa" strings.

One subtlety: values the stdlib reads at declare time follow later changes automatically, but values you copy into a cfg table are fixed at that call. In ui.bar(s, { color = th.bg }, ...) the bar captured the current th.bg; change the theme later and that bar keeps its captured color until the config reloads. Omit color from the cfg and the bar reads theme.bg live instead: a bar's defaults are filled per frame, which is a guarantee, not an accident. See the bar defaults.

4. Per-tag gap

theme.gap sets the workarea inset and the space between layout cells everywhere. A tag can override it: t.gap wins on that tag, and writing it redraws immediately (tag layout parameters are on the dirty list).

screen.on("added", function(s)
local t = tag.new { name = "media", screen = s, layout = kiln.layout.max }
t.gap = 0
end)

A zero-gap max tag gives you edge-to-edge video while every other tag keeps its breathing room. Try it live on the current tag:

scripts/kiln-eval 'screen.focused.selected_tag.gap = 0'

5. Live-tweak over IPC

Every kiln exposes a Lua socket, so theme iteration needs no reload loop. Change the accent and watch the very next frame:

scripts/kiln-eval 'local kiln = require("kiln") kiln.theme.accent = "#ff9e64" kiln.dirty()'
scripts/kiln-eval 'local kiln = require("kiln") kiln.theme.gap = 16 kiln.dirty()'
scripts/kiln-eval 'local kiln = require("kiln") kiln.theme.client_radius = 10 kiln.dirty()'

(The require("kiln") is needed because kiln is a local in your rc, not a global the socket can see; requiring returns the same live module.)

Focus ring, selected tag, tasklist highlight all flip together. When a combination sticks, copy the assignments into your rc. Remember kiln.dirty(): without it the values change but nothing redraws until something else dirties the screen. More on the socket in IPC and scripting.

Complete example

A cohesive cool-gray theme with a teal accent, as the top of an rc:

local kiln = require("kiln")
local th = kiln.theme

-- palette
th.bg = "#2e3440"
th.bg2 = "#242933"
th.fg = "#d8dee9"
th.accent = "#88c0d0"
th.muted = "#4c566a"
th.urgent = "#d08770"
th.close = "#bf616a"

-- titlebars
th.titlebar = "#3b4252"
th.titlebar_focus = "#434c5e"
th.client_radius = 6
th.border_width = 2

-- metrics
th.font_size = 13
th.gap = 10
th.bar_height = 30

Everything downstream (bar, taglist, titlebars, menus, notifications, prompt) picks these up with no further wiring, because they are all declared from the same table on the next frame.

See also