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