-
Notifications
You must be signed in to change notification settings - Fork 1
D Bus API
UnixNotis exposes the standard Freedesktop notification interface and a private, generation-aware control interface on the user session bus.
The standard interface remains the compatibility surface for senders. UnixNotis also advertises
selected desktop extensions, including KDE-compatible inline reply when the notification payload
contains an explicitly supported reply action. Inline reply and NotificationReplied are extensions;
they are not part of the base Freedesktop 1.2 method set.
-
org.freedesktop.Notificationsis the compatibility surface for notification senders -
com.unixnotis.Controlis a private bundled-version interface for the daemon, panel, popup renderer, andnoticenterctl; it is not a stable third-party API - additive fields and methods may be added to the private interface
- private consumers should check
GetApiVersion()and use the matching generated proxy - notification actions and replies must always carry the exact
idandgeneration
-
Bus name:
org.freedesktop.Notifications -
Object path:
/org/freedesktop/Notifications -
Interface:
org.freedesktop.Notifications
Methods:
GetCapabilities() -> string[]Notify(app_name, replaces_id, app_icon, summary, body, actions, hints, expire_timeout) -> u32CloseNotification(id: u32) -> ()GetServerInformation() -> (name, vendor, version, spec_version)
Signals:
NotificationClosed(id: u32, reason: u32)ActionInvoked(id: u32, action_key: string)-
NotificationReplied(id: u32, reply_text: string)— KDE-compatible extension
Reported capabilities include actions, inline-reply, body, and icon-static.
body-markup is not advertised because notification bodies are normalized to display text. The
sound capability is advertised only when sound is enabled, a playback backend is available,
sound.allow_file_hints = true, and at least one sound.allowed_file_hint_dirs entry resolves.
GetServerInformation reports UnixNotis as name and vendor, the package version as server
version, and notification specification 1.2.
Notify sanitizes all fields before storage. A non-zero replaces_id only replaces a notification
when the sender ownership and process lifetime checks match. Otherwise a new id is assigned.
CloseNotification is also sender-ownership checked; a sender cannot close another application's
notification by guessing its numeric id.
The sound-file hint is ignored unless [sound].allow_file_hints is enabled and the requested
path is contained by a configured [sound].allowed_file_hint_dirs entry. Accepted sender hints are
local regular uncompressed PCM WAV files no larger than 16 MiB; invalid or disallowed hints fall
back to sound-name and then the configured sound defaults. file:// hints must name a local path;
remote hosts are rejected.
busctl --user introspect org.freedesktop.Notifications /org/freedesktop/Notifications-
Bus name:
com.unixnotis.Control -
Object path:
/com/unixnotis/Control -
Interface:
com.unixnotis.Control -
API version: query with
GetApiVersion()
The daemon accepts only trusted UnixNotis clients. Same-user code is not a hard security boundary; the authorization layer verifies caller identity and install/build provenance, but applications that already control the same user session can still share a bus connection.
Control calls are accepted only from the current user and the trusted UnixNotis binary set. The
Linux path requires the session bus to provide a stable ProcessFD; the daemon checks that the
descriptor still identifies the reported process before and after reading its executable path.
Installed binaries are checked against the startup provenance snapshot. A third-party script should
call noticenterctl instead of depending on this private interface directly.
GetApiVersion() -> u32GetState() -> ControlStateGetSnapshot() -> ControlSnapshotGetUiHealth() -> UiHealthListActive() -> NotificationView[]ListPopupCandidates() -> NotificationView[]ListHistory() -> NotificationView[]GetActiveNotification(id: u32) -> NotificationView[]GetPopupCandidate(id: u32) -> PopupCandidate[]GetNotificationDiagnostics(id: u32) -> NotificationDiagnosticsView[]ListInhibitors() -> InhibitorInfo[]
GetActiveNotification and GetPopupCandidate return an empty array when no matching current
record exists. A ControlSnapshot captures state, active rows, and history under one store lock.
OpenPanel() -> ()OpenPanelDebug(level: PanelDebugLevel) -> ()ClosePanel() -> ()TogglePanel() -> ()RefreshApplications() -> ()SetDnd(enabled: bool) -> ()SetDndUntil(expires_at: i64) -> ()ToggleDnd() -> ()Inhibit(reason: string, scope: u32) -> u64Uninhibit(id: u64) -> ()
SetDndUntil accepts one future Unix timestamp in seconds no more than 366 days ahead. Zero and
past timestamps are invalid. Use SetDnd(false) to clear DND;
plain SetDnd(true) enables an indefinite DND state. Future deadlines are persisted and revalidated
after restart.
DismissGeneration(id: u32, generation: u64) -> ()InvokeActionGeneration(id: u32, generation: u64, action_key: string, confirmed: bool) -> ()ReplyNotification(id: u32, generation: u64, reply_text: string) -> ()ClearAll() -> ()ClearActive() -> ()ClearHistory() -> ()
The daemon validates the exact active generation, advertised action, interaction policy, sender ownership, and confirmation state before emitting an application signal. A successful action or reply removes a non-resident generation after delivery; resident rows remain active. Failed or stale operations do not remove a row.
MarkPanelReady() -> ()MarkPanelNotReady() -> ()MarkPopupsReady() -> ()MarkPopupsNotReady() -> ()MarkPopupMaterialized(id: u32, generation: u64) -> ()MarkPopupVisible(id: u32, generation: u64) -> ()
The center and popup client call readiness methods after subscriptions and GTK initialization. A
visible generation is never returned by ListPopupCandidates after a reconnect. A generation that
was admitted but never became visible remains eligible for a later renderer.
NotificationAdded(id: u32, generation: u64)NotificationUpdated(id: u32, generation: u64)NotificationClosed(id: u32, generation: u64, reason: CloseReason)StateChanged(state: ControlState)PopupGateChanged(gate: PopupGateState)SnapshotInvalidated()InhibitorsChanged(active: bool, count: u32)PanelRequested(request: PanelRequest)
NotificationAdded and NotificationUpdated carry identity only; clients fetch the full current
view. SnapshotInvalidated asks clients to refresh active/history lists after broad mutations.
PopupGateChanged contains only popup suppression state and is not a replacement for delivery-stage
updates.
All values crossing the private interface are sanitized and bounded before they are serialized. The exact limits are part of the bundled API version and may be tightened in a later version. Important limits for clients are:
- notification body text: at most 16 KiB;
- application name: at most 256 bytes;
- summary: at most 1 KiB;
- category: at most 256 bytes;
- action pairs: at most 8;
- action keys: at most 128 bytes each;
- action labels: at most 256 bytes each;
- hint entries: at most 16, with keys at most 64 bytes and string values at most 2 KiB;
- inline reply text: at most 4 KiB;
- reason text for an inhibitor: at most 256 UTF-8 bytes after trimming;
- active inhibitors: at most 128;
- retained image data: at most 256 KiB and 256×256 source dimensions;
- file-backed sender visuals: at most 2 MiB before bounded decode;
- SVG input: at most 1,024,000 bytes and raster output no larger than 2048×2048;
- diagnostic and plugin output: separately bounded before it is placed in a view.
Clients should treat a truncated or absent optional field as normal input rather than assuming that an unbounded payload can be requested.
dnd_enabled: bool-
dnd_expires_at: i64— future Unix timestamp, or0 history_count: u32inhibited: boolinhibitor_count: u32
id: u32generation: u64-
app_name: string— daemon-selected display label attribution: NotificationAttributionsummary: stringbody: stringactions: Action[]inline_reply: InlineReplyinline_reply_policy: InlineReplyPolicy-
urgency: u8—0low,1normal,2critical category: stringis_transient: boolreceived_at_unix_seconds: i64image: NotificationImagepopup_decision: PopupDecisionRecordpopup_hide_after_ms: u64
popup_hide_after_ms is a sanitized banner duration. It is not a command to close the daemon's
active record.
NotificationAttribution includes display_name, claimed_name, claimed_desktop_id, desktop_id,
badge_icon, status, assurance, interactions, reason, diagnostic_detail, and group_key.
IdentityAssurance values are Authenticated, SystemAssociated, PortalAssociated,
UserAssociated, Unresolved, Conflict, and Relay. Interaction policy is independent for
default activation, action buttons, and inline reply. claimed_name and claimed_desktop_id are
sender-controlled attribution inputs: they may be retained for bounded presentation and diagnostics
but never grant authority. The configuration rule claimed_app can match the claimed name
explicitly. The app rule uses only daemon-resolved identity and fails closed without positive
assurance; the daemon never synthesizes trusted identity from an unresolved claimed value.
NotificationImage contains:
-
badge_icon: string— daemon-owned application badge -
claimed_theme_icon: string— bounded decorative theme lookup hint -
claimed_desktop_id: string— sender-provideddesktop-entryhint retained only for bounded decorative lookup; it is not trusted application identity and mirrors the attribution field above sender_visual_role: none | conversation_avatar | application_provided_icon | content_imagesender_visual: ImageDatacontent_image: ImageData
Images are bounded sanitized pixels. Sender paths are opened and decoded inside the daemon; clients never receive a sender-provided filesystem path. Conversation avatars use the identity slot, application-provided visuals are decorative, and content images are attachments.
PopupDecisionRecord contains admission, renderer state, health revision, maximum visible count,
decision timestamp, delivery stage, and the fixed popup_hide_after_ms value.
Admission values are Show, Rule, Dnd, Inhibitor, RendererUnavailable, and
RendererDisabled. Delivery stages are Suppressed, Admitted, FanoutFailed, RendererFetched,
Materialized, and Visible.
CloseReason values:
-
1expired -
2dismissed by the user -
3closed by a D-Bus call -
4undefined
PanelAction: 0 open, 1 close, 2 toggle.
PanelDebugLevel: 0 off, 1 critical, 2 warn, 3 info, 4 verbose.
The private interface uses these named records. Their fields are versioned with
GetApiVersion(); clients should not infer a tuple layout from a single implementation build.
-
ControlSnapshot: currentControlState, activeNotificationView[], and historyNotificationView[]captured under one store lock. -
UiHealth:center_process_running,center_ready,popups_process_running,popups_ready, and the readinessrevision. -
PopupCandidate: aNotificationViewwith the popup admission and delivery decision used for seeding a renderer. -
NotificationDiagnosticsView: attribution, interaction policy, popup admission, renderer state, delivery stage, and resolved timeout data for one generation. -
InhibitorInfo: wire tuple(id, reason, scope, owner_unique_name)in that order. -
Action:keyandlabel; authorization policy is carried separately in the notification interaction fields. -
InlineReply:available,label,placeholder,submit_label, andsubmit_icon. -
InlineReplyPolicy:0allow,1confirm,2deny. -
ImageData: width, height, row stride, alpha flag, bits per sample, channel count, and bounded pixel bytes. -
PanelRequest: requestedPanelActionandPanelDebugLevel. -
PopupGateState:dnd_enabledandinhibitedpopup-gate flags.
Inhibitors are owned by the caller's unique bus name and removed automatically when that connection exits. The D-Bus scope values are:
| Scope | Value | Meaning |
|---|---|---|
| all notification output | 0 |
inhibit every notification presentation/output path |
| popups | 1 |
suppress popup presentation only |
[inhibit].mode is separate from scope. The mode controls what the daemon does with notifications
while an inhibitor applies:
-
no_popups: retain/store notifications while suppressing popup presentation; -
drop_all: discard incoming notifications while inhibition applies.
Inhibitor details:
- no more than 128 active inhibitors are retained;
- reason text is trimmed and bounded to 256 UTF-8 bytes;
- only the owning unique bus name can remove its token;
- removing an unknown token is a no-op;
- disconnecting the owner removes all of its inhibitors.
busctl --user introspect com.unixnotis.Control /com/unixnotis/ControlSubscribe to signals before fetching the initial snapshot. Treat the private interface as a bundled
client contract, preserve generation fields, and prefer noticenterctl for scripts and keybinds.