Skip to content

Notification Lifecycle

locainin edited this page Aug 26, 2026 · 2 revisions

Notification Lifecycle

UnixNotis stores notifications as active, generation-specific records. A popup is one presentation of that record, not the record itself. Whether hiding a popup also closes the active notification depends on the resolved timeout policy.

Terminology

  • notification: the sanitized payload accepted from an application;
  • active record: the generation-specific daemon record that can still receive validated actions;
  • popup/banner: the temporary on-screen presentation of an active record;
  • history row: an informational record retained after the active generation was closed;
  • generation: the changing identity attached when an id is created or replaced.

Identity and generations

Every active notification has an (id, generation) key. A replacement may reuse the freedesktop notification id, but it receives a new generation. Bundled clients send both values for dismissals, actions, replies, and popup delivery acknowledgements.

This prevents stale UI controls from acting on a newer replacement that happens to reuse the same numeric id.

The daemon owns:

  • payload sanitization and sender attribution
  • rule application and timeout resolution
  • active and history storage
  • popup admission and delivery state
  • action and inline-reply authorization
  • sender ownership checks

The panel and popup renderer consume sanitized NotificationView values. Displayed names, icons, avatars, and message text do not grant authority. Rules that need trusted application matching use app; the explicit claimed_app rule matches only sender-controlled app_name presentation data.

State model

incoming Notify request
    |
    +-- sanitize, attribute, apply rules, resolve timeout policy
    |
    +-- active store ------------------------ panel row and validated controls
    |      |
    |      +-- popup admitted -> materialized -> visible -> banner hidden
    |      |                                      |
    |      |                                      +-- record may remain active
    |      |                                      +-- or close at its own deadline
    |      |
    |      +-- user/app close, expiration, action, reply, or replacement
    |
    +-- history according to close and transient policy

A popup process can disconnect and reconnect. Delivery-stage tracking prevents a generation that was already visible from being replayed as a fresh popup. A candidate admitted but never made visible can still be offered to a later renderer.

Timeout policy

Timeouts are resolved once when the generation is committed. A later configuration reload does not change an existing generation's deadline.

Protocol input and flags Popup banner Active record
expire_timeout = 0 No automatic hide No automatic close
positive timeout, non-critical, resident or not Hides after the requested duration Closes after the same duration
positive timeout, critical Hides after the requested duration Remains active
expire_timeout = -1, ordinary Uses popups.default_timeout_ms Remains active after the banner hides
expire_timeout = -1, non-critical transient, resident or not Uses the configured popup timeout Closes after that duration when it is positive
expire_timeout = -1, critical with a configured timeout Uses critical_timeout_ms Remains active
expire_timeout = -1, critical without a configured critical timeout No automatic hide Remains active
expire_timeout = -1, non-critical non-transient resident Uses the resolved banner timeout Remains active

A positive sender timeout closes every non-critical active record, including resident records. A critical popup can hide at that deadline while its active record remains. Under the default -1 policy, transient controls the automatic active close for non-critical records when the resolved popup timeout is positive, even when resident; the resident flag instead controls dismissal after successful actions and replies. The transient flag also controls history treatment.

Rules can modify urgency, timeout, resident, and transient flags before this table is evaluated. See Rules.

When popups.pause_on_hover is enabled, automatic popup expiration pauses while the pointer is over the popup and resumes with the remaining timeout when the pointer leaves; allow_click_through = true disables this because the popup does not receive pointer input.

Closing and history

An expiration closes the exact active generation and emits the freedesktop close reason Expired. Explicit application closure, user dismissal, successful non-resident action delivery, successful non-resident reply delivery, and replacement also invalidate the old controls.

History retention is separate from popup visibility:

Event Active record History result
default-policy ordinary banner hides remains unchanged
positive timeout elapses removed retained according to close/history policy
user dismisses removed dismissal policy applies
app calls CloseNotification removed close reason is recorded when retained
successful non-resident action or reply removed controls are never replayed from history
successful resident action or reply remains unchanged
same id is replaced old generation removed replacement policy applies
stale control targets an old generation unchanged operation is rejected

Dismissed or expired history rows are informational. They cannot replay historical D-Bus actions or inline replies.

Actions and inline replies

An action is delivered only when all of these still match:

  • the current active id and generation
  • an action key advertised by that generation
  • the daemon-selected interaction policy
  • the original D-Bus sender ownership
  • any required user confirmation

Failed signal delivery, denied confirmation, stale generation, or sender-ownership failure leaves the active record available unless another lifecycle event closes it.

Inline reply is a KDE-compatible extension exposed when the sender advertises an explicit reply action. It uses the same generation and sender checks as ordinary actions. Reply text is bounded and trimmed; a reply is accepted only when the notification advertised the matching reply action.

DND and inhibitors

DND suppresses ordinary popup and sound admission while retaining notifications for the panel. Critical notifications may still be admitted according to configuration.

An inhibitor carries a D-Bus scope, while [inhibit].mode controls handling. Scope 0 applies to all notification output. Scope 1 suppresses popup presentation only. The handling modes are:

  • no_popups: retain/store notifications while suppressing popup rendering;
  • drop_all: discard incoming notifications while the inhibitor is active.

Timed DND stores an absolute Unix deadline. A future deadline resumes after daemon restart; an elapsed deadline is cleared during startup.

Visual roles

The daemon selects one bounded role for notification imagery:

  • conversation avatar: identity image in the lead slot
  • application-provided visual: decorative sender image
  • content image: message attachment or content thumbnail

Sender-controlled host paths are never handed to UI clients. Historical rows clear sender pixels and visual roles, so a retained history row may intentionally have no image. See Theming and Security for the file, SVG, and media-art boundaries.

Diagnostics

Inspect the current generation and its resolved policy with:

noticenterctl dev explain-notification <ID>
UNIXNOTIS_DIAGNOSTIC=1 noticenterctl dev dump-active
noticenterctl doctor --verbose

The explanation includes attribution, action policy, popup admission, renderer readiness, delivery stage, and the resolved timeout. It is diagnostic only and does not bypass generation or sender checks.

Lifecycle example

Notify arrives
  -> sanitize and attribute
  -> apply rules and resolve the generation's timeout policy
  -> store the active generation
  -> admit and materialize a popup when allowed
  -> mark the popup visible
  -> hide only the banner when its display clock expires
  -> keep panel actions while the active record remains valid
  -> close on explicit dismissal, application close, replacement, or a resolved close deadline
  -> retain an informational history row when policy allows

The popup renderer cannot close an active notification by itself. The daemon remains the authority for generation checks, sender ownership, action delivery, and expiration.

Clone this wiki locally