SDL: Add D-Bus notification driver

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, &param_iter);
+
+            // The order of parameters in the array is defined: first the target, then the activation ID.
+            if (dbus->message_iter_get_arg_type(&param_iter) == DBUS_TYPE_VARIANT) {
+                DBusMessageIter target_variant, target_string;
+
+                dbus->message_iter_recurse(&param_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(&param_iter);
+            }
+
+            // System properties array.
+            if (dbus->message_iter_get_arg_type(&param_iter) == DBUS_TYPE_ARRAY) {
+                DBusMessageIter pdata_iter;
+
+                dbus->message_iter_recurse(&param_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(&param_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.)