Ada Extensions
Guides

Events and timers

React to changes, keep your own schedule, take commands from Shortcuts.

Your extension is a long-running process; it doesn't have to wait for update to be called. It can subscribe to events, start timers and change its view with ctx.render whenever it likes.

Subscribing to events

Subscribe in setup:

const { activity, events, media, ui } = AdanotchKit;

activity({
  setup(ctx) {
    events.on("media.change", (track) => {
      // track: NowPlaying | null
      ctx.refresh();
    });
    events.on("system.wake", () => ctx.refresh());
  },
  async update() {
    const track = await media.nowPlaying();
    return { compact: { trailing: ui.text(track?.title ?? "—") } };
  },
});

events.on returns a function that unsubscribes; events.off(event, handler) does the same.

EventPayloadPermission
media.changeNowPlaying | nullmedia.read
battery.changeBatteryStatebattery.read
calendar.changenull; read calendar.events() againcalendar.read
reminders.changenullreminders.read
clipboard.change{ text: string | null }clipboard.read
network.change{ online: boolean }—
system.wake / system.sleepnull—
appearance.change{ appearance: "light" | "dark" }—
command{ name: string, data: JSONValue }—

Handlers can be async. An error thrown in one is logged and doesn't stop the extension.

refresh: manual

For an extension that only changes on events, set "refresh": "manual" in manifest.json; Adanotch won't call update on its own, so it doesn't run for nothing.

Timers

setTimeout, setInterval, clearTimeout, clearInterval and queueMicrotask work as in the browser. You can have up to 200 timers at once; setInterval runs every 50 ms at the fastest.

let endsAt: number | null = null;
let tick: number | undefined;

activity({
  onAction(action, ctx) {
    if (action === "start") {
      endsAt = Date.now() + 25 * 60_000;
      clearInterval(tick);
      tick = setInterval(() => {
        if (endsAt && Date.now() >= endsAt) {
          clearInterval(tick);
          endsAt = null;
          AdanotchKit.notify.island({ text: "Time for a break", icon: "cup.and.saucer", tone: "success" });
          ctx.refresh();
        }
      }, 1000);
    }
  },
  update() {
    return {
      compact: { trailing: endsAt ? ui.timer(endsAt) : ui.icon("timer") },
      expanded: ui.button("Start 25 min", "start", { primary: true }),
    };
  },
});

Don't re-render to count seconds

ui.timer(date) counts on its own. You don't need to call render every second; start a timer only to do something when the time is up.

Partial updates: ctx.render

To show progress during long work:

async update(ctx) {
  ctx.render({ compact: { trailing: ui.icon("arrow.triangle.2.circlepath", { tone: "secondary" }) } });
  const repos = await loadRepos();                       // slow
  return { compact: { trailing: ui.badge(String(repos.length)) }, expanded: list(repos) };
}

Where there's no ctx at hand, AdanotchKit.render(view) does the same.

Commands and Shortcuts

You can send your extension commands from outside. From the terminal:

adanotch ext local.you.pomodoro start 25

From the Shortcuts app, or anywhere, with a URL:

adanotch://ext/local.you.pomodoro/start?data=25

It arrives as a command event. data is parsed as JSON when it is JSON, and comes as a string otherwise:

events.on("command", ({ name, data }, ctx) => {
  if (name === "start") startTimer(Number(data) || 25);
  if (name === "stop") stopTimer();
  ctx.refresh();
});

That way you can tie a Shortcut to a key combination, a Focus mode or an automation and drive your extension from it.

On this page