dnd, a dbus notification daemon
I don't like notifications because they distract and disturb. When applied in small doses, though, they may be useful. Filtering out unwanted sources is another topic, but there also exists a question of delivering such notifications to your eyes. I wanted the delivery program to be minimal yet functional.
Preface
Once I started thinking "what do I want from a notification manager". At this time, I was using dunst. It's lightweight, configurable, and supports all of entities you may want: images, formatting via pango, quick actions, et al. Both disk space (400kb) and memory (11mb RSS) requirements are negotiable, but is this minimal enough?
So, what do I want? I want a notification daemon that can show text. No actions, no images, no formatting. Minimal dependencies for a minimal footprint. Dunst, unfortunately, depends on many things, and although you can remove external functionality like Wayland support, excessive for X11, other parts are tightly coupled.
The next candidate was tiramisu — a project written in Vala. Vala is a language that gets transpiled to C with a couple of useful features like better string support. It has native integration with DBus so target program can be written under 200 lines if you remove formatting and json output. The downside is the need to install Vala compiler. Writing a patch, and we got 4MB RSS. Cool!
What is DBus anyway, and why do we need it?
A notification daemon is essentially a window showing some text. However, users
need to know ways to propagate messages into this program. Multiple solutions
coexist, but a standard way to do it on Linux right now is to use DBus, a
message bus. Ideally you'd expect to open a socket and push some data there.
Unfortunately, DBus uses its own binary format so you'd need to use a client
library or user-facing utilities like dbus-send. The message bus operates on
topics call interfaces, and notifications use org/freedesktop/Notifications
topic. You can also ignore all DBus-related knowledge and use libnotify or
notify-send which then sends a message to DBus.
Writing own daemon
On the manager side, the goal is also easy. We need to create a DBus interface
with the notifications name and listen to incoming messages, showing them to
user. Developer library in Debian is libdbus-1-dev.
First we need to determine whether we'd use a system bus (for root) or a session bus (for current user). As we're not (hopefully) running root all the time, we'd go with a session bus.
#include <dbus/dbus.h>
DBusError err;
dbus_error_init(&err);
DBusConnection *const conn = dbus_bus_get(DBUS_BUS_SESSION, &err);
if (dbus_error_is_set(&err))
return fprintf(stderr, "connection error: %s\n", err.message), dbus_error_free(&err), 1;
if (!conn) return fprintf(stderr, "failed getting connection\n"), dbus_error_free(&err), 1;
Then we need to bind to a notification interface
#define IFACE "org.freedesktop.Notifications"
const int ret = dbus_bus_request_name(conn, IFACE, DBUS_NAME_FLAG_REPLACE_EXISTING, &err);
if (dbus_error_is_set(&err))
return fprintf(stderr, "name claim error: %s\n", err.message), dbus_error_free(&err),
dbus_connection_unref(conn), 1;
if (ret != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER)
return fprintf(stderr, "another notification daemon exists\n"), dbus_error_free(&err),
dbus_connection_unref(conn), 1;
If some notification daemon is already running, this request will succeed but tell us we're not the primary owner. Then we need to tell DBus how we will process incoming messages, and then set up an event loop
dbus_connection_add_filter(conn, filter, NULL, NULL);
while (dbus_connection_read_write_dispatch(conn, -1))
;
return dbus_error_free(&err), dbus_connection_unref(conn), 0;
In the filter function, we can either get a Notify message which we should
show the user, or system info calls like GetCapabilities or
GetServerInformation. Notifications clients query this info on startup to
understand what data type they can show. For example, if a messenger client
knows notification daemon supports quick actions, it can return a "reply" quick
action along with the message (Telegram does this, for example).
static DBusHandlerResult filter(DBusConnection *conn, DBusMessage *msg, void *user_data) {
if (dbus_message_is_method_call(msg, IFACE, "Notify")) notify(conn, msg);
else if (dbus_message_is_method_call(msg, IFACE, "GetCapabilities")) caps(conn, msg);
else if (dbus_message_is_method_call(msg, IFACE, "GetServerInformation")) info(conn, msg);
else if (dbus_message_is_method_call(msg, IFACE, "CloseNotification")) {
} else return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
return DBUS_HANDLER_RESULT_HANDLED;
}
We support plain text messages (body) only, and for that we need to form an
answer containing an array with a single supported capability. This is a chain
operation (open a container, append the capability, close the container), so
instead of checking the return code after every call, I call them with &&.
static void caps(DBusConnection *dbus, DBusMessage *msg) {
DBusMessage *reply = dbus_message_new_method_return(msg);
if (!reply) return;
DBusMessageIter args, subargs;
dbus_message_iter_init_append(reply, &args);
dbus_message_iter_open_container(&args, DBUS_TYPE_ARRAY, DBUS_TYPE_STRING_AS_STRING, &subargs) &&
dbus_message_iter_append_basic(&subargs, DBUS_TYPE_STRING, (const char *[]){"body"}) &&
dbus_message_iter_close_container(&args, &subargs) && dbus_connection_send(dbus, reply, NULL);
dbus_message_unref(reply);
}
Our info function tells the client basic daemon info like name
or version:
static void info(DBusConnection *dbus, DBusMessage *msg) {
DBusMessage *reply = dbus_message_new_method_return(msg);
if (!reply) return;
DBusMessageIter args;
dbus_message_iter_init_append(reply, &args);
const char *info[] = {"dnd", "dnd", "0.1", "1.0"};
dbus_message_iter_append_basic(&args, DBUS_TYPE_STRING, &info[0]) &&
dbus_message_iter_append_basic(&args, DBUS_TYPE_STRING, &info[1]) &&
dbus_message_iter_append_basic(&args, DBUS_TYPE_STRING, &info[2]) &&
dbus_message_iter_append_basic(&args, DBUS_TYPE_STRING, &info[3]) &&
dbus_connection_send(dbus, reply, NULL);
dbus_message_unref(reply);
}
Finally, the code that shows us some text. We read a message, ignore all fields but the summary and body, trim the message so it wouldn't take too much space, and show it to user. After that we need to send a reply confirming the message has been received with message id as an argument.
What's the best way to draw some text on the screen? For me it would be using
X11 API directly, but, fortunately, I already have a program
herbe which does that. We spawn a subprocess
and invoke herbe with messages joined and newlines replaced.
static dbus_uint32_t uid = 0;
static void notify(DBusConnection *dbus, DBusMessage *msg) {
DBusMessageIter args;
const char *summary, *body;
dbus_uint32_t id;
dbus_message_iter_init(msg, &args);
dbus_message_iter_next(&args); // appname
dbus_message_iter_get_basic(&args, &id);
dbus_message_iter_next(&args), dbus_message_iter_next(&args); // icon
dbus_message_iter_get_basic(&args, &summary);
dbus_message_iter_next(&args);
dbus_message_iter_get_basic(&args, &body);
if (!id) id = uid++;
char buf[102];
const int len = snprintf(buf, sizeof buf, "%.50s:%.50s", summary, body);
for (int i = 0; i < len; ++i)
if (buf[i] == '\n') buf[i] = ' ';
char *herbe_args[] = {"herbe", buf, NULL};
const int pid = fork();
if (pid < 0) fprintf(stderr, "fork failed\n"), exit(1);
if (!pid && execvp("herbe", herbe_args)) fprintf(stderr, "%s\n", strerror(errno)), exit(1);
DBusMessage *reply = dbus_message_new_method_return(msg);
dbus_message_iter_init_append(reply, &args);
dbus_message_iter_append_basic(&args, DBUS_TYPE_UINT32, &id) && dbus_connection_send(dbus, reply, NULL);
dbus_message_unref(reply);
}
One may argue herbe, a full-pledged notification daemon itself, is not the
most minimal choice. That's true! However, if you have manual notifications set
up somewhere else, these two stack nicely. You can use notify-send everywhere
else and replace herbe with manually opening an X11 window, I know a
good guide
for that.
As we're spawning child processes, we want to remove then when they exit.
For that we need to call signal(SIGCHLD, SIG_IGN) before registering our bus.
If we don't do that, exited subprocesses will stay as zombies.
When compiled with -O3 -flto=auto -s, the program uses 932kb RSS and takes
15kb disk space.
Do you need notifications?
Having used this daemon for a while, I've re-evaluated my need for
notifications. The only messenger I use (Pidgin) can invoke herbe
directly via a command-execute
plugin. I don't need notifications from a web browser — video conferencing web
sites are the only use case, and I open them only on purpose. Thus I don't
use dnd anymore, but this was an interesting thing to write.