Before your widget’s HTML loads, the native app installs one global object,
window.PREEN. 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?) | Chirp this phone’s bird on the Mac — each paired device has its own bird in the hub’s device strip. Side-effect-free; works in release builds. |
db | The widget’s own durable datastore on the Mac — db.put / get / update / delete / clear. Each returns a Promise. Works in release builds. |
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.PREEN.onData((d) => {
d = d || {};
if (d.available === false) return; // a metric may be unavailable on some Macs
render(d);
});
window.PREEN.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.PREEN.trigger("mutetoggle"));
window.PREEN.trigger("volset", { vol: 40 }); // with arguments
// Any widget: a calm echo back to the Mac (chirps this phone's bird there).
window.PREEN.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.
Storing data
onData gives you a single, last-value feed pushed to you. When a widget needs
to remember things across reloads — a high score, a history of runs, anything
it computes itself — use its datastore, window.PREEN.db. Each
widget gets its own SQLite database on the Mac; the calls are async and return
Promises:
// Append a record, then read recent ones back.
await window.PREEN.db.put("sessions", { dur: 5400, end: Date.now() });
const recent = await window.PREEN.db.get("sessions", { limit: 30 });
const best = Math.max(0, ...recent.map((r) => r.body.dur));
Unlike localStorage (which is unreliable in the widget sandbox), the datastore
is durable, queryable, and survives reloads. It works in release builds. See
Widget datastore for the full API.
Reserved internals — don’t touch
The runtime uses these on the object; treat them as private:
__cb, __last, __deliver, __dbReq, __dbPending, __dbResolve.
Your widget should only ever call the public methods above.