Building widgets

The window.PERCH API

The single global the runtime injects before your widget loads — data in, and (for interactive widgets) signals out.

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

MethodPurpose
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 onData after 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.