Before your widget’s HTML loads, the native app installs one global object,
window.PERCH. It’s the entire interface between your widget and the hub. There
is no other bridge — no network, no token, no imports.
Methods
| Method | Purpose |
|---|---|
onData(cb) | Register your renderer. cb(payload) runs on every data update. If data already arrived, it fires immediately with the last payload. |
ready() | Signal the UI is hydrated (flips the loading card to “live”). Optional — onData delivery calls it for you. |
trigger(actionId, args?) | Fire a pre-registered action (interactive widgets). args is an optional JSON object. |
pulse(detail?) | Echo the Perch “bird beat” on the Mac (and a notification, if granted). Side-effect-free; works in release builds. |
onPermission(cb) | Observe Mac-side capability status, e.g. { notifications: "granted" | "denied" | "notDetermined" }. |
requestPermission(name) | Request a Mac-side capability (e.g. "notifications"); the result arrives via onPermission. |
Receiving data
onData is all most widgets need. The payload is exactly the JSON last pushed to
the widget (via push_data or its data endpoint).
window.PERCH.onData((d) => {
d = d || {};
if (d.available === false) return; // a metric may be unavailable on some Macs
render(d);
});
window.PERCH.ready();
Delivery semantics:
- The callback fires once per update, with the full payload (not a diff).
- It replays the last payload if you register
onDataafter data has already arrived — so you never miss the first value, regardless of script timing. - Data is passed as a structured argument, never string-interpolated, so payloads can’t break out into code.
Sending signals out
Two outbound calls exist. Both post over a native message-handler bridge — not
the network — so the connect-src 'none' CSP still holds.
// Interactive widgets: invoke a named action registered against this widget.
button.addEventListener("click", () => window.PERCH.trigger("mutetoggle"));
window.PERCH.trigger("volset", { vol: 40 }); // with arguments
// Any widget: a calm echo back to the Mac (bird beat + optional notification).
window.PERCH.pulse({ count: 3 });
trigger only fires actions registered against the widget’s own id — see
Interactive actions. In release builds the action runner is
disabled, so trigger is a no-op there; pulse works in every build.
Permissions
A widget may request a Mac-side capability on mount and react to its status:
window.PERCH.onPermission((p) => {
if (p.notifications !== "granted") showHint();
});
window.PERCH.requestPermission("notifications");
Reserved internals — don’t touch
The runtime uses these on the object; treat them as private:
__cb, __last, __deliver, __perm, __permCb, __deliverPermission.
Your widget should only ever call the six public methods above.