commit 6f7afe3b93c96c82437f4aa3fedbf66ef3cac26b
parent 3eea16c68c32c074a33b7369e4c9fcd3a98f0f28
Author: Santtu Lakkala <inz@inz.fi>
Date: Fri, 25 Feb 2022 15:24:12 +0200
Major paradigm shift
Implement notification handling, by default reporting only the titles of
all currently open notifications, and allow closing notifications via
D-Bus or USR1/USR2 signals, as well as updating notifications.
Diffstat:
M | src/tmisu.c | | | 468 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- |
M | src/tmisu.h | | | 13 | ++++++++++++- |
2 files changed, 465 insertions(+), 16 deletions(-)
diff --git a/src/tmisu.c b/src/tmisu.c
@@ -1,10 +1,13 @@
+#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
-#include <stdbool.h>
#include <stdlib.h>
#include <getopt.h>
+#include <time.h>
+#include <sys/poll.h>
+
#include <dbus/dbus.h>
#include "tmisu.h"
@@ -15,12 +18,330 @@ struct conf {
const char *delimiter;
};
+struct notification {
+ struct notification *next;
+ DBusMessage *msg;
+ dbus_uint32_t id;
+ time_t expiry;
+ char *title;
+};
+
+#define MAX_WATCH 32
+#define N(x) (sizeof(x) / sizeof(*(x)))
+static struct pollfd pfdb[MAX_WATCH + 1] = { 0 };
+static struct pollfd *pfds = pfdb + 1;
+static DBusWatch *watches[MAX_WATCH] = { 0 };
+static size_t nw = 1;
+static const char *delimiter = ", ";
+int use_json = 0;
+int changed = 1;
+
+static time_t expirys[MAX_WATCH] = { 0 };
+static DBusTimeout *timeouts[MAX_WATCH] = { 0 };
+static size_t nt = 0;
+
+static short poll_flags(unsigned int dbf)
+{
+ return (dbf & DBUS_WATCH_READABLE ? POLLIN : 0) |
+ (dbf & DBUS_WATCH_WRITABLE ? POLLOUT : 0) |
+ (dbf & DBUS_WATCH_ERROR ? POLLERR : 0) |
+ (dbf & DBUS_WATCH_HANGUP ? POLLHUP : 0);
+}
+
+static unsigned int dw_flags(short pf)
+{
+ return (pf & POLLIN ? DBUS_WATCH_READABLE : 0) |
+ (pf & POLLOUT ? DBUS_WATCH_WRITABLE : 0) |
+ (pf & POLLERR ? DBUS_WATCH_ERROR : 0) |
+ (pf & POLLHUP ? DBUS_WATCH_HANGUP : 0);
+}
+
+static void toggle_watch(DBusWatch *watch, void *data)
+{
+ size_t i = (size_t)dbus_watch_get_data(watch);
+
+ (void)data;
+
+ if (dbus_watch_get_enabled(watch))
+ pfds[i].events = poll_flags(dbus_watch_get_flags(watch));
+ else
+ pfds[i].events = 0;
+}
+
+static void remove_watch(DBusWatch *watch, void *data)
+{
+ size_t i = (size_t)dbus_watch_get_data(watch);
+
+ (void)data;
+
+ memmove(&pfds[i], &pfds[i + 1], (nw - i - 1) * sizeof(*pfds));
+ memmove(&watches[i], &watches[i + 1], (nw - i - 1) * sizeof(*watches));
+
+ for (; i < nw - 1; i++)
+ dbus_watch_set_data(watches[i], (void *)i, NULL);
+ nw--;
+}
+
+static dbus_bool_t add_watch(DBusWatch *watch, void *data)
+{
+ (void)data;
+
+ if (nw == N(watches))
+ return FALSE;
+
+ watches[nw] = watch;
+ pfds[nw].fd = dbus_watch_get_unix_fd(watch);
+ dbus_watch_set_data(watch, (void *)nw++, NULL);
+
+ toggle_watch(watch, NULL);
+
+ return TRUE;
+}
+
+void toggle_timeout(DBusTimeout *timeout,
+ void *data)
+{
+ size_t i = (size_t)dbus_timeout_get_data(timeout);
+
+ (void)data;
+
+ if (dbus_timeout_get_enabled(timeout))
+ expirys[i] = time(NULL) +
+ (dbus_timeout_get_interval(timeout) + 999) / 1000;
+ else
+ expirys[i] = 0;
+}
+
+static dbus_bool_t add_timeout(DBusTimeout *timeout,
+ void *data)
+{
+ (void)data;
+
+ if (nt == N(timeouts))
+ return FALSE;
+
+ timeouts[nt] = timeout;
+ expirys[nt] = 0;
+ dbus_timeout_set_data(timeout, (void *)nt++, NULL);
+
+ toggle_timeout(timeout, NULL);
+
+ return TRUE;
+}
+
+void remove_timeout(DBusTimeout *timeout,
+ void *data)
+{
+ size_t i = (size_t)dbus_timeout_get_data(timeout);
+
+ (void)data;
+
+ memmove(&timeouts[i], &timeouts[i + 1], (nt - i - 1) * sizeof(*timeouts));
+ memmove(&expirys[i], &expirys[i + 1], (nt - i - 1) * sizeof(*expirys));
+
+ for (; i < nt - 1; i++)
+ dbus_timeout_set_data(timeouts[i], (void *)i, NULL);
+ nt--;
+}
+
+void trigger_timeouts(void)
+{
+ size_t i;
+ time_t now = time(NULL);
+
+ for (i = 0; i < nt; i++) {
+ DBusTimeout *to;
+ if (expirys[i] > now)
+ continue;
+ to = timeouts[i];
+ dbus_timeout_handle(to);
+ if (i < nt && timeouts[i] != to)
+ i--;
+ }
+}
+
+time_t timeout_next_expiry(void)
+{
+ time_t rv = 0;
+ size_t i;
+
+ for (i = 0; i < nt; i++)
+ if (expirys[i] && (!rv || expirys[i] < rv))
+ rv = expirys[i];
+
+ return rv;
+}
+
+static struct notification *notifications = NULL;
+static DBusConnection *connection = NULL;
+
+const struct notification *notif_update(DBusMessage *msg, dbus_uint32_t id, const char *title, time_t expiry)
+{
+ struct notification *i;
+
+ for (i = notifications; i && i->id != id; i = i->next);
+
+ if (!i)
+ return NULL;
+
+ if (!use_json)
+ changed |= !!strcmp(title, i->title);
+ free(i->title);
+ if (use_json)
+ dbus_message_unref(i->msg);
+ i->title = strdup(title);
+ i->expiry = expiry;
+ if (use_json)
+ i->msg = dbus_message_ref(msg);
+
+ return i;
+}
+
+const struct notification *notif_add(DBusMessage *msg, const char *title, time_t expiry)
+{
+ static dbus_uint32_t notification_id = 0;
+ struct notification *nn = malloc(sizeof(*nn));
+ struct notification *ne;
+
+ nn->id = ++notification_id;
+ if (use_json)
+ nn->msg = dbus_message_ref(msg);
+ nn->title = strdup(title);
+ nn->expiry = expiry;
+ nn->next = NULL;
+
+ if (notifications) {
+ for (ne = notifications; ne->next; ne = ne->next);
+ ne->next = nn;
+ } else {
+ notifications = nn;
+ }
+
+ changed = 1;
+
+ return nn;
+}
+
+enum reason {
+ EXPIRED = 1,
+ DISMISSED = 2,
+ REQUEST = 3,
+ UNKNOWN = 4
+};
+
+int notif_close(dbus_uint32_t id, dbus_uint32_t reason)
+{
+ struct notification *n;
+
+ if (!notifications)
+ return 0;
+ if (notifications->id == id) {
+ n = notifications;
+ notifications = n->next;
+ if (use_json)
+ dbus_message_unref(n->msg);
+ free(n->title);
+ free(n);
+ } else {
+ struct notification *i;
+ for (n = notifications;
+ n->next && n->next->id != id; n = n->next);
+ if (!n->next)
+ return 0;
+ i = n->next;
+ n->next = n->next->next;
+ if (use_json)
+ dbus_message_unref(i->msg);
+ free(i->title);
+ free(i);
+ }
+
+ DBusMessage *sig = dbus_message_new_signal("/",
+ "org.freedesktop.Notifications",
+ "NotificationClosed");
+ dbus_message_append_args(sig,
+ DBUS_TYPE_UINT32, &id,
+ DBUS_TYPE_UINT32, &reason,
+ DBUS_TYPE_INVALID);
+
+ dbus_connection_send(connection, sig, NULL);
+ dbus_message_unref(sig);
+
+ changed = 1;
+
+ return 1;
+}
+
+time_t notif_next_expiry(void)
+{
+ struct notification *i;
+ time_t rv = 0;
+
+ for (i = notifications; i; i = i->next) {
+ if (i->expiry &&
+ (!rv || i->expiry < rv))
+ rv = i->expiry;
+ }
+
+ return rv;
+}
+
+void notif_check_expiry(void)
+{
+ time_t now = time(NULL);
+ struct notification *i;
+ struct notification *n;
+
+ for (i = notifications; i; i = n) {
+ n = i->next;
+
+ if (i->expiry && i->expiry <= now)
+ notif_close(i->id, EXPIRED);
+ }
+}
+
+static void print_sanitized(const char *string, const char *escape) {
+ while (*string) {
+ size_t len = strcspn(string, escape);
+ printf("%.*s", (int)len, string);
+ string += len;
+ while (*string && strchr(escape, *string)) {
+ if (*string == '\n')
+ printf("\\n");
+ else
+ printf("\\%c", *string);
+ string++;
+ }
+ }
+}
+
+void notif_dump(void)
+{
+ const char *sep = "";
+ struct notification *i;
+
+ if (use_json)
+ printf("[");
+ for (i = notifications; i; i = i->next) {
+ printf("%s", sep);
+ if (use_json)
+ output_notification(i->msg, i->id, FORMAT_JSON, "");
+ else
+ print_sanitized(i->title, "\\\n");
+ sep = delimiter;
+ }
+ if (use_json)
+ printf("]");
+ puts("");
+ fflush(stdout);
+}
+
DBusHandlerResult handle_message(DBusConnection *connection, DBusMessage *message, void *user_data)
{
- static unsigned notification_id = 0;
- struct conf *cnf = user_data;
DBusMessage *reply;
+ (void)user_data;
+
if (dbus_message_is_method_call(message, "org.freedesktop.DBus.Introspectable", "Introspect")) {
static const char *notificationpath = "/org/freedesktop/Notifications";
const char *path = dbus_message_get_path(message);
@@ -67,13 +388,59 @@ DBusHandlerResult handle_message(DBusConnection *connection, DBusMessage *messag
dbus_message_iter_append_basic(&j, DBUS_TYPE_STRING, &(const char *){ "body-markup" });
dbus_message_iter_close_container(&i, &j);
} else if (dbus_message_is_method_call(message, "org.freedesktop.Notifications", "Notify")) {
+ const struct notification *n = NULL;
+ struct notification_data d;
+ DBusMessageIter iter;
+ time_t expiry;
+
if (!dbus_message_has_signature(message, "susssasa{sv}i"))
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+
+ dbus_message_iter_init(message, &iter);
+ dbus_message_iter_get_basic(&iter, &d.app_name);
+ dbus_message_iter_next(&iter);
+ dbus_message_iter_get_basic(&iter, &d.replaces);
+ dbus_message_iter_next(&iter);
+ dbus_message_iter_get_basic(&iter, &d.icon);
+ dbus_message_iter_next(&iter);
+ dbus_message_iter_get_basic(&iter, &d.summary);
+ dbus_message_iter_next(&iter);
+ dbus_message_iter_get_basic(&iter, &d.body);
+ dbus_message_iter_next(&iter);
+ dbus_message_iter_recurse(&iter, &d.actions);
+ dbus_message_iter_next(&iter);
+ dbus_message_iter_recurse(&iter, &d.hints);
+ dbus_message_iter_next(&iter);
+ dbus_message_iter_get_basic(&iter, &d.expiry_ms);
+
+ if (d.expiry_ms < 0)
+ d.expiry_ms = 10000;
+ if (d.expiry_ms)
+ expiry = time(NULL) + (d.expiry_ms + 999) / 1000;
+ else
+ expiry = 0;
+
+ if (d.replaces)
+ n = notif_update(message, d.replaces, d.summary, expiry);
+ if (!n)
+ n = notif_add(message, d.summary, expiry);
+
reply = dbus_message_new_method_return(message);
- output_notification(message, ++notification_id, cnf->fmt, cnf->delimiter);
+ // output_notification(message, n->id, cnf->fmt, cnf->delimiter);
dbus_message_append_args(reply,
- DBUS_TYPE_UINT32, &(dbus_uint32_t){ notification_id },
+ DBUS_TYPE_UINT32, &n->id,
DBUS_TYPE_INVALID);
+ } else if (dbus_message_is_method_call(message, "org.freedesktop.Notifications", "CloseNotification")) {
+ if (!dbus_message_has_signature(message, "u"))
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ dbus_uint32_t id;
+ dbus_message_get_args(message, NULL,
+ DBUS_TYPE_UINT32, &id,
+ DBUS_TYPE_INVALID);
+ if (notif_close(id, REQUEST))
+ reply = dbus_message_new_method_return(message);
+ else
+ reply = dbus_message_new_error(message, DBUS_ERROR_INVALID_ARGS, "Notification not found");
} else {
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
@@ -84,23 +451,21 @@ DBusHandlerResult handle_message(DBusConnection *connection, DBusMessage *messag
return DBUS_HANDLER_RESULT_HANDLED;
}
-static DBusConnection *connection = NULL;
+int sfd[2];
void sig_handler(int signal)
{
- (void)signal;
- dbus_connection_close(connection);
+ if (write(sfd[1], &(char){ signal }, 1) != 1)
+ exit(1);
}
int main(int argc, char **argv) {
- /* Parse arguments */
- struct conf cnf = { FORMAT_TEXT, "\n" };
char argument;
while ((argument = getopt(argc, argv, "hjd:")) >= 0) {
switch (argument) {
case 'd':
- cnf.delimiter = optarg;
+ delimiter = optarg;
break;
case 'h':
printf("%s\n",
@@ -111,15 +476,30 @@ int main(int argc, char **argv) {
return EXIT_SUCCESS;
break;
case 'j':
- cnf.fmt = FORMAT_JSON;
+ use_json = 1;
break;
default:
break;
}
}
+ if (pipe(sfd))
+ return 1;
+
connection = dbus_bus_get_private(DBUS_BUS_SESSION, NULL);
+ dbus_connection_set_watch_functions(connection,
+ add_watch,
+ remove_watch,
+ toggle_watch,
+ NULL, NULL);
+ dbus_connection_set_timeout_functions(connection,
+ add_timeout,
+ remove_timeout,
+ toggle_timeout,
+ NULL, NULL);
+
+
if (!connection) {
fprintf(stderr, "Could not connect to D-Bus\n");
return 1;
@@ -137,17 +517,75 @@ int main(int argc, char **argv) {
dbus_bus_add_match(connection, "interface=org.freedesktop.Notifications,path=/org/freedesktop/Notifications,type=method_call", NULL);
dbus_bus_add_match(connection, "interface=org.freedesktop.DBus.Introspectable,method=Introspect,type=method_call", NULL);
- dbus_connection_add_filter(connection, handle_message, &cnf, NULL);
+ dbus_connection_add_filter(connection, handle_message, NULL, NULL);
signal(SIGINT, sig_handler);
signal(SIGTERM, sig_handler);
+ signal(SIGUSR1, sig_handler);
+ signal(SIGUSR2, sig_handler);
+
+ pfdb[0].fd = sfd[0];
+ pfdb[0].events = POLLIN;
+
+ for (;;) {
+ size_t i;
+ time_t nexp;
+ time_t texp;
+ int interval;
+
+ notif_check_expiry();
+ trigger_timeouts();
+
+ nexp = notif_next_expiry();
+ texp = timeout_next_expiry();
+
+ if (changed)
+ notif_dump();
+ changed = 0;
- while (dbus_connection_read_write_dispatch(connection, -1));
+ if (nexp && (!texp || nexp <= texp))
+ interval = (nexp - time(NULL)) * 1000;
+ else if (texp && (!nexp || texp < nexp))
+ interval = (texp - time(NULL)) * 1000;
+ else
+ interval = -1;
+
+ int r = poll(pfdb, nw + 1, interval);
+
+ if (pfdb[0].revents) {
+ char s;
+ if (read(pfdb[0].fd, &s, 1) != 1)
+ break;
+
+ if (s == SIGINT || s == SIGTERM)
+ break;
+ if (s == SIGUSR1) {
+ if (notifications)
+ notif_close(notifications->id, DISMISSED);
+ } else if (s == SIGUSR2) {
+ while (notifications)
+ notif_close(notifications->id, DISMISSED);
+ }
+ signal(s, sig_handler);
+ }
+ for (i = 0; i < nw && r; i++) {
+ if (!pfds[i].revents)
+ continue;
+ dbus_watch_handle(watches[i],
+ dw_flags(pfds[i].revents));
+ toggle_watch(watches[i], NULL);
+ r--;
+ }
+
+ while (dbus_connection_get_dispatch_status(connection) == DBUS_DISPATCH_DATA_REMAINS)
+ dbus_connection_dispatch(connection);
+ }
dbus_bus_release_name(connection, "org.freedesktop.Notifications", NULL);
- dbus_connection_remove_filter(connection, handle_message, &cnf);
+ dbus_connection_remove_filter(connection, handle_message, NULL);
dbus_bus_remove_match(connection, "interface=org.freedesktop.Notifications,path=/org/freedesktop/Notifications,type=method_call", NULL);
dbus_bus_remove_match(connection, "interface=org.freedesktop.DBus.Introspectable,method=Introspect,type=method_call", NULL);
+ dbus_connection_close(connection);
dbus_connection_unref(connection);
return 0;
diff --git a/src/tmisu.h b/src/tmisu.h
@@ -3,9 +3,20 @@
#include <stdio.h>
#include <string.h>
+#include <dbus/dbus.h>
extern char print_json;
-extern const char *delimiter;
+
+struct notification_data {
+ const char *app_name;
+ dbus_uint32_t replaces;
+ const char *icon;
+ const char *summary;
+ const char *body;
+ DBusMessageIter actions;
+ DBusMessageIter hints;
+ dbus_int32_t expiry_ms;
+};
#define INTROSPECTION_XML "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"\
"<node>\n"\