Skip to content

D Bus API

locainin edited this page Aug 12, 2026 · 7 revisions

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.

Stability policy

  • org.freedesktop.Notifications is the compatibility surface for notification senders
  • com.unixnotis.Control is a private bundled-version interface for the daemon, panel, popup renderer, and noticenterctl; 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 id and generation

Freedesktop notification interface

  • 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) -> u32
  • CloseNotification(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

UnixNotis control interface

  • 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.

Read methods

  • GetApiVersion() -> u32
  • GetState() -> ControlState
  • GetSnapshot() -> ControlSnapshot
  • GetUiHealth() -> UiHealth
  • ListActive() -> 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.

Panel, DND, and application-index methods

  • OpenPanel() -> ()
  • OpenPanelDebug(level: PanelDebugLevel) -> ()
  • ClosePanel() -> ()
  • TogglePanel() -> ()
  • RefreshApplications() -> ()
  • SetDnd(enabled: bool) -> ()
  • SetDndUntil(expires_at: i64) -> ()
  • ToggleDnd() -> ()
  • Inhibit(reason: string, scope: u32) -> u64
  • Uninhibit(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.

Generation-aware notification methods

  • 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.

UI readiness and popup delivery

  • 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.

Signals

  • 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.

Payload limits

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.

Data types

ControlState

  • dnd_enabled: bool
  • dnd_expires_at: i64 — future Unix timestamp, or 0
  • history_count: u32
  • inhibited: bool
  • inhibitor_count: u32

NotificationView

  • id: u32
  • generation: u64
  • app_name: string — daemon-selected display label
  • attribution: NotificationAttribution
  • summary: string
  • body: string
  • actions: Action[]
  • inline_reply: InlineReply
  • inline_reply_policy: InlineReplyPolicy
  • urgency: u80 low, 1 normal, 2 critical
  • category: string
  • is_transient: bool
  • received_at_unix_seconds: i64
  • image: NotificationImage
  • popup_decision: PopupDecisionRecord
  • popup_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.

Attribution and interaction

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.

Images

NotificationImage contains:

  • badge_icon: string — daemon-owned application badge
  • claimed_theme_icon: string — bounded decorative theme lookup hint
  • claimed_desktop_id: string — sender-provided desktop-entry hint 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_image
  • sender_visual: ImageData
  • content_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.

Popup decisions

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.

Close and panel values

CloseReason values:

  • 1 expired
  • 2 dismissed by the user
  • 3 closed by a D-Bus call
  • 4 undefined

PanelAction: 0 open, 1 close, 2 toggle.

PanelDebugLevel: 0 off, 1 critical, 2 warn, 3 info, 4 verbose.

Compound types

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: current ControlState, active NotificationView[], and history NotificationView[] captured under one store lock.
  • UiHealth: center_process_running, center_ready, popups_process_running, popups_ready, and the readiness revision.
  • PopupCandidate: a NotificationView with 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: key and label; authorization policy is carried separately in the notification interaction fields.
  • InlineReply: available, label, placeholder, submit_label, and submit_icon.
  • InlineReplyPolicy: 0 allow, 1 confirm, 2 deny.
  • ImageData: width, height, row stride, alpha flag, bits per sample, channel count, and bounded pixel bytes.
  • PanelRequest: requested PanelAction and PanelDebugLevel.
  • PopupGateState: dnd_enabled and inhibited popup-gate flags.

Inhibitors and DND

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.

Introspection and client guidance

busctl --user introspect com.unixnotis.Control /com/unixnotis/Control

Subscribe 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.

Clone this wiki locally