Skip to content
Browse the docs

notify

Desktop notifications over org.freedesktop.Notifications, sent the way KNotification sends them so Plasma groups them under the app and honours the user's per-event choice, with the notify feature.

The notify module is the one way a Telamon app sends a desktop notification. It needs the notify feature of telamon-framework-system. It sends KDE's hints (desktop-entry, x-kde-appname, x-kde-eventId), so Plasma groups the notifications under the app and honours the user's choice per event in System Settings.

Example

use telamon_framework_system::notify::{DEFAULT_ACTION, Note, Notifier, escape};

let app = telamon_framework_core::app_info! {
    name: "Telamon Updater",
    id: "net.eterneon.telamon.updater",
    repo: "atlasos-updater",
};
let notifier = Notifier::new(&app);
let mut note = Note::new("updateStaged", "Update ready", escape("44.20261001"));
note.actions.push((DEFAULT_ACTION, "Open Telamon Updater".into()));
notifier.send_blocking(&note)?; // from a worker thread, when nobody acts on it

In async code, use notifier.send(&conn, &note).await on a session-bus zbus::Connection.

The notifyrc file

The names the app gives KNotification come from its AppInfo: the component (the notifyrc's name) is AppInfo::short_name(), the desktop entry and the icon are the app ID, and the app name is the display name. Each app ships <short name>.notifyrc (telamon- and the last part of the app ID) in /usr/share/knotifications6/ with DesktopEntry=<app id> and one camelCase [Event/<eventId>] per kind of notification, whose Action= says Popup. The app template has one. Plasma writes the user's choice to ~/.config/<component>.notifyrc ([Event/<id>] Action=, a |-separated list; a choice made before 2.0.0, in ~/.config/atlas-<app>.notifyrc, counts for an event the new file has no entry for); without a user file, an [Event/<id>] group or an Action key there, the shipped file's ($XDG_DATA_DIRS/knotifications6/<component>.notifyrc, the first directory that has the key) applies, and with neither the event pops up. send reads these files on a blocking thread, not on the async worker.

Telamon OS rules for notifications

  • Notify only when the user can act on it, or must know: not for progress or success they did not wait for.
  • Popups only, no sounds. Leave urgency as None or Normal (High at most, never Critical); persistent only when ignoring the notification has consequences (a restart is due).
  • Actions are short verbs that open the right page ("Restart now", "Open Telamon Updater"), never "OK" or "Dismiss". DEFAULT_ACTION is the click on the notification itself.
  • Never from root to a user's session. A system service records the event, and the user-session app notices it and notifies.
  • Do Not Disturb is Plasma's: do not second-guess it.
  • Everything that came from outside (an app name, an error, a version) goes through escape before it goes into Note::text.

Errors and limits

  • Calls to the server give up after 10 seconds. The server may still show the notification after that, so do not retry blindly.
  • With no notification service (or no session bus), the error reads "No notification service is running".
  • send and close need a Tokio runtime with the time driver (#[tokio::main] and enable_all() have it); without one the timeout panics.
  • send_blocking runs on a thread of its own with a runtime of its own, so it is safe to call inside an existing Tokio runtime's spawn_blocking, but not from the UI thread or from async code.
  • An event ID that is not valid is refused with an error. An icon that is not valid is replaced by the app icon and logged.

Note

#[derive(Debug, Clone)] pub struct Note

Field Type Description
event &'static str The notifyrc event (updateStaged, restartSoon, ...): camelCase, letters and digits only
title String
text String Body markup: escape anything that came from outside
icon String Empty (the app's own icon), an icon name (letters, digits and ._+-) or an absolute path without ... Anything else is replaced by the app's icon
actions Vec<(&'static str, String)> (key, label) pairs. DEFAULT_ACTION is the click on the notification itself
urgency Option<Urgency> None leaves the server's default
persistent bool Stays until the user acts (no timeout)

Note::new(event: &'static str, title: impl Into<String>, text: impl Into<String>) -> Note is a plain notification: the app's icon, no actions, the server's default urgency and timeout.

#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Urgency { Normal, High, Critical }. High is KNotification's HighUrgency: the spec has no such level, so it goes out as normal. Critical goes out as level 2.

Notifier

#[derive(Debug, Clone, PartialEq, Eq)] pub struct Notifier

Method Description
fn new(app: &AppInfo) -> Notifier Takes the names from the app's AppInfo
fn component(&self) -> &str The notifyrc's name: the app's short_name()
fn desktop_entry(&self) -> &str The desktop file's name, which is the app ID
fn app_name(&self) -> &str The display name
fn app_icon(&self) -> &str The app's own icon: its ID
fn popup_enabled(&self, event: &str) -> bool Whether event pops up, from the user's notifyrc. An event ID that is not valid never pops up
async fn send(&self, conn: &zbus::Connection, n: &Note) -> zbus::Result<Option<Sent>> Sends n. Ok(None) means the user turned this event's popup off
fn send_blocking(&self, n: &Note) -> Result<(), String> send over a new session-bus connection, blocking, for a notification nobody acts on (its actions are not followed)

#[derive(Debug, Clone, PartialEq, Eq)] pub struct Sent { pub id: u32, pub server: Option<String> }: id is the server's notification ID, and server is the server's unique bus name (only its signals are about id).

Functions and constants

Name Signature or value Description
SERVICE, PATH, INTERFACE "org.freedesktop.Notifications", "/org/freedesktop/Notifications", "org.freedesktop.Notifications" The notification server's bus name, object path and interface
DEFAULT_ACTION "default" The key the server sends when the notification itself is clicked
escape pub fn escape(s: &str) -> String Escapes text for the body, which the server reads as markup: &, <, >, " and ' become entities, and control characters other than line feed and tab become spaces
event_id_ok pub fn event_id_ok(event: &str) -> bool 1 to 64 ASCII letters and digits, not starting with a digit
icon_ok pub fn icon_ok(icon: &str) -> bool Empty, an icon name, or an absolute path without .. and control characters. A URL or relative path is not
close pub async fn close(conn: &zbus::Connection, id: u32) -> zbus::Result<()> Closes notification id. Gives up after 10 seconds