Plugin lifecycle¶
Plugins are scheduled, sandboxed Lua programs. Cleat's plugin engine calls into your plugin at well-defined moments; this page walks through those moments.
The three callbacks¶
Every plugin defines (some subset of) three top-level functions:
function collect(ssh, cfg)
-- Fetch raw data from the remote host.
return "..."
end
function transform(raw, cfg)
-- Parse raw data into a structured value.
return { ... }
end
function render(ctx, store, cfg)
-- Draw the UI from store data.
ctx:section({ ... })
end
collect and transform are required for plugins that produce data
(snapshot / timeseries / log / table). render is optional but
expected for plugins with a visible panel.
How cleat schedules them¶
┌─────────────────────────────────────────┐
│ Scheduler thread (one per cleat run) │
├─────────────────────────────────────────┤
│ every default_interval seconds: │
│ raw = collect(ssh, cfg) │ ← runs on scheduler thread
│ value = transform(raw, cfg) │ ← runs on scheduler thread
│ store.append(value) │
└────────────────┬────────────────────────┘
│
▼ store data
┌─────────────────────────────────────────┐
│ Main thread (UI render loop, 60fps) │
├─────────────────────────────────────────┤
│ on plugin panel visible: │
│ render(ctx, store, cfg) │ ← runs on main thread
└─────────────────────────────────────────┘
The key takeaways:
collectblocks the scheduler thread. Use timeouts on long-running commands (ssh:exec("...", timeout_ms)). The scheduler runs your plugin in series with every other plugin on the same connection, so a 30-secondcollectblocks everyone.transformruns immediately aftercollecton the same thread, with the raw return value. Pure function — no host access (thessharg is not available here).renderruns on the UI thread. Keep it fast — it's called every frame the panel is visible. Don't do I/O or expensive parsing here; that belongs intransform.
Streaming mode¶
For plugins that consume a continuous data stream (logs, syscall
traces, etc.), set streaming = true in metadata. Then:
collect(ssh, cfg)is called once at plugin start. It returns the command string to stream.transform(line, cfg)is called for each line of output, with the individual line asraw. Returns a value to append to the store.render(ctx, store, cfg)is unchanged.
The engine maintains the channel and restarts it on
streaming_idle_seconds of silence.
The cfg argument¶
cfg is the user's per-plugin configuration as filled in by the
Plugin Manager's Settings panel. Values are typed according to the
plugin's settings = { ... } array:
plugin = {
...
settings = {
{ key = "warn_pct", label = "Warning %", type = "number", default = "80" },
{ key = "skip_lo", label = "Skip lo iface", type = "boolean", default = "true" },
{ key = "mode", label = "Mode", type = "string", default = "polling",
values = { "polling", "streaming" } },
},
}
function collect(ssh, cfg)
if cfg.skip_lo then ... end -- boolean
local threshold = cfg.warn_pct -- number
local mode = cfg.mode -- string
end
See the manifest settings reference for the full schema.
Data types¶
Each plugin declares a data_type in its metadata. This drives how
the engine stores returned values and what methods are available on
store in render():
data_type |
transform() returns |
store:* methods |
Best for |
|---|---|---|---|
snapshot |
a table — exposed as store directly |
(none — store is the table itself) |
"Latest value" data: uptime, OS info, summary stats |
timeseries |
a table of { name = value, ... } numeric values (one tick at a time) |
latest, range, avg, max, min, count |
Graphed metrics — CPU, memory, latency |
log |
a table of log entries { t, level, message, source, labels } |
logs, latest_logs, count_logs |
Tailing events — auth failures, error logs |
table |
a table — exposed as store directly |
(none — store is the table itself) |
Structured rows: containers, ports, processes |
Snapshot and table both bypass the data-store query API — they pass
the latest transform() return verbatim to render() as store.
Timeseries and log accumulate data across ticks and expose query
helpers via the store API.
Sandbox guarantees¶
Cleat runs each plugin instance in its own Lua state with the
standard library pruned: io, os.execute, os.exit, os.remove,
os.rename, os.tmpname, debug, loadfile, dofile, package,
load, loadstring, rawset, rawget, rawequal, rawlen are
all removed.
Plugins only reach outside their Lua state through the explicitly
provided API: ssh:*, cleat:*, cleat.kv:*, ctx:*, store:*.
Anything else — direct filesystem access, child processes,
arbitrary network calls — is not available.
What cleat doesn't do¶
- No retries on
collect()errors. Ifcollect()raises a Lua error, the engine logs it, increments the plugin's error counter, and tries again on the next tick. After enough consecutive failures, the plugin enters paused state with an exponential backoff. The user can hit Retry now in the Actions row to short-circuit. - No deduplication of state across ticks. If you want to track
"did anything change since last tick?", store the last seen value
via
cleat.kv:set()and compare in the next call. - No cross-pane shared state by default. Each (pane × plugin)
pair gets its own data store. Use
cleat.kv:*for per-connection shared state across plugins.