Skip to main content

Widgets

A kiln widget is just a function that declares nodes, called from your bar function every dirty frame. There is no widget object and no update method: the only question a widget ever raises is "what makes the screen dirty when my data changes?" ui.widget{} answers it, wiring timers and signals to dirty marks and handing you back the plain function.

Two names sound alike and do different jobs. ui.widget is the wiring constructor this page is about. kiln.widgets is the stock widgets (taglist, clock, the form widgets); your own widgets compose stock pieces the same way your bar does.

Prerequisites

A bar of your own (see A bar from scratch). Snippets assume:

local kiln = require("kiln")
local ui = kiln.ui
local widgets = kiln.widgets
local th = kiln.theme
warning

Build widgets once, at config load (top level of your rc), never inside a bar function. ui.widget{ every = n } arms a recurring timer when it is called; calling it inside a declare function would arm a new timer every frame.

1. A seconds clock with every

The built-in widgets.clock() redraws on the minute. For seconds you need a faster cadence:

local seconds_clock = ui.widget {
every = 1,
function()
ui.text(os.date("%H:%M:%S"), { size = 12 })
end,
}

The spec's first positional entry (or a fn field) is the declare function; every = 1 marks every screen dirty each second. Then, in your bar:

	ui.bar(s, { edge = "top" }, function()
ui.spacer()
seconds_clock()
end)

The widget is the function itself: you call it where you want its cells.

Reload. The clock sits at the right end of your bar and the seconds tick visibly: every = 1 marks the screen dirty once a second, so the bar redraws once a second, and only because this widget asked it to.

2. External data: a battery reader with spawn.watch

For data that lives outside the compositor, poll a command and park the result in a table the widget reads. kiln.spawn.watch(cmd, interval, cb) runs the command, feeds each output line to cb, and re-runs it interval seconds after it exits:

local stats = { battery = nil }

kiln.spawn.watch("cat /sys/class/power_supply/BAT0/capacity", 30,
function(line)
stats.battery = tonumber(line)
kiln.dirty()
end)

local battery = ui.widget {
function()
ui.text(stats.battery and (stats.battery .. "%") or "n/a", { size = 12 })
end,
}

Add battery() to your bar and reload. The bar shows your charge percentage, or n/a if there is no BAT0 to read (some machines name it BAT1; adjust the path). Either way the widget renders: the fallback is part of the declare function, not an error case.

The callback stores the fact and calls kiln.dirty(), so the redraw happens when the data changes, not on a render timer; the widget itself needs no every. The same pattern reads CPU load, volume, or anything else a command can print: one spawn.watch per source, one shared stats table.

spawn.watch returns a handle with :stop(), and kiln.reload() stops all watches automatically, so an edited config never doubles its pollers. See Spawn lifecycle.

3. Reacting to signals with watch

watch wires object signals to redraws. Entries are "Class::signal" where the class is one of the globals (client, screen, tag, layer, notification):

local focus_indicator = ui.widget {
watch = { "client::focus", "client::property::title" },
function()
local c = client.focus
ui.text(c and (c.title or c.app_id or "") or "nothing focused",
{ size = 12, color = th.muted })
end,
}

Put it in your bar, reload, and click between two windows: the label follows the focused title as you switch, and reads "nothing focused" on an empty tag. Whenever any client emits focus or property::title, the screens are marked dirty and the next frame re-declares the widget with the new fact. Listeners are registered once per signal name no matter how many widgets watch it. The full signal list is in the signals reference.

(For this particular example watch is belt and braces: focus and title changes already dirty the screen. watch earns its keep for properties you invent yourself, which redraw nothing on their own.)

4. Keyed lists with ui.each

When a widget shows one cell per item of a changing collection, declare it with ui.each(items, key, declare). The key function gives each item a stable id, so a cell's identity (its press handler, its per-item state) follows the item rather than its list position:

local function note_key(n)
return "note:" .. n.seq
end

local notification_chips = ui.widget {
watch = { "notification::added", "notification::dismissed" },
function()
ui.each(notification.all(), note_key, function(n, id, st)
ui.box({
id = id, color = th.accent, radius = 3, pad = { x = 6 },
on_press = function() n:dismiss() end,
}, function()
ui.text(n.title or "", { size = 12 })
end)
end)
end,
}

Wire the chips into your bar, reload, and send yourself a notification:

scripts/kiln-eval 'require("kiln").notify { title = "hello" }'

A chip labeled "hello" appears in the bar alongside the popup. Press the chip and both vanish: the press handler dismisses the notification, the dismissal fires notification::dismissed, and the next frame declares one chip fewer.

declare(item, id, st) receives a per-key state table st that survives across frames as long as the key is still declared, then vanishes with it. Define the key function at module level, not inline: the state store is scoped by the key function's identity.

5. A stock meter

A percent meter is a track with a fill whose width is a percentage of its parent: two nested boxes, no drawing code. That composition ships stock as widgets.progress, so a meter is one call:

	widgets.progress({ id = "meter", value = 0.42,
w = { "grow", max = 100 }, h = 8 })

Drop that line into your bar function and reload: a thin 100 px track appears with the left 42 percent filled in the accent color.

The form widgets (progress, separator, toggle, slider) are controlled: you own the value and the widget draws it. Nothing inside widgets.progress remembers 0.42; the next frame draws whatever your declare function passes, which is exactly how every other fact on the bar already behaves. Wiring one to ui.widget is therefore just reading your own state in the declare function, as the complete example below does. The cfg for each form widget is in the kiln.widgets reference.

Complete example

A battery meter, wired end to end:

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

local stats = { battery = nil }

kiln.spawn.watch("cat /sys/class/power_supply/BAT0/capacity", 30,
function(line)
stats.battery = tonumber(line)
kiln.dirty()
end)

local battery = ui.widget {
function()
local pct = stats.battery
if pct == nil then
ui.text("no battery", { size = 12, color = th.muted })
return
end
ui.row({ gap = 6, align = { y = "center" } }, function()
ui.text(pct .. "%", { size = 12 })
widgets.progress({ id = "bat", value = pct / 100,
w = { "grow", max = 100 }, h = 8,
color = pct < 20 and th.urgent or th.accent })
end)
end,
}

screen.on("added", function(s)
tag.new { name = "1", screen = s, layout = kiln.layout.tile }
ui.bar(s, { edge = "top" }, function()
widgets.taglist(s)
ui.spacer()
battery()
widgets.clock()
end)
end)

Reload. On battery hardware the meter tracks your charge; on anything else the bar reads "no battery" instead of erroring. The fill even recolors below 20 percent, because state read at declare time is the entire styling model: spawn.watch owns the number, ui.widget owns the dirty mark, and widgets.progress just draws whatever it is handed.

See also