an image

How do notifications work?

Years ago when I started using linux, I installed dunst so that applications could get my attention by showing a notification at the top right of my screen, like this:

screenshot

I learned to pause notifications so that they would not interrupt me when I was screensharing or streaming:

dunstctl set-paused true

Beyond that, I didn't know what it was or how it worked. It was just a thing that I started and sent to the background when before starting the X display server by putting this in my .xinitrc:

#.xinitrc
[[ -f ~/.Xresources ]] && xrdb -merge -I$HOME ~/.Xresources

dunst &
exec i3

Since I'm running systemd, I apparently could have just used "the provided systemd service":

Autostarting dunst

On most installations, dunst should be able to automatically be started by D-Bus when a notification is sent. This is not recommended when multiple notification deamons are installed, because D-Bus will not know which one to start. Other ways of autostarting dunst include starting dunst with your desktop environment or window manager’s autostart functionality or via the provided systemd service.

[From dunst(1)]

Either way, this is all I understood about dunst. It showed desktop notifications.

Eventually, I wanted to show some desktop notifications of my own. I created some i3 keyboard shortcuts for changing my volume, and used notify-send to show the volume whenever this happened:

#!/bin/bash
pactl set-sink-mute @DEFAULT_SINK@ 0;
pactl set-sink-volume @DEFAULT_SINK@ "+$1%"
notify-send -t 1000 -u normal -i dove "Volume: $(pulsemixer --get-volume | cut -d " " -f 1)"

The "dove" image is one that I added to my icon collection, because I thought it'd be nice to have a little birdy tell me what's going on, but I could use any theme from my current theme by name:

notify-send -t 3000 -u normal -i mark-location-symbolic "foo"

screenshot

Alternatively, I can specify a file by its full path

notify-send -t 3000 -u normal -i /home/john/.icons/dove.png "foo"

screenshot

Applications are free to send notifications with any image they want. Here's an notification generated by Discord:

screenshot

So what else is there?

Ok so I knew I could use this command line utility notify-send to make desktop notifications... But what is a desktop notification, anyway?

Well, as it turns out, there's something called the Desktop Notification Specification, a "standard for a desktop notifications service, through which applications can generate passive popups to notify the user in an asynchronous manner of events.".

The spec declares:

...all notifications are controlled by a single session-scoped service which exposes a D-BUS interface.

On startup, a conforming implementation should take the org.freedesktop.Notifications service on the session bus.

Ah, D-BUS... I've heard of this before! It's mentioned in the third sentence of the systemd homepage:

screenshot

systemd provides aggressive parallelization capabilities, uses socket and D-Bus activation for starting services, ...

The Autostarting dunst section above also mentioned that dunst itself could be automatically started by D-Bus.

D-Bus

I can see that the dunst package depends on dbus:

trizen -Qi dunst
Name            : dunst
Version         : 1.9.2-1
Description     : Customizable and lightweight notification-daemon
Architecture    : x86_64
URL             : https://dunst-project.org/
Licenses        : BSD
Groups          : None
Provides        : notification-daemon
Depends On      : libxinerama  dbus  systemd  wayland  libxss  pango  gdk-pixbuf2  libxrandr  glib2
Optional Deps   : libnotify: dunstify [installed]
Required By     : None
Optional For    : None
Conflicts With  : None
Replaces        : None
Installed Size  : 253.85 KiB
Packager        : Morten Linderud <foxboron@archlinux.org>
Build Date      : Thu 20 Apr 2023 05:59:41 PM EDT
Install Date    : Wed 28 Jun 2023 10:48:30 AM EDT
Install Reason  : Explicitly installed
Install Script  : No
Validated By    : Signature

But I want a shortcut explanation. Lately, when I want to learn about something conversationally, I'll ask chatGPT:

Does dunst use dbus? How do system notifications work?

And it's quick to reply:

Yes, Dunst, a highly customizable and lightweight notification daemon for Linux and BSD, uses D-Bus for system notifications.

Here is a simplified explanation of how system notifications work:

D-Bus (Desktop Bus) is a software bus, inter-process communication (IPC), and remote procedure call (RPC) mechanism that allows for communication between multiple processes running concurrently on the same machine.

Applications communicate with each other by sending messages over D-Bus. These messages can represent function calls or signals. Function calls are directed to a specific object, while signals are broadcast to all interested objects.

When an application wants to show a notification, it sends a D-Bus message to the org.freedesktop.Notifications service. This service is usually implemented by the desktop environment, which displays the notification in some way.

In a system where Dunst is installed, it acts as the implementation for the org.freedesktop.Notifications service. When it receives a D-Bus message requesting a notification, it displays the notification in a customizable and minimalistic way.

Dunst allows for a high degree of customization, including the position, color, and format of notifications. It also supports actions, allowing you to respond to notifications from the notification itself.

This is a simplified explanation. The exact details can get quite technical, but hopefully, this gives a good general idea.

Hmm, the exact details can get quite technical, eh? Ah what the heck, let's dig in a bit more.

After digging in a bit more, I learned that I can monitor the events on the bus with dbus-monitor.

If I monitor the system bus for some time, I'll see various network related messages:

sudo dbus-monitor --system   # Monitor the system bus

If I monitor the session bus, then I can see my own notifications in the log:

dbus-monitor --session

As it turns out, the notification is quite long:

method call time=1689302706.770395 sender=:1.217 -> destination=org.freedesktop.DBus serial=1 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=Hello
method return time=1689302706.770406 sender=org.freedesktop.DBus -> destination=:1.217 serial=1 reply_serial=1
   string ":1.217"
signal time=1689302706.770411 sender=org.freedesktop.DBus -> destination=(null destination) serial=8 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=NameOwnerChanged
   string ":1.217"
   string ""
   string ":1.217"
signal time=1689302706.770417 sender=org.freedesktop.DBus -> destination=:1.217 serial=2 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=NameAcquired
   string ":1.217"
method call time=1689302706.770542 sender=:1.217 -> destination=org.freedesktop.DBus serial=2 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=AddMatch
   string "type='signal',sender='org.freedesktop.Notifications',interface='org.freedesktop.Notifications',path='/org/freedesktop/Notifications'"
method return time=1689302706.770547 sender=org.freedesktop.DBus -> destination=:1.217 serial=3 reply_serial=2
method call time=1689302706.770556 sender=:1.217 -> destination=org.freedesktop.DBus serial=3 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=AddMatch
   string "type='signal',sender='org.freedesktop.DBus',interface='org.freedesktop.DBus',member='NameOwnerChanged',path='/org/freedesktop/DBus',arg0='org.freedesktop.Notifications'"
method return time=1689302706.770560 sender=org.freedesktop.DBus -> destination=:1.217 serial=4 reply_serial=3
method call time=1689302706.770580 sender=:1.217 -> destination=org.freedesktop.DBus serial=4 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=StartServiceByName
   string "org.freedesktop.Notifications"
   uint32 0
method return time=1689302706.770587 sender=org.freedesktop.DBus -> destination=:1.217 serial=5 reply_serial=4
   uint32 2
method call time=1689302706.770650 sender=:1.217 -> destination=org.freedesktop.DBus serial=5 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=GetNameOwner
   string "org.freedesktop.Notifications"
method return time=1689302706.770655 sender=org.freedesktop.DBus -> destination=:1.217 serial=6 reply_serial=5
   string ":1.205"
method call time=1689302706.770710 sender=:1.217 -> destination=:1.205 serial=6 path=/org/freedesktop/Notifications; interface=org.freedesktop.Notifications; member=GetServerInformation
method return time=1689302706.770869 sender=:1.205 -> destination=:1.217 serial=63 reply_serial=6
   string "dunst"
   string "knopwob"
   string "1.9.2 (2023-04-20)"
   string "1.2"
method call time=1689302706.770971 sender=:1.217 -> destination=:1.205 serial=7 path=/org/freedesktop/Notifications; interface=org.freedesktop.Notifications; member=GetServerInformation
method return time=1689302706.771040 sender=:1.205 -> destination=:1.217 serial=64 reply_serial=7
   string "dunst"
   string "knopwob"
   string "1.9.2 (2023-04-20)"
   string "1.2"
method call time=1689302706.771147 sender=:1.217 -> destination=:1.205 serial=8 path=/org/freedesktop/Notifications; interface=org.freedesktop.Notifications; member=Notify
   string "notify-send"
   uint32 0
   string "/home/john/.icons/dove.png"
   string "foo"
   string ""
   array [
   ]
   array [
      dict entry(
         string "urgency"
         variant             byte 1
      )
      dict entry(
         string "sender-pid"
         variant             int64 334958
      )
   ]
   int32 3000
method return time=1689302706.774155 sender=:1.205 -> destination=:1.217 serial=65 reply_serial=8
   uint32 12
signal time=1689302706.774214 sender=:1.205 -> destination=(null destination) serial=66 path=/org/freedesktop/Notifications; interface=org.freedesktop.DBus.Properties; member=PropertiesChanged
   string "org.dunstproject.cmd0"
   array [
      dict entry(
         string "displayedLength"
         variant             uint32 1
      )
   ]
   array [
   ]
signal time=1689302706.774282 sender=org.freedesktop.DBus -> destination=:1.217 serial=7 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=NameLost
   string ":1.217"
signal time=1689302706.774285 sender=org.freedesktop.DBus -> destination=(null destination) serial=9 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=NameOwnerChanged
   string ":1.217"
   string ":1.217"
   string ""
signal time=1689302

In this big mess I noticed two things:

  1. These strings that start with colons are called bus names in D-Bus parliance. They are unique names given to processes connected to the bus. (Notice that they name processes, not busses. These messages are being sent across the session bus.)

  2. That knopwob string is kind of strange looking. What is that? It turns out, it's the github username of the primary author of dunst! Thank you, Sascha Kruse (knopwob)!

What if I want to write an application that sends notifications?

My next thought after monitoring my notifications was -- how do I send them? Not through the command line, but in my programs.

As it turns out, there's a rust crate called zbus which "allows you to communicate from one program to another, using the D-Bus protocol."

Great! Let's write a program to communicate with org.freedesktop.Notifications:

main.rs:

use std::collections::HashMap;
use std::error::Error;

use zbus::{zvariant::Value, Connection};

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let connection = Connection::session().await?;

    let m = connection
        .call_method(
            Some("org.freedesktop.Notifications"),
            "/org/freedesktop/Notifications",
            Some("org.freedesktop.Notifications"),
            "Notify",
            &(
                "my-app",
                0u32,
                "dialog-information",
                "Hey, John",
                "This is a notification.",
                vec![""; 0],
                HashMap::<&str, &Value>::new(),
                3000,
            ),
        )
        .await?;
    let reply: u32 = m.body().unwrap();
    dbg!(reply);
    Ok(())
}

Cargo.toml:

[package]
name = "rust-notify"
version = "0.1.0"
edition = "2021"

[dependencies]
tokio = {version = "1.29", features = ["full"]}
zbus = { version = "3", default-features = false, features = ["tokio"] }

Sure enough, that works!

screenshot

What now?

Well, this exploration really gave me more questions than answers. D-Bus is more than just a way to talk to the notification service: It's a way to enable inter-process communication (IPC) in general! And apparently, there are D-Bus bindings in several languages (including Rust).

I'll want to dig into this further another day.

As a coincidence, I was just thinking about something else that crosses language boundaries: Platform ABI's! Rust 1.17.0 stable was just released and it included a new feature called C-unwind ABI.

In order to understand this change, I needed to take a step back and get a better understanding of ABIs and unwinding behavior in general, which I learned from this blog post (as well as a lengthy conversation with -- you guessed it -- ChatGPT.)

Anyway, I'll stop writing for now.

It's somewhat fun to record some of these explorations, though I doubt they're useful to anyone, including me. Oh well. Morning Pages.