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.

Reply via email to