tmisu

Notification to stdout daemon
git clone https://git.inz.fi/tmisu/
Log | Files | Refs | README | LICENSE

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:
Msrc/tmisu.c | 468++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/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"\