← Otto Developer Guideotto-islands

User guide: Dynamic Island
Specs: Dynamic Island, Notification Island, Notification Daemon, Portal Access Dialog

otto-islands is the strip of bubbles at the top centre of the screen. It does three jobs for the session. It is the notification daemon: anything that calls org.freedesktop.Notifications (notify-send, a browser, a mail client) ends up here. It shows activities: running tasks other programs report over D-Bus, and the track a media player is playing. And it draws the system dialog: the permission and question panel that the portal backend, the agent service and the log-out menu ask through. It runs as its own process, an otto-kit app on a layer-shell surface, started by the compositor from an exec_once entry in the shipped config. The compositor knows nothing about it beyond that.

The model

Three ideas carry the whole design.

Everything is an activity. A notification, a running download, the music player: each is one activity in a single shared list, with an app id, a title, a body, an icon, and optionally progress, actions and a timeout. The D-Bus services only edit that list and wake the UI. The UI reads the list and makes the screen match it. Nothing on the D-Bus side touches a surface.

One island is one activity, and one subsurface. The layer surface is a transparent container. Each activity gets its own Wayland subsurface, and that subsurface lives as long as the activity does. It is never swapped for another to change shape: the same bubble grows from a circle (Mini) to a pill (Compact) to a card (Expanded) and back. Activities from the same app overlap into a deck rather than being folded into one representative, so every notification stays addressable.

The compositor animates, the client only paints. Shape, corner radius, blur, shadow, position and size are set through the surface style protocol, and every change is a spring the compositor runs. The client redraws a buffer only when what it shows changes, and between those moments it sleeps.

Callers feed one activity list; the compositor shows and animates the islands

Start to finish: a notification

  1. An app calls Notify on the session bus. The call lands on a tokio task in otto-islands that owns the org.freedesktop.Notifications name.
  2. The daemon works out who sent it. It asks the bus for the caller’s process, then takes the Flatpak sandbox’s own record of its app id, or else the desktop-entry hint, or else the desktop entry that runs the process’s executable. A notification passed on by xdg-desktop-portal was already checked there and goes through as is.
  3. It looks that app up in the notifications table of the permission store. An app the table has never seen is recorded as allowed, so Settings can list it and switch it off. An app switched off is dropped, but the caller still gets an id back, from a range nothing on screen uses. See Permissions for the table.
  4. The daemon adds an activity to the shared list (or updates one in place for replaces_id), marks the list dirty and wakes the UI thread.
  5. The UI thread reconciles. It hands the whole list to the dock first, then drops the quiet activities and diffs the rest against its islands: a new activity gets a new subsurface, a gone one is animated out and destroyed a moment later, once the exit animation has played.
  6. Layout places the row. Islands are grouped into decks by app id, groups are ordered by their oldest notification, and the row is centred, so a growing island pushes its neighbours both ways. The push cascades outward one island at a time.
  7. Each island’s new mode, size and position go to the compositor as one spring transaction. The buffer is redrawn only if its content signature (mode, icon, text, actions, size) changed.
  8. A new notification opens Expanded so it can be read on sight. After a few seconds, unless the pointer is on it, it settles back to Mini. When its timeout runs out it is marked expired and stops announcing itself, but it stays in the list: it has not been read.
  9. The dock reads the same list. Every app with outstanding notifications gets a badge with the count, published through otto_dock_v1.
  10. The user opens the island and clicks it. A click on an action, or on the body for the default action, emits ActionInvoked, asks the compositor to focus the app, removes the activity and emits NotificationClosed. A click on the Close zone only removes it and emits NotificationClosed. The badge drops by one on the next sync.

The full presentation rules (modes, decks, arrival, hover, click targets) are in the notification island spec.

Activities, music and the dialog

Running tasks. org.otto.Island1 lets any process create, update and dismiss an activity. A live activity is a job, not news: it ignores its timeout, ends only when its app says so, and shows its progress as a ring or a bar. The same progress fills the app’s dock icon. A quiet activity is reported to the dock and left out of the row. The app sets it while its own window is in front, where a bubble would only repeat what the window says.

Music. otto-islands is also an MPRIS client. It follows the session’s players on its own threads and keeps one live activity, with the app id org.otto.music, in step with whatever is playing. It goes quiet while the player’s window is focused. The equaliser bars sit on a child subsurface of the music island, the one thing redrawn on a timer, and only while a track plays. They are measured from the player’s own PipeWire stream, matched by process or by name, never from the default output. The capture stream is open only while the bars move. A player with no presence in the PipeWire graph is taken to be playing on another device, and a cast glyph stands in for the bars.

The dialog. org.otto.Dialog1 presents one panel at a time from a queue. A request is a title, a body, an icon and zero or more choice groups, the shape of the portal’s Access interface. The D-Bus call blocks until the user answers, and the answer travels back to it on a one-shot channel. A dialog whose caller has stopped waiting is answered as ended and taken down, and a caller can withdraw its own by cookie. A modal dialog takes the keyboard exclusively and catches clicks over the whole layer. A non-modal one takes the keyboard briefly so it can be answered by typing, catches clicks only on itself, and shrinks into a circle at the end of the row when the user moves on, still waiting. The portal access dialog spec has the contract.

Boundaries

Decisions

Limits and direction

Open questions are tracked in the dynamic island spec.

See also