-
Notifications
You must be signed in to change notification settings - Fork 1
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.
- 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.
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.
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.
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.
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.
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 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.
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.
Inspect the current generation and its resolved policy with:
noticenterctl dev explain-notification <ID>
UNIXNOTIS_DIAGNOSTIC=1 noticenterctl dev dump-active
noticenterctl doctor --verboseThe 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.
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.