Perch
Lua APIThe host API

The host API

The functions every mod gets — presence, data, timers, logging — and their exact semantics.

2 min readUpdated Jul 21, 2026

Every handler receives host; the global perch is the same table — capability namespaces (perch.time, perch.media…) and host verbs live side by side on it. Use whichever spelling reads better.

host.set_presence(widget, present, data)

host.set_presence("worldclock", true, { city = "Tokyo", time = "17:42" })
host.set_presence("worldclock", false)

Declares that a widget exists (or stops existing) and delivers its payload in one call. The arbitration engine takes it from there — placement, entrance, exit, and every negotiation with other widgets are not your concern.

  • For widgets with a transient context, presence auto-retracts after the context’s timeout_ms; a fresh set_presence/push_data extends the moment.
  • Call it false from on_stop if your data becomes meaningless when the mod stops; otherwise the runtime retracts for you.

host.push_data(widget, data)

host.push_data("worldclock", { time = "17:43" })

The steady-state channel. The payload is a table serialized to JSON; nodes bound to changed fields repaint, and nothing else happens.

Partial payloads are the intended style — push what changed. Merging is by key at every level that matters:

  • Melt: sustained pushes are admitted at ~5/s per mod (burst of 20). An over-cap push parks; parked payloads merge per key (newest wins) and deliver as the window drains. The final state of a burst always reaches the view — coalesced, never dropped.
  • A parked art-only push survives a later art-less push — disjoint keys coexist.

host.timer(name, ms, once?)

host.timer("poll", 60 * 1000)      -- repeating, floored at 1000 ms
host.timer("settle", 250, true)    -- one-shot, floored at 100 ms
host.cancel_timer("poll")

Named timers firing on_timer(host, name). Re-arming a name replaces it; cancel_timer removes it. Floors are enforced (1000 ms repeating / 100 ms one-shot) — they are the idle-cost contract, not a suggestion.

host.log(msg)

host.log("fetch ok: " .. #items .. " items")

Writes to the Perch log as perch: mod[<id>]: <msg>. Visible with the app logs; pairs with PERCH_LOG=mods=debug for the loader’s own tracing.

What host is not

  • There is no request_quit, no window handle, no DOM access, no way to reach another mod. The host surface is deliberately exactly what a well-behaved widget needs.
  • Handlers all run on the main thread; host calls are non-blocking, always.
esc
Type to search
navigate open