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.
| Event | Payload | Permission |
|---|---|---|
media.change | NowPlaying | null | media.read |
battery.change | BatteryState | battery.read |
calendar.change | null; read calendar.events() again | calendar.read |
reminders.change | null | reminders.read |
clipboard.change | { text: string | null } | clipboard.read |
network.change | { online: boolean } | — |
system.wake / system.sleep | null | — |
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 25From the Shortcuts app, or anywhere, with a URL:
adanotch://ext/local.you.pomodoro/start?data=25It 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.