From 682da4ee9885c28bccf0dee832f3089b0abca547 Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Sat, 4 Apr 2026 22:28:06 -0400
Subject: [PATCH] misc: Use the OpenURI D-Bus portal for opening URLs
This works inside of containers, and supports passing an activation token with the request, which is needed on Wayland to transfer focus to the browser.
---
src/core/linux/SDL_dbus.c | 57 ++++++++++++
src/core/linux/SDL_dbus.h | 2 +
src/misc/unix/SDL_sysurl.c | 39 ++++++++
src/video/wayland/SDL_waylandutil.c | 134 ++++++++++++++++++++++++++++
src/video/wayland/SDL_waylandutil.h | 34 +++++++
test/testurl.c | 52 ++++++++---
6 files changed, 307 insertions(+), 11 deletions(-)
create mode 100644 src/video/wayland/SDL_waylandutil.c
create mode 100644 src/video/wayland/SDL_waylandutil.h
diff --git a/src/core/linux/SDL_dbus.c b/src/core/linux/SDL_dbus.c
index 2d083e8f29bdc..df114e0e19189 100644
--- a/src/core/linux/SDL_dbus.c
+++ b/src/core/linux/SDL_dbus.c
@@ -471,6 +471,63 @@ static bool SDL_DBus_AppendDictWithKeyValue(DBusMessageIter *iterInit, const cha
return SDL_DBus_AppendDictWithKeysAndValues(iterInit, keys, values, 1);
}
+bool SDL_DBus_OpenURI(const char *uri, const char *window_id, const char *activation_token)
+{
+ const char *bus_name = "org.freedesktop.portal.Desktop";
+ const char *path = "/org/freedesktop/portal/desktop";
+ const char *interface = "org.freedesktop.portal.OpenURI";
+ DBusMessageIter iterInit;
+ bool ret = false;
+
+ if (!dbus.session_conn) {
+ /* We either lost connection to the session bus or were not able to
+ * load the D-Bus library at all.
+ */
+ return false;
+ }
+
+ DBusMessage *msg = dbus.message_new_method_call(bus_name, path, interface, "OpenURI");
+ if (!msg) {
+ return false;
+ }
+
+ if (!window_id) {
+ window_id = "";
+ }
+ if (!dbus.message_append_args(msg, DBUS_TYPE_STRING, &window_id, DBUS_TYPE_STRING, &uri, DBUS_TYPE_INVALID)) {
+ goto done;
+ }
+
+ dbus.message_iter_init_append(msg, &iterInit);
+
+ if (activation_token) {
+ if (!SDL_DBus_AppendDictWithKeyValue(&iterInit, "activation_token", activation_token)) {
+ goto done;
+ }
+ } else {
+ // The array must be in the parameter list, even if empty.
+ DBusMessageIter iterArray;
+ if (!dbus.message_iter_open_container(&iterInit, DBUS_TYPE_ARRAY, "{sv}", &iterArray)) {
+ goto done;
+ }
+ if (!dbus.message_iter_close_container(&iterInit, &iterArray)) {
+ goto done;
+ }
+ }
+
+ {
+ DBusMessage *reply = dbus.connection_send_with_reply_and_block(dbus.session_conn, msg, -1, NULL);
+ if (reply) {
+ ret = true;
+ dbus.message_unref(reply);
+ }
+ }
+
+done:
+ dbus.message_unref(msg);
+ return ret;
+}
+
bool SDL_DBus_ScreensaverInhibit(bool inhibit)
{
const char *default_inhibit_reason = "Playing a game";
diff --git a/src/core/linux/SDL_dbus.h b/src/core/linux/SDL_dbus.h
index e6f81b48acea2..568dd74bb2c20 100644
--- a/src/core/linux/SDL_dbus.h
+++ b/src/core/linux/SDL_dbus.h
@@ -119,6 +119,8 @@ extern void SDL_DBus_FreeReply(DBusMessage **saved_reply);
extern void SDL_DBus_ScreensaverTickle(void);
extern bool SDL_DBus_ScreensaverInhibit(bool inhibit);
+extern bool SDL_DBus_OpenURI(const char *uri, const char *window_id, const char *activation_token);
+
extern void SDL_DBus_PumpEvents(void);
extern char *SDL_DBus_GetLocalMachineId(void);
diff --git a/src/misc/unix/SDL_sysurl.c b/src/misc/unix/SDL_sysurl.c
index 8745576e2ca1a..32e2d476ead98 100644
--- a/src/misc/unix/SDL_sysurl.c
+++ b/src/misc/unix/SDL_sysurl.c
@@ -33,8 +33,42 @@
extern char **environ;
#endif
+#ifdef HAVE_DBUS_DBUS_H
+#include "../../core/linux/SDL_dbus.h"
+#endif
+
+#ifdef SDL_VIDEO_DRIVER_WAYLAND
+#include "../../video/wayland/SDL_waylandutil.h"
+#endif
+
+// Wayland requires an activation token for the browser to take focus.
+static void GetActivationToken(char **token, char **window_id)
+{
+#ifdef SDL_VIDEO_DRIVER_WAYLAND
+ SDL_VideoDevice *vid = SDL_GetVideoDevice();
+
+ if (vid && SDL_strcmp(vid->name, "wayland") == 0) {
+ Wayland_GetActivationTokenForExport(vid, token, window_id);
+ }
+#endif
+}
+
bool SDL_SYS_OpenURL(const char *url)
{
+ char *activation_token = NULL;
+ char *window_id = NULL;
+
+ GetActivationToken(&activation_token, &window_id);
+
+ // Prefer the D-Bus portal, if available.
+#ifdef HAVE_DBUS_DBUS_H
+ if (SDL_DBus_OpenURI(url, window_id, activation_token)) {
+ SDL_free(activation_token);
+ SDL_free(window_id);
+ return true;
+ }
+#endif
+
const char *args[] = { "xdg-open", url, NULL };
SDL_Environment *env = NULL;
SDL_Process *process = NULL;
@@ -47,6 +81,9 @@ bool SDL_SYS_OpenURL(const char *url)
// Clear LD_PRELOAD so Chrome opens correctly when this application is launched by Steam
SDL_UnsetEnvironmentVariable(env, "LD_PRELOAD");
+ if (activation_token) {
+ SDL_SetEnvironmentVariable(env, "XDG_ACTIVATION_TOKEN", activation_token, false);
+ }
SDL_PropertiesID props = SDL_CreateProperties();
if (!props) {
@@ -64,6 +101,8 @@ bool SDL_SYS_OpenURL(const char *url)
result = true;
done:
+ SDL_free(activation_token);
+ SDL_free(window_id);
SDL_DestroyEnvironment(env);
SDL_DestroyProcess(process);
diff --git a/src/video/wayland/SDL_waylandutil.c b/src/video/wayland/SDL_waylandutil.c
new file mode 100644
index 0000000000000..d6461cb07130a
--- /dev/null
+++ b/src/video/wayland/SDL_waylandutil.c
@@ -0,0 +1,134 @@
+/*
+ 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 "SDL_internal.h"
+
+#ifdef SDL_VIDEO_DRIVER_WAYLAND
+
+#include "SDL_waylandevents_c.h"
+#include "SDL_waylandutil.h"
+#include "xdg-activation-v1-client-protocol.h"
+
+#define WAYLAND_HANDLE_PREFIX "wayland:"
+
+typedef struct Wayland_ActivationParams
+{
+ char **token;
+ bool done;
+} Wayland_ActivationParams;
+
+static void handle_xdg_activation_done(void *data, struct xdg_activation_token_v1 *xdg_activation_token_v1, const char *token)
+{
+ Wayland_ActivationParams *activation_params = (Wayland_ActivationParams *)data;
+ *activation_params->token = SDL_strdup(token);
+ activation_params->done = true;
+
+ xdg_activation_token_v1_destroy(xdg_activation_token_v1);
+}
+
+static const struct xdg_activation_token_v1_listener xdg_activation_listener = {
+ handle_xdg_activation_done
+};
+
+bool Wayland_GetActivationTokenForExport(SDL_VideoDevice *_this, char **token, char **window_id)
+{
+ if (!_this || !token) {
+ return false;
+ }
+
+ SDL_VideoData *viddata = _this->internal;
+
+ SDL_WaylandSeat *seat = viddata->last_implicit_grab_seat;
+ SDL_WindowData *focus = NULL;
+
+ if (seat) {
+ focus = seat->keyboard.focus;
+ if (!focus) {
+ focus = seat->pointer.focus;
+ }
+ }
+
+ const char *xdg_activation_token = SDL_getenv("XDG_ACTIVATION_TOKEN");
+ if (xdg_activation_token) {
+ *token = SDL_strdup(xdg_activation_token);
+ if (!*token) {
+ return false;
+ }
+
+ // Unset the envvar after claiming the token.
+ SDL_unsetenv_unsafe("XDG_ACTIVATION_TOKEN");
+ } else if (viddata->activation_manager) {
+ struct wl_surface *requesting_surface = focus ? focus->surface : NULL;
+ Wayland_ActivationParams params = {
+ .token = token,
+ .done = false
+ };
+
+ struct wl_event_queue *activation_token_queue = Wayland_DisplayCreateQueue(viddata->display, "SDL Activation Token Generation Queue");
+
+ struct wl_proxy *activation_manager_wrapper = WAYLAND_wl_proxy_create_wrapper(viddata->activation_manager);
+ WAYLAND_wl_proxy_set_queue(activation_manager_wrapper, activation_token_queue);
+ struct xdg_activation_token_v1 *activation_token = xdg_activation_v1_get_activation_token((struct xdg_activation_v1 *)activation_manager_wrapper);
+ xdg_activation_token_v1_add_listener(activation_token, &xdg_activation_listener, ¶ms);
+
+ if (requesting_surface) {
+ // This specifies the surface from which the activation request is originating, not the activation target surface.
+ xdg_activation_token_v1_set_surface(activation_token, requesting_surface);
+ }
+ if (seat && seat->wl_seat) {
+ xdg_activation_token_v1_set_serial(activation_token, seat->last_implicit_grab_serial, seat->wl_seat);
+ }
+ if (focus && focus->app_id) {
+ // Set the app ID for external use.
+ xdg_activation_token_v1_set_app_id(activation_token, focus->app_id);
+ }
+ xdg_activation_token_v1_commit(activation_token);
+
+ while (!params.done) {
+ WAYLAND_wl_display_dispatch_queue(viddata->display, activation_token_queue);
+ }
+ WAYLAND_wl_proxy_wrapper_destroy(activation_manager_wrapper);
+ WAYLAND_wl_event_queue_destroy(activation_token_queue);
+
+ if (!*token) {
+ return false;
+ }
+ }
+
+ if (focus && window_id) {
+ const char *id = SDL_GetStringProperty(focus->sdlwindow->props, SDL_PROP_WINDOW_WAYLAND_XDG_TOPLEVEL_EXPORT_HANDLE_STRING, NULL);
+ if (id) {
+ const size_t len = SDL_strlen(id) + sizeof(WAYLAND_HANDLE_PREFIX) + 1;
+ *window_id = SDL_malloc(len);
+ if (!*window_id) {
+ SDL_free(*token);
+ *token = NULL;
+ return false;
+ }
+
+ SDL_strlcpy(*window_id, WAYLAND_HANDLE_PREFIX, len);
+ SDL_strlcat(*window_id, id, len);
+ }
+ }
+
+ return true;
+}
+
+#endif // SDL_VIDEO_DRIVER_WAYLAND
diff --git a/src/video/wayland/SDL_waylandutil.h b/src/video/wayland/SDL_waylandutil.h
new file mode 100644
index 0000000000000..daad6effb3479
--- /dev/null
+++ b/src/video/wayland/SDL_waylandutil.h
@@ -0,0 +1,34 @@
+/*
+ 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 "SDL_internal.h"
+
+#ifndef SDL_waylandutil_h_
+#define SDL_waylandutil_h_
+
+#include "../SDL_sysvideo.h"
+
+/**
+ * Generates an activation token that can be passed to external clients.
+ * The token and window_id parameters must be freed with SDL_free() when done.
+ */
+extern bool Wayland_GetActivationTokenForExport(SDL_VideoDevice *_this, char **token, char **window_id);
+
+#endif // SDL_waylandutil_h_
diff --git a/test/testurl.c b/test/testurl.c
index 56b1904e6adea..a6fd9c5ec41a5 100644
--- a/test/testurl.c
+++ b/test/testurl.c
@@ -25,29 +25,27 @@ static void tryOpenURL(const char *url)
int main(int argc, char **argv)
{
- int i;
- SDLTest_CommonState *state;
-
- state = SDLTest_CommonCreateState(argv, 0);
-
- if (!SDL_Init(SDL_INIT_VIDEO)) {
- SDL_Log("SDL_Init failed: %s", SDL_GetError());
- return 1;
- }
+ const char *url = NULL;
+ SDLTest_CommonState *state = SDLTest_CommonCreateState(argv, 0);
+ bool use_gui = false;
/* Parse commandline */
- for (i = 1; i < argc;) {
+ for (int i = 1; i < argc;) {
int consumed;
consumed = SDLTest_CommonArg(state, i);
if (consumed == 0) {
if (argv[i][0] != '-') {
- tryOpenURL(argv[i]);
+ url = argv[i];
+ consumed = 1;
+ } else if (SDL_strcasecmp(argv[i], "--gui") == 0) {
+ use_gui = true;
consumed = 1;
}
}
if (consumed <= 0) {
static const char *options[] = {
+ "[--gui]"
"[URL [...]]",
NULL,
};
@@ -57,6 +55,38 @@ int main(int argc, char **argv)
i += consumed;
}
+ state->flags = SDL_INIT_VIDEO;
+ if (!SDLTest_CommonInit(state)) {
+ return SDL_APP_FAILURE;
+ }
+
+ if (!use_gui) {
+ tryOpenURL(url);
+ } else {
+ SDL_Event event;
+ bool quit = false;
+
+ while (!quit) {
+ while (SDL_PollEvent(&event)) {
+ if (event.type == SDL_EVENT_KEY_DOWN) {
+ if (event.key.key == SDLK_SPACE) {
+ tryOpenURL(url);
+ } else if (event.key.key == SDLK_ESCAPE) {
+ quit = true;
+ }
+ } else if (event.type == SDL_EVENT_QUIT) {
+ quit = true;
+ }
+ }
+
+ SDL_SetRenderDrawColor(state->renderers[0], 0, 0, 0, 255);
+ SDL_RenderClear(state->renderers[0]);
+ SDL_SetRenderDrawColor(state->renderers[0], 255, 255, 255, 255);
+ SDL_RenderDebugTextFormat(state->renderers[0], 8.f, 16.f, "Press space to open %s", url);
+ SDL_RenderPresent(state->renderers[0]);
+ }
+ }
+
SDL_Quit();
SDLTest_CommonDestroyState(state);
return 0;