AnotherOS

Documentation

Notifications

Banners, alerts, action buttons and the Notification Centre — from a shell, from C, or from the toolkit.

Fasadas has a notification system modelled on macOS: banners that slide in at the top right and leave by themselves, alerts that wait for a decision, action buttons whose click reaches the application that posted, grouping by application with a count, Do Not Disturb with a schedule, and a history that outlives the compositor.

Clicking the menu-bar clock opens the Notification Centre: a sidebar with the history, a calendar, weather, system metrics and the Do Not Disturb switch.

Everything is drawn by the compositor, for the same reason the menu bar is: a notification must appear over every window, and it must outlive the process that posted it — a daemon posting one and exiting is the normal case, not an edge case.

From a shell or a daemon

anotify needs no window. Its flags, output and exit codes are a stable interface.

anotify --app "Software Update" --id 1 --alert \
        --title "Updates available" --body "3 packages can be updated." \
        --action Install --action Later --wait
# posted 1 shown
# action 1 Install
Flag Meaning
--app NAME Required. The name on the card and in Settings.
--title, --subtitle, --body At least one of title or body.
--action LABEL A button, up to four. Repeat for more.
--alert Ask for alert style, which stays until answered. The user's setting wins.
--id N Posting the same application and id again replaces it in place, including from a later run.
--open PATH An absolute executable or .aop bundle the compositor runs on a click after this process has exited.
--icon PNG An absolute image path. Defaults to the application's icon.
--wait Stay connected and report the user's choice.
--timeout S Give up waiting after S seconds.
--withdraw N Remove it from the screen and the history. 0 means all of this application's.

Output is always posted <id> <state>. With --wait you then get one of action <n> <label>, dismissed, removed or timeout.

Exit codes: 0 ok, 1 usage, 2 no compositor or the connection was lost, 3 refused, 4 dismissed or removed, 5 timed out.

From C

#include "anotify.h"          /* link -lfasadas */

anotify *n = anotify_open("Software Update", 5.0);
anotify_note note = { .title = "Updates available", .body = "3 packages",
                      .actions = { "Install", "Later" }, .nactions = 2,
                      .alert = 1 };
uint32_t id = anotify_post(n, 1, &note);

anotify_event ev;
if (anotify_wait(n, id, -1, &ev) == 1 &&
    ev.kind == ANOTIFY_ACTION && ev.value == 1)
    install();

anotify_close(n);

anotify_open() fails with ENOENT or ECONNREFUSED when there is no compositor, ETIMEDOUT when one is listening but not answering, and ENOTSUP against a compositor without notifications.

A daemon with its own loop polls anotify_fd() and calls anotify_dispatch(); anotify_set_callback() receives every event.

From a toolkit application

An application built on the AO toolkit uses AO_UserNotificationCenter, modelled on NSUserNotificationCenter. It rides the application's existing compositor connection — no second client — and the answer comes back to a delegate on the run loop, like every other event.

static void on_note(AO_UserNotificationCenter *nc,
                    const AO_UserNotificationEvent *ev, void *user)
{
    if (ev->kind == AO_USER_NOTIFICATION_EVENT_ACTIVATED && ev->action == 1)
        install();                      /* ev->action_title is "Install" */
}

AO_UserNotificationCenter *nc = AO_UserNotificationCenterDefault();
AO_UserNotificationCenterSetDelegate(nc, on_note, NULL);

AO_UserNotification *n = AO_UserNotificationCreate();
AO_UserNotificationSetIdentifier(n, "updates");
AO_UserNotificationSetTitle(n, "Updates available");
AO_UserNotificationSetInformativeText(n, "3 packages can be updated.");
AO_UserNotificationAddAction(n, "Install");     /* returns 1 */
AO_UserNotificationAddAction(n, "Later");       /* returns 2 */
AO_UserNotificationSetStyle(n, AO_USER_NOTIFICATION_STYLE_ALERT);
AO_UserNotificationCenterDeliver(nc, n);
AO_Release(n);                     /* the centre copied what it needs */
  • The identifier is the key. Delivering the same identifier again replaces the notification in place, including from a later run. Leave it unset for a one-shot and one is made up for you.
  • Actions are numbered the same way everywhere on this page: 0 is a click on the notification itself, 1 to n are your buttons in order. At most four; a fifth is refused at the call, not silently dropped at delivery.
  • The delegate runs on the UI thread, on a run-loop turn — never inside some other call that happened to read the connection while waiting for its own reply. Delivering before the loop has started is fine; the events wait for its first turn.
  • The style is a request. The user's per-application setting wins.
  • AO_UserNotificationCenterIsAvailable() says whether the compositor implements notifications at all, so delivering to one that does not fails loudly instead of vanishing.

What happens to a notification

Every post is answered with exactly one state:

State When
shown On screen now.
dnd Do Not Disturb: recorded in the history, not shown.
quiet The application's style is None: recorded, never on screen.
blocked The application is not allowed to notify: neither shown nor recorded.
rejected Malformed — no title and no body, or an unterminated string.

Then, as they happen: expired (a banner left by itself — it is still in the history and still clickable there), dismissed (closed without a choice), removed (cleared, withdrawn, or pushed out by age), and the action events.

Actions reach the connection that posted, and only while it lives. A click after the poster has gone runs its --open target instead, if it has one.

Banners, alerts and grouping

The geometry is a contract. Nothing about a card is measured from its text.

Card 344 × 74 px, corner radius 12. Text is fitted and ellipsized, never grows the card.
Position 12 px from the right edge, the first 10 px below the 22 px menu bar.
Stacking Top to bottom, newest application first, 10 px apart, at most five on screen.
Grouping One card per application, showing the newest. Up to two 6 px plates peek below it, and a red count badge sits on the icon.
Buttons An 88 px column on the right, its buttons splitting the card's height evenly.
Close box A circle on the card's top-left corner, shown while the pointer is on the card. It closes the whole stack.
Banner lifetime 5 s, configurable from 2 to 60. A new banner restarts its group's clock; the pointer resting on a card holds it, and it lingers 2 s after the pointer leaves.
Animation In from the right edge in 0.30 s, out in 0.25 s, reflow in 0.20 s.

An alert always shows its button column; a banner shows it only under the pointer. With no actions an alert shows Close; one action shows it and Close; two show both; more than two show the first and Options, which drops down the rest.

A card never takes the keyboard, and an alert blocks nothing. Every window keeps its input and the compositor keeps compositing. Clicking the card body is the default action: the card leaves and the notification stays in the history, marked read.

Cards are a solid material rather than live blur. A blur behind a card that moves every frame of its slide would cost a blur per frame, and the slowest supported machine sets the frame budget.

Settings and Do Not Disturb

notifications.conf in the configuration directory; the compositor re-reads it on change.

Key
dnd 1 = Do Not Disturb on, now.
dnd_scheduled, dnd_from, dnd_to The schedule, in minutes after midnight, local time. to at or before from wraps midnight, so 22:00–07:00 is 1320 and 420. Equal values are an empty window.
<App>_enabled 0 blocks the application.
<App>_style 0 None (history only), 1 Banners, 2 Alerts. Overrides what the poster asked for.
banner_secs Banner lifetime.

<App> is the application name with every byte that is not a letter or digit turned into an underscore, so "Software Update" becomes Software_Update.

Under Do Not Disturb nothing is put on screen, alerts included; everything is still recorded in the history.