This is an automated email from the git hooks/post-receive script.
git pushed a commit to branch wl/browser-all
in repository enlightenment.
View the commit online.
commit d3609d162402760aee36d31d44f740d30ff84a3d
Author: Cedric BAIL <[email protected]>
AuthorDate: Wed Aug 12 16:54:19 2026 -0600
e_comp_wl - add xdg_activation_v1
E-04. Without it "open this link in the browser that is already running",
clicking a notification, and cross-app raise all silently do nothing - they
fall back to an urgency hint and a taskbar entry that blinks.
A client that wants to hand its focus away asks for a token, says what it is
for, and commits; we answer with a string. The receiving client passes that
string to activate() and is raised and focused.
The token is a capability, so the only interesting question is whether it is
guessable - if it is, this is a focus-stealing hole with a protocol wrapped
round it. uuid_generate gives an unguessable one and is already how
xdg-foreign names its handles. A token is good once and is dropped from the
table when it is redeemed.
Redeeming an unknown or already-spent token is ignored rather than fatal: the
spec is explicit that a token may expire, so a client must not be killed for
presenting a stale one. Committing the same token object twice is the one case
that is an error, and it is the one the protocol names, already_used.
The vtables are written with designated initialisers. That is not style: in
this interface destroy comes last where in most others it comes first, and the
positional list I wrote first silently wired commit to set_surface, so commit
did nothing and no token ever came back. The test caught it.
src/tests/wayland/test_activation.c asserts what makes the token worth having
- that it round-trips, that two tokens differ, that one is good once, that a
stale one is ignored and a double commit is not. It deliberately stops short
of asserting the focus change itself; that needs a mapped toplevel with
something to take focus from, and it is what the browser bring-up covers.
Note for whoever runs the suite next: eight nested compositors in parallel is
enough to make two unrelated tests fail their registry roundtrip on this
machine. --num-processes 1 is clean. That limit was already there; an eighth
test is just what found it.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FtoiXoSKUmZb6Aix6U3GZS
---
src/bin/e_comp_wl.h | 4 +
src/bin/e_comp_wl_extensions.c | 172 ++++++++++++++++++++++++++++++++++
src/bin/generated/meson.build | 1 +
src/tests/wayland/globals.expected | 1 +
src/tests/wayland/meson.build | 2 +
src/tests/wayland/test_activation.c | 180 ++++++++++++++++++++++++++++++++++++
src/tests/wlcs/e_wlcs.c | 1 +
7 files changed, 361 insertions(+)
diff --git a/src/bin/e_comp_wl.h b/src/bin/e_comp_wl.h
index d889b46ab..7fd18dd07 100644
--- a/src/bin/e_comp_wl.h
+++ b/src/bin/e_comp_wl.h
@@ -139,6 +139,10 @@ typedef struct E_Comp_Wl_Extension_Data
{
struct wl_global *global;
} zxdg_importer_v1;
+ struct
+ {
+ struct wl_global *global;
+ } xdg_activation_v1;
/* end xdg-foreign */
struct
{
diff --git a/src/bin/e_comp_wl_extensions.c b/src/bin/e_comp_wl_extensions.c
index 880373636..64275fb49 100644
--- a/src/bin/e_comp_wl_extensions.c
+++ b/src/bin/e_comp_wl_extensions.c
@@ -7,6 +7,7 @@
#include "relative-pointer-unstable-v1-server-protocol.h"
#include "pointer-constraints-unstable-v1-server-protocol.h"
#include "action_route-server-protocol.h"
+#include "xdg-activation-v1-server-protocol.h"
/* mutter uses 32, seems reasonable */
#define HANDLE_LEN 32
@@ -1009,6 +1010,175 @@ e_comp_wl_extension_action_route_interface_get(int *version)
return &_e_action_route_interface;
}
+/* xdg_activation_v1.
+ *
+ * A client that wants to hand its focus to another asks for a token, says
+ * what the token is for, and commits it; we hand back a string. The receiving
+ * client passes that string to activate() and gets raised and focused. It is
+ * the mechanism behind "open this link in the browser that is already
+ * running" and behind clicking a notification, both of which fall back to an
+ * urgency hint and a flashing taskbar entry without it.
+ *
+ * The token is a capability, so it has to be unguessable - anything else is a
+ * focus-stealing hole dressed up as a protocol. uuid_generate gives us that
+ * and is already how xdg-foreign names its handles. One use each: a token is
+ * dropped from the table the moment it is redeemed. */
+typedef struct Activation_Token
+{
+ struct wl_resource *res;
+ E_Client *ec; /* the surface the request came from */
+ struct wl_resource *seat;
+ const char *app_id;
+ uint32_t serial;
+ char token[37]; /* uuid_unparse writes 36 + NUL */
+ Eina_Bool committed E_BITFIELD;
+} Activation_Token;
+
+static Eina_Hash *xdg_activation_tokens;
+
+static void
+_xdg_activation_token_free(Activation_Token *t)
+{
+ if (!t) return;
+ eina_stringshare_del(t->app_id);
+ free(t);
+}
+
+static void
+_e_xdg_activation_token_cb_destroy(struct wl_client *client EINA_UNUSED, struct wl_resource *resource)
+{
+ wl_resource_destroy(resource);
+}
+
+static void
+_e_xdg_activation_token_res_destroy(struct wl_resource *resource)
+{
+ Activation_Token *t = wl_resource_get_user_data(resource);
+
+ if (!t) return;
+ /* A committed token outlives its object on purpose: the string has been
+ * handed over and the other client still has to be able to redeem it.
+ * Only an uncommitted one dies with the object it was never used from. */
+ if (t->committed) return;
+ _xdg_activation_token_free(t);
+}
+
+static void
+_e_xdg_activation_token_cb_set_serial(struct wl_client *client EINA_UNUSED, struct wl_resource *resource, uint32_t serial, struct wl_resource *seat)
+{
+ Activation_Token *t = wl_resource_get_user_data(resource);
+
+ if (!t) return;
+ t->serial = serial;
+ t->seat = seat;
+}
+
+static void
+_e_xdg_activation_token_cb_set_app_id(struct wl_client *client EINA_UNUSED, struct wl_resource *resource, const char *app_id)
+{
+ Activation_Token *t = wl_resource_get_user_data(resource);
+
+ if (!t) return;
+ eina_stringshare_replace(&t->app_id, app_id);
+}
+
+static void
+_e_xdg_activation_token_cb_set_surface(struct wl_client *client EINA_UNUSED, struct wl_resource *resource, struct wl_resource *surface)
+{
+ Activation_Token *t = wl_resource_get_user_data(resource);
+
+ if (!t) return;
+ t->ec = wl_resource_get_user_data(surface);
+}
+
+static void
+_e_xdg_activation_token_cb_commit(struct wl_client *client EINA_UNUSED, struct wl_resource *resource)
+{
+ Activation_Token *t = wl_resource_get_user_data(resource);
+ uuid_t u;
+
+ if (!t) return;
+ if (t->committed)
+ {
+ wl_resource_post_error(resource, XDG_ACTIVATION_TOKEN_V1_ERROR_ALREADY_USED,
+ "token has already been committed");
+ return;
+ }
+ uuid_generate(u);
+ uuid_unparse_lower(u, t->token);
+ t->committed = EINA_TRUE;
+
+ if (!xdg_activation_tokens)
+ xdg_activation_tokens = eina_hash_string_superfast_new(NULL);
+ eina_hash_add(xdg_activation_tokens, t->token, t);
+
+ xdg_activation_token_v1_send_done(resource, t->token);
+}
+
+/* Named rather than positional: destroy is last in this interface and first
+ * in most others, and a positional list quietly wires commit to set_surface. */
+static const struct xdg_activation_token_v1_interface _e_xdg_activation_token_v1_interface =
+{
+ .set_serial = _e_xdg_activation_token_cb_set_serial,
+ .set_app_id = _e_xdg_activation_token_cb_set_app_id,
+ .set_surface = _e_xdg_activation_token_cb_set_surface,
+ .commit = _e_xdg_activation_token_cb_commit,
+ .destroy = _e_xdg_activation_token_cb_destroy,
+};
+
+static void
+_e_xdg_activation_v1_cb_destroy(struct wl_client *client EINA_UNUSED, struct wl_resource *resource)
+{
+ wl_resource_destroy(resource);
+}
+
+static void
+_e_xdg_activation_v1_cb_get_activation_token(struct wl_client *client, struct wl_resource *resource, uint32_t id)
+{
+ struct wl_resource *res;
+ Activation_Token *t;
+
+ res = wl_resource_create(client, &xdg_activation_token_v1_interface,
+ wl_resource_get_version(resource), id);
+ if (!res)
+ {
+ wl_client_post_no_memory(client);
+ return;
+ }
+ t = E_NEW(Activation_Token, 1);
+ t->res = res;
+ wl_resource_set_implementation(res, &_e_xdg_activation_token_v1_interface, t,
+ _e_xdg_activation_token_res_destroy);
+}
+
+static void
+_e_xdg_activation_v1_cb_activate(struct wl_client *client EINA_UNUSED, struct wl_resource *resource EINA_UNUSED, const char *token, struct wl_resource *surface)
+{
+ Activation_Token *t;
+ E_Client *ec;
+
+ if (!token) return;
+ if (!xdg_activation_tokens) return;
+ t = eina_hash_find(xdg_activation_tokens, token);
+ /* An unknown token is not a protocol error - the spec says the request is
+ * simply ignored, because a token can expire on its own. */
+ if (!t) return;
+ eina_hash_del_by_key(xdg_activation_tokens, token);
+
+ ec = wl_resource_get_user_data(surface);
+ if (ec && (!e_object_is_del(E_OBJECT(ec))))
+ e_client_activate(ec, EINA_TRUE);
+
+ _xdg_activation_token_free(t);
+}
+
+static const struct xdg_activation_v1_interface _e_xdg_activation_v1_interface =
+{
+ .destroy = _e_xdg_activation_v1_cb_destroy,
+ .get_activation_token = _e_xdg_activation_v1_cb_get_activation_token,
+ .activate = _e_xdg_activation_v1_cb_activate,
+};
+
#define GLOBAL_BIND_CB(NAME, IFACE, ...) \
static void \
_e_comp_wl_##NAME##_cb_bind(struct wl_client *client, void *data EINA_UNUSED, uint32_t version, uint32_t id) \
@@ -1031,6 +1201,7 @@ GLOBAL_BIND_CB(zxdg_exporter_v1, zxdg_exporter_v1_interface)
GLOBAL_BIND_CB(zxdg_importer_v1, zxdg_importer_v1_interface)
GLOBAL_BIND_CB(zwp_relative_pointer_manager_v1, zwp_relative_pointer_manager_v1_interface)
GLOBAL_BIND_CB(zwp_pointer_constraints_v1, zwp_pointer_constraints_v1_interface)
+GLOBAL_BIND_CB(xdg_activation_v1, xdg_activation_v1_interface)
GLOBAL_BIND_CB(action_route, action_route_interface,
e_binding_key_list_cb = _action_route_key_list_cb;
key_bindings = eina_hash_string_superfast_new(NULL);
@@ -1129,6 +1300,7 @@ e_comp_wl_extensions_init(void)
GLOBAL_CREATE_OR_RETURN(zwp_pointer_constraints_v1, zwp_pointer_constraints_v1_interface, 1);
e_comp_wl->extensions->zwp_pointer_constraints_v1.constraints = eina_hash_pointer_new(NULL);
GLOBAL_CREATE_OR_RETURN(action_route, action_route_interface, 1);
+ GLOBAL_CREATE_OR_RETURN(xdg_activation_v1, xdg_activation_v1_interface, 1);
ecore_event_handler_add(ECORE_WL2_EVENT_SYNC_DONE, _dmabuf_add, NULL);
diff --git a/src/bin/generated/meson.build b/src/bin/generated/meson.build
index 0ad40be1f..c9beef897 100644
--- a/src/bin/generated/meson.build
+++ b/src/bin/generated/meson.build
@@ -6,6 +6,7 @@ protos = [
'@0@/unstable/xdg-foreign/xdg-foreign-unstable-v1.xml'.format(dir_wayland_protocols),
'@0@/unstable/relative-pointer/relative-pointer-unstable-v1.xml'.format(dir_wayland_protocols),
'@0@/unstable/pointer-constraints/pointer-constraints-unstable-v1.xml'.format(dir_wayland_protocols),
+ '@0@/staging/xdg-activation/xdg-activation-v1.xml'.format(dir_wayland_protocols),
]
proto_c = []
diff --git a/src/tests/wayland/globals.expected b/src/tests/wayland/globals.expected
index 800c39749..94ed0330b 100644
--- a/src/tests/wayland/globals.expected
+++ b/src/tests/wayland/globals.expected
@@ -7,6 +7,7 @@ wl_seat 5
wl_shell 1
wl_shm 1
wl_subcompositor 1
+xdg_activation_v1 1
xdg_wm_base 6
zwp_e_session_recovery 1
zwp_pointer_constraints_v1 1
diff --git a/src/tests/wayland/meson.build b/src/tests/wayland/meson.build
index 1dc6922b1..24009633b 100644
--- a/src/tests/wayland/meson.build
+++ b/src/tests/wayland/meson.build
@@ -30,6 +30,7 @@ test_proto_src = []
foreach p: [
'../../protocol/wl-test.xml',
'@0@/stable/xdg-shell/xdg-shell.xml'.format(dir_wayland_protocols),
+ '@0@/staging/xdg-activation/xdg-activation-v1.xml'.format(dir_wayland_protocols),
]
test_proto_src += gen_scanner_client.process(p)
test_proto_src += gen_scanner_impl.process(p)
@@ -52,6 +53,7 @@ wl_protocol_tests = [
['pointer-frame', 'test_pointer_frame.c'],
['surface-unmap', 'test_surface_unmap.c'],
['output', 'test_output.c'],
+ ['activation', 'test_activation.c'],
]
foreach t: wl_protocol_tests
diff --git a/src/tests/wayland/test_activation.c b/src/tests/wayland/test_activation.c
new file mode 100644
index 000000000..b76467c9f
--- /dev/null
+++ b/src/tests/wayland/test_activation.c
@@ -0,0 +1,180 @@
+/* xdg_activation_v1 conformance (E-04).
+ *
+ * The interesting part of activation is not the focus change - that is one
+ * call into E's own activate path - it is the token, which is a capability.
+ * Get the token wrong and "open this link in the running browser" becomes a
+ * way for any client to steal focus whenever it likes. So this asserts the
+ * properties that make the token worth having:
+ *
+ * - the global is advertised and a token round-trips: commit answers done
+ * with a non-empty string;
+ * - two tokens are different, i.e. it is not a counter or a constant;
+ * - a token is good once - redeeming it twice does nothing the second time;
+ * - committing the same token object twice is the already_used error rather
+ * than a second string;
+ * - activate() with a token we never issued is ignored, not fatal. The spec
+ * is explicit that a token may expire, so a client cannot be killed for
+ * presenting a stale one.
+ *
+ * What is deliberately not asserted here is that the surface actually takes
+ * focus: that needs a mapped toplevel and a compositor with something to take
+ * focus away from, and it is what the browser bring-up covers.
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <wayland-client.h>
+#include "xdg-activation-v1-client-protocol.h"
+
+#define FAIL(fmt, ...) \
+ do { fprintf(stderr, "test-activation: " fmt "\n", ##__VA_ARGS__); return 1; } while (0)
+
+typedef struct
+{
+ struct wl_compositor *compositor;
+ struct xdg_activation_v1 *activation;
+ uint32_t bound_version;
+
+ char token[256];
+ int done_count;
+
+ int error_code;
+ int errored;
+} Act;
+
+static Act act;
+
+static void
+_token_done(void *data, struct xdg_activation_token_v1 *t, const char *token)
+{
+ Act *a = data;
+
+ (void)t;
+ snprintf(a->token, sizeof(a->token), "%s", token ?: "");
+ a->done_count++;
+}
+
+static const struct xdg_activation_token_v1_listener _token_listener =
+{
+ _token_done
+};
+
+static void
+_global_add(void *data, struct wl_registry *reg, uint32_t id,
+ const char *iface, uint32_t version)
+{
+ Act *a = data;
+
+ if (!strcmp(iface, "wl_compositor"))
+ a->compositor = wl_registry_bind(reg, id, &wl_compositor_interface,
+ version < 4 ? version : 4);
+ else if (!strcmp(iface, "xdg_activation_v1"))
+ {
+ a->bound_version = version < 1 ? version : 1;
+ a->activation = wl_registry_bind(reg, id, &xdg_activation_v1_interface,
+ a->bound_version);
+ }
+}
+
+static void
+_global_remove(void *data, struct wl_registry *reg, uint32_t id)
+{
+ (void)data; (void)reg; (void)id;
+}
+
+static const struct wl_registry_listener _registry_listener =
+{
+ _global_add, _global_remove
+};
+
+/* Ask for one token and return the string it answers with. */
+static int
+_mint(struct wl_display *disp, struct wl_surface *surf, char *out, size_t len)
+{
+ struct xdg_activation_token_v1 *tok;
+
+ act.token[0] = 0;
+ act.done_count = 0;
+
+ tok = xdg_activation_v1_get_activation_token(act.activation);
+ if (!tok) return -1;
+ xdg_activation_token_v1_add_listener(tok, &_token_listener, &act);
+ xdg_activation_token_v1_set_app_id(tok, "test-activation");
+ if (surf) xdg_activation_token_v1_set_surface(tok, surf);
+ xdg_activation_token_v1_commit(tok);
+
+ if (wl_display_roundtrip(disp) < 0) return -1;
+ if (act.done_count != 1) return -1;
+
+ snprintf(out, len, "%s", act.token);
+ xdg_activation_token_v1_destroy(tok);
+ return 0;
+}
+
+int
+main(void)
+{
+ struct wl_display *disp;
+ struct wl_registry *reg;
+ struct wl_surface *surf;
+ struct xdg_activation_token_v1 *tok;
+ char a_tok[256], b_tok[256];
+
+ disp = wl_display_connect(NULL);
+ if (!disp) FAIL("cannot connect to WAYLAND_DISPLAY=%s",
+ getenv("WAYLAND_DISPLAY") ?: "(unset)");
+
+ reg = wl_display_get_registry(disp);
+ wl_registry_add_listener(reg, &_registry_listener, &act);
+ if (wl_display_roundtrip(disp) < 0) FAIL("registry roundtrip failed");
+
+ if (!act.activation)
+ FAIL("compositor advertises no xdg_activation_v1 -- xdg-open into a "
+ "running browser silently does nothing without it");
+ if (!act.compositor) FAIL("compositor advertises no wl_compositor");
+
+ surf = wl_compositor_create_surface(act.compositor);
+ if (!surf) FAIL("could not create a surface");
+
+ if (_mint(disp, surf, a_tok, sizeof(a_tok)))
+ FAIL("no done event after commit -- the token never came back");
+ if (!a_tok[0]) FAIL("done carried an empty token");
+
+ if (_mint(disp, surf, b_tok, sizeof(b_tok)))
+ FAIL("second token was not issued");
+
+ /* A guessable token is a focus-stealing hole with extra steps. */
+ if (!strcmp(a_tok, b_tok))
+ FAIL("two tokens are identical (\"%s\") -- a token has to be "
+ "unguessable, not a constant or a counter", a_tok);
+
+ /* One use each. Neither call may kill the connection: the second is a
+ * stale token, which the spec says to ignore. */
+ xdg_activation_v1_activate(act.activation, a_tok, surf);
+ if (wl_display_roundtrip(disp) < 0)
+ FAIL("connection died redeeming a valid token");
+ xdg_activation_v1_activate(act.activation, a_tok, surf);
+ if (wl_display_roundtrip(disp) < 0)
+ FAIL("connection died redeeming a token a second time -- a spent token "
+ "has to be ignored, not fatal");
+
+ /* Never issued at all. Same rule: ignored. */
+ xdg_activation_v1_activate(act.activation, "not-a-token-we-ever-made", surf);
+ if (wl_display_roundtrip(disp) < 0)
+ FAIL("connection died on an unknown token -- a token may expire, so a "
+ "client cannot be killed for presenting a stale one");
+
+ /* Committing one token object twice is the one case that IS an error. */
+ tok = xdg_activation_v1_get_activation_token(act.activation);
+ xdg_activation_token_v1_add_listener(tok, &_token_listener, &act);
+ act.done_count = 0;
+ xdg_activation_token_v1_commit(tok);
+ xdg_activation_token_v1_commit(tok);
+ if (wl_display_roundtrip(disp) >= 0)
+ FAIL("committing a token twice was accepted; xdg_activation_token_v1 "
+ "error already_used exists to reject it");
+
+ printf("test-activation: ok (v%u, tokens \"%s\" != \"%s\")\n",
+ act.bound_version, a_tok, b_tok);
+ return 0;
+}
diff --git a/src/tests/wlcs/e_wlcs.c b/src/tests/wlcs/e_wlcs.c
index b1ead391a..d519eef66 100644
--- a/src/tests/wlcs/e_wlcs.c
+++ b/src/tests/wlcs/e_wlcs.c
@@ -700,6 +700,7 @@ static const WlcsExtensionDescriptor _extensions[] =
{ "wl_seat", 5 },
{ "wl_output", 2 },
{ "wl_data_device_manager", 3 },
+ { "xdg_activation_v1", 1 },
{ "xdg_wm_base", 6 },
{ "zxdg_shell_v6", 1 },
{ "wl_shell", 1 },
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.