From 8275f6e90d60daa31c5641ab695903c842b117fc Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Mon, 6 Apr 2026 12:44:16 -0400
Subject: [PATCH] Add D-Bus notification driver
Use the core and portal notification implementations to dispatch system notifications.
---
src/notification/unix/SDL_dbusnotification.c | 1581 ++++++++++++++++++
src/video/wayland/SDL_waylandutil.c | 5 +
src/video/wayland/SDL_waylandwindow.c | 24 +
test/testnotification.c | 1 -
4 files changed, 1610 insertions(+), 1 deletion(-)
create mode 100644 src/notification/unix/SDL_dbusnotification.c
diff --git a/src/notification/unix/SDL_dbusnotification.c b/src/notification/unix/SDL_dbusnotification.c
new file mode 100644
index 0000000000000..18a7c175d7b75
--- /dev/null
+++ b/src/notification/unix/SDL_dbusnotification.c
@@ -0,0 +1,1581 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+#include "../../core/linux/SDL_dbus.h"
+#include "../../core/unix/SDL_appid.h"
+#include "../../events/SDL_notificationevents_c.h"
+#include "../../io/SDL_iostream_c.h"
+#include "../../video/SDL_surface_c.h"
+#include <SDL3/SDL_iostream.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <signal.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#ifdef HAVE_MEMFD_CREATE
+#include <sys/mman.h>
+#endif
+
+#define NOTIFICATION_PORTAL_NODE "org.freedesktop.portal.Desktop"
+#define NOTIFICATION_PORTAL_PATH "/org/freedesktop/portal/desktop"
+#define NOTIFICATION_PORTAL_INTERFACE "org.freedesktop.portal.Notification"
+
+#define NOTIFICATION_CORE_NODE "org.freedesktop.Notifications"
+#define NOTIFICATION_CORE_PATH "/org/freedesktop/Notifications"
+#define NOTIFICATION_CORE_INTERFACE "org.freedesktop.Notifications"
+
+#define NOTIFICATION_ACTION_SIGNAL_NAME "ActionInvoked"
+#define NOTIFICATION_CLOSED_SIGNAL_NAME "NotificationClosed"
+#define NOTIFICATION_ACTIVATION_TOKEN_SIGNAL_NAME "ActivationToken"
+
+#define SDL_NOTIFICATION_PREAMBLE "SDL_LocalNotification-"
+
+#define ACTIVATION_TOKEN_LIFETIME SDL_SECONDS_TO_NS(1)
+
+static Uint64 activation_token_time_ns;
+static char *activation_token;
+
+static char *icon_uri;
+static Uint64 session_id;
+
+static Uint32 core_id_list[32];
+static Uint32 core_id_count;
+
+static Uint32 interface_version;
+
+static bool core_interface_initialized;
+static bool portal_interface_initialized;
+
+static void GetRandom(void *dst, size_t size)
+{
+ int fd = open("/dev/urandom", O_RDONLY);
+ if (fd < 0) {
+ fd = open("/dev/random", O_RDONLY);
+ }
+ if (fd >= 0) {
+ while (read(fd, dst, size) != size) {
+ }
+ close(fd);
+ } else {
+ size_t written = 0;
+
+ while (written < size) {
+ const Uint64 tval = SDL_GetTicksNS();
+ const size_t towrite = SDL_min(size - written, sizeof(tval));
+ SDL_memcpy((Uint8 *)dst + written, &tval, towrite);
+ written += towrite;
+ }
+ }
+}
+
+static bool AppendOption(SDL_DBusContext *dbus, DBusMessageIter *options, const char *type, const char *key, const void *value)
+{
+ DBusMessageIter options_pair, options_value;
+
+ if (!dbus->message_iter_open_container(options, DBUS_TYPE_DICT_ENTRY, NULL, &options_pair)) {
+ return false;
+ }
+ if (!dbus->message_iter_append_basic(&options_pair, DBUS_TYPE_STRING, &key)) {
+ return false;
+ }
+ if (!dbus->message_iter_open_container(&options_pair, DBUS_TYPE_VARIANT, type, &options_value)) {
+ return false;
+ }
+ if (!dbus->message_iter_append_basic(&options_value, (int)type[0], value)) {
+ return false;
+ }
+ if (!dbus->message_iter_close_container(&options_pair, &options_value)) {
+ return false;
+ }
+ return (bool)dbus->message_iter_close_container(options, &options_pair);
+}
+
+static bool AppendStringOption(SDL_DBusContext *dbus, DBusMessageIter *options, const char *key, const char *value)
+{
+ DBusMessageIter options_pair, options_value;
+
+ if (!dbus->message_iter_open_container(options, DBUS_TYPE_DICT_ENTRY, NULL, &options_pair)) {
+ return false;
+ }
+ if (!dbus->message_iter_append_basic(&options_pair, DBUS_TYPE_STRING, &key)) {
+ return false;
+ }
+ if (!dbus->message_iter_open_container(&options_pair, DBUS_TYPE_VARIANT, DBUS_TYPE_STRING_AS_STRING, &options_value)) {
+ return false;
+ }
+ if (!dbus->message_iter_append_basic(&options_value, DBUS_TYPE_STRING, &value)) {
+ return false;
+ }
+ if (!dbus->message_iter_close_container(&options_pair, &options_value)) {
+ return false;
+ }
+ return (bool)dbus->message_iter_close_container(options, &options_pair);
+}
+
+static bool AppendTargetString(SDL_DBusContext *dbus, DBusMessageIter *options, const char *key)
+{
+ char target_buf[128];
+ const char *target_val = target_buf;
+ DBusMessageIter options_pair, target_variant, target_string;
+
+ SDL_snprintf(target_buf, sizeof(target_buf), "%" SDL_PRIu64, session_id);
+
+ if (!dbus->message_iter_open_container(options, DBUS_TYPE_DICT_ENTRY, NULL, &options_pair)) {
+ return false;
+ }
+ if (!dbus->message_iter_append_basic(&options_pair, DBUS_TYPE_STRING, &key)) {
+ return false;
+ }
+ if (!dbus->message_iter_open_container(&options_pair, DBUS_TYPE_VARIANT, DBUS_TYPE_VARIANT_AS_STRING, &target_variant)) {
+ return false;
+ }
+ if (!dbus->message_iter_open_container(&target_variant, DBUS_TYPE_VARIANT, DBUS_TYPE_STRING_AS_STRING, &target_string)) {
+ return false;
+ }
+ if (!dbus->message_iter_append_basic(&target_string, DBUS_TYPE_STRING, &target_val)) {
+ return false;
+ }
+ if (!dbus->message_iter_close_container(&target_variant, &target_string)) {
+ return false;
+ }
+ if (!dbus->message_iter_close_container(&options_pair, &target_variant)) {
+ return false;
+ }
+ return (bool)dbus->message_iter_close_container(options, &options_pair);
+}
+
+static void RemoveIDFromListAtIndex(Uint32 index)
+{
+ --core_id_count;
+ if (index < core_id_count) {
+ SDL_memmove(&core_id_list[index], &core_id_list[index + 1], (core_id_count - index) * sizeof(Uint32));
+ }
+}
+
+static bool HasDesktopFile(const char *app_id)
+{
+ char path[PATH_MAX];
+ struct stat st;
+
+ if (!app_id) {
+ return NULL;
+ }
+
+ const char *xdg_data_home = SDL_getenv("XDG_DATA_HOME");
+ if (xdg_data_home) {
+ SDL_snprintf(path, SDL_arraysize(path), "%s/applications/%s.desktop", xdg_data_home, app_id);
+ if (stat(path, &st) == 0) {
+ return true;
+ }
+ } else {
+ xdg_data_home = SDL_getenv("HOME");
+ if (xdg_data_home) {
+ SDL_snprintf(path, SDL_arraysize(path), "%s/.local/share/applications/%s.desktop", xdg_data_home, app_id);
+ if (stat(path, &st) == 0) {
+ return true;
+ }
+ }
+ }
+
+ if (xdg_data_home) {
+ SDL_snprintf(path, SDL_arraysize(path), "%s/.local/share/applications/%s.desktop", xdg_data_home, app_id);
+ if (stat(path, &st) == 0) {
+ return true;
+ }
+ }
+
+ SDL_snprintf(path, SDL_arraysize(path), "/usr/share/local/applications/%s.desktop", app_id);
+ if (stat(path, &st) == 0) {
+ return true;
+ }
+
+ SDL_snprintf(path, SDL_arraysize(path), "/usr/share/applications/%s.desktop", app_id);
+ if (stat(path, &st) == 0) {
+ return true;
+ }
+
+ return false;
+}
+
+// org.freedesktop.Notifications, used when not running inside a container.
+static DBusHandlerResult CoreNotificationFilter(DBusConnection *conn, DBusMessage *msg, void *data)
+{
+ SDL_DBusContext *dbus = SDL_DBus_GetContext();
+
+ if (dbus->message_is_signal(msg, NOTIFICATION_CORE_NODE, NOTIFICATION_ACTION_SIGNAL_NAME)) {
+ DBusMessageIter signal_iter; // variant_iter;
+ const char *button = NULL;
+ Uint32 id = 0;
+ bool own_id = false;
+
+ if (!dbus->message_iter_init(msg, &signal_iter)) {
+ goto not_our_signal;
+ }
+
+ // Check if the parameters are what we expect
+ if (dbus->message_iter_get_arg_type(&signal_iter) != DBUS_TYPE_UINT32) {
+ goto not_our_signal;
+ }
+ dbus->message_iter_get_basic(&signal_iter, &id);
+
+ // See if this signal is for this client.
+ for (Uint32 i = 0; i < core_id_count; ++i) {
+ if (id == core_id_list[i]) {
+ RemoveIDFromListAtIndex(i);
+ own_id = true;
+ break;
+ }
+ }
+ if (!own_id) {
+ goto not_our_signal;
+ }
+
+ if (!dbus->message_iter_next(&signal_iter)) {
+ goto not_our_signal;
+ }
+ if (dbus->message_iter_get_arg_type(&signal_iter) != DBUS_TYPE_STRING) {
+ goto not_our_signal;
+ }
+ dbus->message_iter_get_basic(&signal_iter, &button);
+ if (button) {
+ SDL_SendNotificationAction(id, button);
+ }
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+ } else if (dbus->message_is_signal(msg, NOTIFICATION_CORE_NODE, NOTIFICATION_CLOSED_SIGNAL_NAME)) {
+ DBusMessageIter signal_iter; // variant_iter;
+ Uint32 id = 0, reason = 0;
+
+ dbus->message_iter_init(msg, &signal_iter);
+ // Check if the parameters are what we expect
+ if (dbus->message_iter_get_arg_type(&signal_iter) != DBUS_TYPE_UINT32) {
+ goto not_our_signal;
+ }
+ dbus->message_iter_get_basic(&signal_iter, &id);
+
+ if (!dbus->message_iter_next(&signal_iter)) {
+ goto not_our_signal;
+ }
+ if (dbus->message_iter_get_arg_type(&signal_iter) != DBUS_TYPE_UINT32) {
+ goto not_our_signal;
+ }
+ dbus->message_iter_get_basic(&signal_iter, &reason);
+ if (id && reason) {
+ for (Uint32 i = 0; i < core_id_count; ++i) {
+ if (core_id_list[i] == id) {
+ RemoveIDFromListAtIndex(i);
+ return DBUS_HANDLER_RESULT_HANDLED;
+ }
+ }
+ }
+ } else if (dbus->message_is_signal(msg, NOTIFICATION_CORE_NODE, NOTIFICATION_ACTIVATION_TOKEN_SIGNAL_NAME)) {
+ DBusMessageIter signal_iter; // variant_iter;
+ const char *token = NULL;
+ Uint32 id = 0;
+
+ dbus->message_iter_init(msg, &signal_iter);
+ // Check if the parameters are what we expect
+ if (dbus->message_iter_get_arg_type(&signal_iter) != DBUS_TYPE_UINT32) {
+ goto not_our_signal;
+ }
+ dbus->message_iter_get_basic(&signal_iter, &id);
+
+ if (!dbus->message_iter_next(&signal_iter)) {
+ goto not_our_signal;
+ }
+ if (dbus->message_iter_get_arg_type(&signal_iter) != DBUS_TYPE_STRING) {
+ goto not_our_signal;
+ }
+ dbus->message_iter_get_basic(&signal_iter, &token);
+ if (id && token) {
+ for (Uint32 i = 0; i < core_id_count; ++i) {
+ if (core_id_list[i] == id) {
+ SDL_free(activation_token);
+ activation_token = SDL_strdup(token);
+ activation_token_time_ns = SDL_GetTicksNS();
+ return DBUS_HANDLER_RESULT_HANDLED;
+ }
+ }
+ }
+ }
+
+not_our_signal:
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+}
+
+static bool SetCoreImage(SDL_DBusContext *dbus, DBusMessageIter *iterInit, SDL_Surface *surface)
+{
+ DBusMessageIter iterEntry, iterValue;
+ DBusMessageIter iter, array;
+ const dbus_bool_t alpha = true;
+ Sint32 bpp = 8;
+
+ if (!dbus->message_iter_open_container(iterInit, DBUS_TYPE_DICT_ENTRY, NULL, &iterEntry)) {
+ return false;
+ }
+
+ const char *key = "image-data";
+ if (!dbus->message_iter_append_basic(&iterEntry, DBUS_TYPE_STRING, &key)) {
+ return false;
+ }
+
+ if (!dbus->message_iter_open_container(&iterEntry, DBUS_TYPE_VARIANT, "(iiibiiay)", &iterValue)) {
+ return false;
+ }
+
+ if (!dbus->message_iter_open_container(&iterValue, DBUS_TYPE_STRUCT, NULL, &iter)) {
+ return false;
+ }
+
+ // Width
+ if (!dbus->message_iter_append_basic(&iter, DBUS_TYPE_INT32, &surface->w)) {
+ return false;
+ }
+
+ // Height
+ if (!dbus->message_iter_append_basic(&iter, DBUS_TYPE_INT32, &surface->h)) {
+ return false;
+ }
+
+ // Pitch
+ if (!dbus->message_iter_append_basic(&iter, DBUS_TYPE_INT32, &surface->pitch)) {
+ return false;
+ }
+
+ // Alpha yes/no
+ if (!dbus->message_iter_append_basic(&iter, DBUS_TYPE_BOOLEAN, &alpha)) {
+ return false;
+ }
+
+ // BPP
+ if (!dbus->message_iter_append_basic(&iter, DBUS_TYPE_INT32, &bpp)) {
+ return false;
+ }
+
+ // Channels (always 4 with alpha)
+ bpp = 4;
+ if (!dbus->message_iter_append_basic(&iter, DBUS_TYPE_INT32, &bpp)) {
+ return false;
+ }
+
+ // Raw image bytes
+ if (!dbus->message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "y", &array)) {
+ return false;
+ }
+
+ const Uint8 *pixels = surface->pixels;
+ for (int i = 0; i < surface->pitch * surface->h; i++) {
+ if (!dbus->message_iter_append_basic(&array, DBUS_TYPE_BYTE, &pixels[i])) {
+ return false;
+ }
+ }
+
+ if (!dbus->message_iter_close_container(&iter, &array)) {
+ return false;
+ }
+ if (!dbus->message_iter_close_container(&iterValue, &iter)) {
+ return false;
+ }
+ if (!dbus->message_iter_close_container(&iterEntry, &iterValue)) {
+ return false;
+ }
+ if (!dbus->message_iter_close_container(iterInit, &iterEntry)) {
+ return false;
+ }
+
+ return true;
+}
+
+static bool SetCoreHints(SDL_DBusContext *dbus, DBusMessageIter *iterInit, SDL_PropertiesID props)
+{
+ DBusMessageIter iterDict;
+ const char *app_id = SDL_GetAppMetadataProperty(SDL_PROP_APP_METADATA_IDENTIFIER_STRING);
+ const char *sound = SDL_GetStringProperty(props, SDL_PROP_NOTIFICATION_SOUND_STRING, "default");
+ SDL_Surface *image = SDL_GetPointerProperty(props, SDL_PROP_NOTIFICATION_IMAGE_POINTER, NULL);
+ SDL_NotificationPriority priority = SDL_GetNumberProperty(props, SDL_PROP_NOTIFICATION_PRIORITY_NUMBER, SDL_NOTIFICATION_PRIORITY_NORMAL);
+ const bool transient = SDL_GetBooleanProperty(props, SDL_PROP_NOTIFICATION_TRANSIENT_BOOLEAN, false);
+
+ if (!dbus->message_iter_open_container(iterInit, DBUS_TYPE_ARRAY, "{sv}", &iterDict)) {
+ return false;
+ }
+
+ Uint8 dbus_priority;
+
+ switch (priority) {
+ case SDL_NOTIFICATION_PRIORITY_NORMAL:
+ case SDL_NOTIFICATION_PRIORITY_HIGH:
+ default:
+ dbus_priority = 1;
+ break;
+ case SDL_NOTIFICATION_PRIORITY_LOW:
+ dbus_priority = 0;
+ break;
+ case SDL_NOTIFICATION_PRIORITY_CRITICAL:
+ dbus_priority = 2;
+ break;
+ }
+
+ if (!AppendOption(dbus, &iterDict, DBUS_TYPE_BYTE_AS_STRING, "urgency", &dbus_priority)) {
+ return false;
+ }
+
+ if (app_id) {
+ if (HasDesktopFile(app_id)) {
+ if (!AppendOption(dbus, &iterDict, DBUS_TYPE_STRING_AS_STRING, "desktop-entry", &app_id)) {
+ return false;
+ }
+ }
+ }
+
+ const dbus_bool_t db_transient = transient;
+ if (!AppendOption(dbus, &iterDict, DBUS_TYPE_BOOLEAN_AS_STRING, "transient", &db_transient)) {
+ return false;
+ }
+
+ if (SDL_strcmp(sound, "default") == 0) {
+ if (!AppendStringOption(dbus, &iterDict, "sound-name", "dialog-information")) {
+ return false;
+ }
+ } else if (SDL_strcmp(sound, "silent") != 0) {
+ char sound_path[PATH_MAX];
+ if (realpath(sound, sound_path)) {
+ if (!AppendStringOption(dbus, &iterDict, "sound-file", sound_path)) {
+ return false;
+ }
+ } else {
+ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Notification sound '%s' not found", sound);
+
+ // Play the default if the custom sound is not found.
+ if (!AppendStringOption(dbus, &iterDict, "sound-name", "dialog-information")) {
+ return false;
+ }
+ }
+ }
+
+ if (image) {
+ SDL_Surface *image_surface = image;
+ if (image->format != SDL_PIXELFORMAT_ABGR8888) {
+ image_surface = SDL_ConvertSurface(image, SDL_PIXELFORMAT_ABGR8888);
+ }
+
+ SetCoreImage(dbus, &iterDict, image_surface);
+
+ if (image_surface != image) {
+ SDL_DestroySurface(image_surface);
+ }
+ }
+
+ if (!dbus->message_iter_close_container(iterInit, &iterDict)) {
+ return false;
+ }
+
+ return true;
+}
+
+static bool InitCoreSignalListener(SDL_DBusContext *dbus)
+{
+ static bool interface_unavailable = false;
+ if (interface_unavailable) {
+ return false;
+ }
+
+ // Query the server information to see if the notification interface is available.
+ DBusMessage *msg = dbus->message_new_method_call(NOTIFICATION_CORE_NODE, NOTIFICATION_CORE_PATH, NOTIFICATION_CORE_INTERFACE, "GetServerInformation");
+ if (msg == NULL) {
+ return false;
+ }
+ DBusMessage *reply = dbus->connection_send_with_reply_and_block(dbus->session_conn, msg, -1, NULL);
+ dbus->message_unref(msg);
+ if (!reply) {
+ // Mark the interface as unavailable.
+ interface_unavailable = true;
+ return false;
+ }
+ dbus->message_unref(reply);
+
+ if (!session_id) {
+ GetRandom(&session_id, sizeof(session_id));
+ }
+
+ DBusError error;
+ dbus->error_init(&error);
+
+ dbus->bus_add_match(dbus->session_conn,
+ "type='signal', interface='" NOTIFICATION_CORE_INTERFACE "',"
+ "member='" NOTIFICATION_ACTION_SIGNAL_NAME "'",
+ &error);
+ if (dbus->error_is_set(&error)) {
+ SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Failed to register DBus notification filter: %s", error.message);
+ dbus->error_free(&error);
+ return false;
+ }
+
+ dbus->bus_add_match(dbus->session_conn,
+ "type='signal', interface='" NOTIFICATION_CORE_INTERFACE "',"
+ "member='" NOTIFICATION_CLOSED_SIGNAL_NAME "'",
+ &error);
+ if (dbus->error_is_set(&error)) {
+ SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Failed to register DBus notification filter: %s", error.message);
+ dbus->error_free(&error);
+ goto unregister_action;
+ }
+
+ dbus->bus_add_match(dbus->session_conn,
+ "type='signal', interface='" NOTIFICATION_CORE_INTERFACE "',"
+ "member='" NOTIFICATION_ACTIVATION_TOKEN_SIGNAL_NAME "'",
+ &error);
+ if (dbus->error_is_set(&error)) {
+ SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Failed to register DBus notification filter: %s", error.message);
+ dbus->error_free(&error);
+ goto unregister_closed;
+ }
+ dbus->error_free(&error);
+
+ if (dbus->connection_add_filter(dbus->session_conn, &CoreNotificationFilter, NULL, NULL)) {
+ dbus->connection_flush(dbus->session_conn);
+ SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Registered DBus portal notification filter");
+ } else {
+ SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Failed to register DBus notification filter: %s", error.message);
+ goto unregister_token;
+ }
+
+ core_interface_initialized = true;
+ return true;
+
+ // On failure, undo all registrations.
+unregister_token:
+ dbus->bus_remove_match(dbus->session_conn,
+ "type='signal', interface='" NOTIFICATION_CORE_INTERFACE "',"
+ "member='" NOTIFICATION_ACTIVATION_TOKEN_SIGNAL_NAME "'",
+ NULL);
+
+unregister_closed:
+ dbus->bus_remove_match(dbus->session_conn,
+ "type='signal', interface='" NOTIFICATION_CORE_INTERFACE "',"
+ "member='" NOTIFICATION_CLOSED_SIGNAL_NAME "'",
+ NULL);
+
+unregister_action:
+ dbus->bus_remove_match(dbus->session_conn,
+ "type='signal', interface='" NOTIFICATION_CORE_INTERFACE "',"
+ "member='" NOTIFICATION_ACTION_SIGNAL_NAME "'",
+ NULL);
+
+ return false;
+}
+
+static const char *GetIconURI()
+{
+ if (icon_uri) {
+ return icon_uri;
+ }
+
+ char full_path[PATH_MAX];
+ SDL_PropertiesID props = SDL_GetGlobalProperties();
+ const char *icon = SDL_GetStringProperty(props, SDL_PROP_GLOBAL_NOTIFICATION_HEADER_ICON_STRING, NULL);
+ if (icon && realpath(icon, full_path)) {
+ size_t len = SDL_strlen(full_path) + 8;
+ icon_uri = SDL_malloc(len);
+ if (icon_uri) {
+ SDL_strlcpy(icon_uri, "file://", len);
+ SDL_strlcat(icon_uri, full_path, len);
+ }
+ }
+
+ return icon_uri;
+}
+
+static SDL_NotificationID ShowCoreNotification(SDL_DBusContext *dbus, SDL_PropertiesID props)
+{
+ DBusConnection *conn = dbus->session_conn;
+ DBusMessage *msg = NULL;
+ DBusMessageIter iter, array;
+ const char *tmpstr = NULL;
+ const Sint32 timeout = -1;
+ Uint32 message_id = 0;
+ bool error_set = false;
+
+ const SDL_PropertiesID replaces = SDL_GetNumberProperty(props, SDL_PROP_NOTIFICATION_REPLACES_NUMBER, 0);
+ const char *title = SDL_GetStringProperty(props, SDL_PROP_NOTIFICATION_TITLE_STRING, NULL);
+ const char *message = SDL_GetStringProperty(props, SDL_PROP_NOTIFICATION_MESSAGE_STRING, NULL);
+ const SDL_NotificationAction *actions = SDL_GetPointerProperty(props, SDL_PROP_NOTIFICATION_ACTIONS_POINTER, NULL);
+ const int num_actions = (int)SDL_GetNumberProperty(props, SDL_PROP_NOTIFICATION_ACTION_COUNT_NUMBER, 0);
+
+ // Call org.freedesktop.Notifications.Notify()
+ msg = dbus->message_new_method_call(NOTIFICATION_CORE_NODE, NOTIFICATION_CORE_PATH, NOTIFICATION_CORE_INTERFACE, "Notify");
+ if (msg == NULL) {
+ goto failure;
+ }
+
+ dbus->message_iter_init_append(msg, &iter);
+ // App ID
+ tmpstr = SDL_GetAppMetadataProperty(SDL_PROP_APP_METADATA_NAME_STRING);
+ if (!tmpstr) {
+ SDL_GetAppID();
+ }
+ if (!dbus->message_iter_append_basic(&iter, DBUS_TYPE_STRING, &tmpstr)) {
+ goto failure;
+ }
+
+ // Replaces id
+ if (!dbus->message_iter_append_basic(&iter, DBUS_TYPE_UINT32, &replaces)) {
+ goto failure;
+ }
+
+ // Icon URI
+ tmpstr = GetIconURI();
+ if (!tmpstr) {
+ tmpstr = "";
+ }
+ if (!dbus->message_iter_append_basic(&iter, DBUS_TYPE_STRING, &tmpstr)) {
+ goto failure;
+ }
+
+ // Summary
+ if (!dbus->message_iter_append_basic(&iter, DBUS_TYPE_STRING, &title)) {
+ goto failure;
+ }
+
+ // Body
+ tmpstr = message ? message : "";
+ if (!dbus->message_iter_append_basic(&iter, DBUS_TYPE_STRING, &message)) {
+ goto failure;
+ }
+
+ {
+ // Actions
+ if (!dbus->message_iter_open_container(&iter, DBUS_TYPE_ARRAY, DBUS_TYPE_STRING_AS_STRING, &array)) {
+ goto failure;
+ }
+
+ // Add the default action
+ tmpstr = "default";
+ if (!dbus->message_iter_append_basic(&array, DBUS_TYPE_STRING, &tmpstr)) {
+ goto failure;
+ }
+ if (!dbus->message_iter_append_basic(&array, DBUS_TYPE_STRING, &tmpstr)) {
+ goto failure;
+ }
+
+ // Add the actions
+ if (actions) {
+ for (int i = 0; i < num_actions; ++i) {
+ if (actions[i].type == SDL_NOTIFICATION_ACTION_TYPE_BUTTON) {
+ if (!dbus->message_iter_append_basic(&array, DBUS_TYPE_STRING, &actions[i].button.action_id)) {
+ goto failure;
+ }
+ if (!dbus->message_iter_append_basic(&array, DBUS_TYPE_STRING, &actions[i].button.action_label)) {
+ goto failure;
+ }
+ }
+ }
+ }
+ if (!dbus->message_iter_close_container(&iter, &array)) {
+ goto failure;
+ }
+ }
+
+ // Hints
+ if (!SetCoreHints(dbus, &iter, props)) {
+ goto failure;
+ }
+
+ // Timeout
+ if (!dbus->message_iter_append_basic(&iter, DBUS_TYPE_INT32, &timeout)) {
+ goto failure;
+ }
+
+ {
+ DBusMessageIter reply_iter;
+ DBusError error;
+
+ dbus->error_init(&error);
+ DBusMessage *reply = dbus->connection_send_with_reply_and_block(conn, msg, -1, &error);
+ if (!reply) {
+ if (error.message) {
+ SDL_SetError("Notification failed: %s", error.message);
+ error_set = true;
+ }
+ dbus->error_free(&error);
+ goto failure;
+ }
+
+ dbus->error_free(&error);
+ dbus->message_unref(msg);
+
+ if (!dbus->message_iter_init(reply, &reply_iter)) {
+ goto failure;
+ }
+ if (dbus->message_iter_get_arg_type(&reply_iter) != DBUS_TYPE_UINT32) {
+ dbus->message_unref(reply);
+ goto failure;
+ }
+ dbus->message_iter_get_basic(&reply_iter, &message_id);
+ dbus->message_unref(reply);
+ }
+
+ if (core_id_count == SDL_arraysize(core_id_list)) {
+ RemoveIDFromListAtIndex(0);
+ }
+ core_id_list[core_id_count++] = message_id;
+
+ return message_id;
+
+failure:
+ if (msg) {
+ dbus->message_unref(msg);
+ }
+ if (!error_set) {
+ SDL_SetError("Failed to dispatch org.freedesktop.Notifications request (Out of memory?)");
+ }
+ return 0;
+}
+
+static bool RemoveCoreNotification(SDL_DBusContext *dbus, SDL_NotificationID id)
+{
+ if (!id) {
+ return SDL_InvalidParamError("id");
+ }
+
+ DBusMessageIter iter;
+ bool ret = false;
+
+ // Call org.freedesktop.Notifications.CloseNotification()
+ DBusMessage *msg = dbus->message_new_method_call(NOTIFICATION_CORE_NODE, NOTIFICATION_CORE_PATH, NOTIFICATION_CORE_INTERFACE, "CloseNotification");
+ if (!msg) {
+ return SDL_OutOfMemory();
+ }
+
+ dbus->message_iter_init_append(msg, &iter);
+ if (dbus->message_iter_append_basic(&iter, DBUS_TYPE_UINT32, &id)) {
+ ret = (bool)dbus->connection_send(dbus->session_conn, msg, NULL);
+ if (!ret) {
+ SDL_SetError("Failed to send notification removal request");
+ }
+ } else {
+ ret = SDL_OutOfMemory();
+ }
+
+ dbus->message_unref(msg);
+ return ret;
+}
+
+// org.freedesktop.portal.Notification interface, used when running in a Flatpak or SNAP container
+static DBusHandlerResult PortalNotificationFilter(DBusConnection *conn, DBusMessage *msg, void *data)
+{
+ SDL_DBusContext *dbus = SDL_DBus_GetContext();
+
+ if (dbus->message_is_signal(msg, NOTIFICATION_PORTAL_INTERFACE, NOTIFICATION_ACTION_SIGNAL_NAME)) {
+ DBusMessageIter signal_iter; //, variant_iter;
+ const char *str = NULL;
+
+ if (!dbus->message_iter_init(msg, &signal_iter)) {
+ goto not_our_signal;
+ }
+
+ // Check if the parameters are what we expect
+ if (dbus->message_iter_get_arg_type(&signal_iter) != DBUS_TYPE_STRING) {
+ goto not_our_signal;
+ }
+ dbus->message_iter_get_basic(&signal_iter, &str);
+
+ // Parse the ID.
+ if (SDL_strncmp(str, SDL_NOTIFICATION_PREAMBLE, sizeof(SDL_NOTIFICATION_PREAMBLE) - 1) != 0) {
+ goto not_our_signal;
+ }
+ const Uint32 id = (Uint32)SDL_strtoul(str + sizeof(SDL_NOTIFICATION_PREAMBLE), NULL, 10);
+ if (!id) {
+ goto not_our_signal;
+ }
+
+ if (!dbus->message_iter_next(&signal_iter)) {
+ goto not_our_signal;
+ }
+ if (dbus->message_iter_get_arg_type(&signal_iter) != DBUS_TYPE_STRING) {
+ goto not_our_signal;
+ }
+ dbus->message_iter_get_basic(&signal_iter, &str);
+
+ // Check for the target and optional XDG activation parameter.
+ const char *target = NULL, *token = NULL;
+
+ if (dbus->message_iter_next(&signal_iter) && dbus->message_iter_get_arg_type(&signal_iter) == DBUS_TYPE_ARRAY) {
+ DBusMessageIter param_iter;
+ dbus->message_iter_recurse(&signal_iter, ¶m_iter);
+
+ // The order of parameters in the array is defined: first the target, then the activation ID.
+ if (dbus->message_iter_get_arg_type(¶m_iter) == DBUS_TYPE_VARIANT) {
+ DBusMessageIter target_variant, target_string;
+
+ dbus->message_iter_recurse(¶m_iter, &target_variant);
+ if (dbus->message_iter_get_arg_type(&target_variant) == DBUS_TYPE_VARIANT) {
+ dbus->message_iter_recurse(&target_variant, &target_string);
+ if (dbus->message_iter_get_arg_type(&target_string) == DBUS_TYPE_STRING) {
+ dbus->message_iter_get_basic(&target_string, &target);
+ }
+ }
+ dbus->message_iter_next(¶m_iter);
+ }
+
+ // System properties array.
+ if (dbus->message_iter_get_arg_type(¶m_iter) == DBUS_TYPE_ARRAY) {
+ DBusMessageIter pdata_iter;
+
+ dbus->message_iter_recurse(¶m_iter, &pdata_iter);
+ while (dbus->message_iter_get_arg_type(&pdata_iter) == DBUS_TYPE_DICT_ENTRY) {
+ DBusMessageIter dict_entry_iter;
+
+ // Enter the dictionary entry.
+ dbus->message_iter_recurse(¶m_iter, &dict_entry_iter);
+
+ // Get the key
+ const char *key = NULL;
+ dbus->message_iter_get_basic(&dict_entry_iter, &key);
+
+ // Get the activation token string.
+ if (SDL_strcmp(key, "activation-token") == 0) {
+ DBusMessageIter dict_val_iter;
+ // Enter the value variant.
+ dbus->message_iter_recurse(&dict_entry_iter, &dict_val_iter);
+
+ if (dbus->message_iter_get_arg_type(&dict_val_iter) == DBUS_TYPE_STRING) {
+ dbus->message_iter_get_basic(&dict_val_iter, &token);
+ }
+
+ // Found the activation tok
(Patch may be truncated, please check the link at the top of this post.)