Skip to main content

IPC and Scripting

Every running kiln listens on a unix socket and evaluates whatever Lua you send it, in the same VM your config runs in. Anything your config can do, a shell script can do: the entire API surface is drivable from outside.

1. The socket and the protocol​

The socket is $XDG_RUNTIME_DIR/kiln.sock (owner-only, mode 0600), or wherever KILN_SOCK pointed when the compositor started. The protocol is minimal: connect, write Lua source, half-close, read one reply. The kiln-eval script in the kiln repo wraps that with nc:

scripts/kiln-eval 'return 1 + 1'
# 2

echo 'core.dirty()' | scripts/kiln-eval

An argument is sent as-is; with no argument, stdin is sent. Because the socket evaluates arbitrary Lua in your session, it is deliberately restricted to your own user.

2. Expressions and statements​

The evaluator tries your input as an expression first, then as a statement block:

  • An expression replies with its value: 'client.focus.title' answers the title directly, no return needed.
  • Statements run and reply empty: 'screen.focused.tags[2]:view()'.
  • A multi-statement chunk that should answer something needs an explicit return at the end.

Multiple return values come back tab-joined; each value goes through tostring, so tables print as table: 0x... (format what you want to see):

scripts/kiln-eval 'return #client.all(), #screen.all()'
# 5 2

Errors come back prefixed ERROR: with the Lua message.

note

The reply is capped at 4096 bytes. For big dumps, return compact strings, or write to a file from inside the chunk and read that.

3. One-liners​

The seven config globals (client, screen, tag, layer, notification, drag, core) are available directly; the kiln module is one require away.

List every client:

scripts/kiln-eval 'local out = {}
for _, c in ipairs(client.all()) do
out[#out + 1] = (c.app_id or c.class or "?") .. " [" .. (c.title or "") .. "]"
end
return table.concat(out, "\n")'

Switch to tag 2 on the focused screen:

scripts/kiln-eval 'screen.focused.tags[2]:view()'

Tweak the theme live:

scripts/kiln-eval 'require("kiln").theme.accent = "#ff8800" core.dirty()'

Raise a notification:

scripts/kiln-eval 'require("kiln").notify { title = "build", message = "done" }'

Reload the config:

scripts/kiln-eval 'require("kiln").reload()'

Most one-liners like these have a kiln-client verb that spares you the Lua: kiln-client client list, kiln-client tag view 2, kiln-client theme set accent "#ff8800", kiln-client notify --title build done, kiln-client reload. It speaks the same socket, adds --json for scripts, and ships shell completions; see the kiln-client reference. Raw eval remains the door to anything without a verb.

4. Target a nested dev instance​

Each kiln instance owns one socket. Your daily session uses the default path; a nested test instance started with make dev picks a private socket (like /tmp/kiln-dev-1.sock) and prints it at startup, so scripts target it explicitly:

KILN_SOCK=/tmp/kiln-dev-1.sock scripts/kiln-eval 'return #screen.all()'

See Reload and Debugging for the nested-instance workflow, and Testing Headless for driving an invisible instance in tests.

5. Request-reply only: getting events out​

The socket has no subscription mechanism: you cannot ask it to stream events. When something outside kiln needs to hear about changes, invert the flow: a signal listener inside the config pushes out with kiln.spawn:

-- In your config: tell an external script whenever focus changes.
client.on("focus", function(c)
kiln.spawn({ "/home/me/bin/on-focus-change", c.app_id or "" })
end)

Polling over IPC is the other option and is fine for status bars, since a query is one cheap socket round trip.

6. Scripting examples​

A layout cycler, bindable in any external tool:

#!/bin/sh
# kiln-next-layout: cycle the focused tag's layout
exec scripts/kiln-eval '
local kiln = require("kiln")
local t = screen.focused.selected_tag
if t ~= nil then
t.layout = kiln.layout.next(t.layout)
end
return kiln.layout.name(t.layout) or "custom"'

A focused-window query for a status bar:

#!/bin/sh
# focused-title: print the focused window, empty when none
exec scripts/kiln-eval \
'return client.focus and (client.focus.title or client.focus.app_id) or ""'

Both are plain request-reply, so they compose with watch loops, status bar exec modules, and cron alike.

See also​