xdg-activation: focus windows by token

Let launchers focus existing windows via xdg-activation-v1.  On
request_activate, find the kick window owning the surface and switch to
its tab (set_active_tab + window_enter, both gated on session_locked so
activation can never bypass the lock screen).

Add a --activate wltest mode (two windows; second steals focus, then a
token activates the first) and two smoke tests proving the global is
advertised and the inactive window receives an activated configure.
This commit is contained in:
Andrew Hamilton 2026-09-03 14:11:11 +10:00
parent 0475216913
commit 9ea0766bd3
4 changed files with 250 additions and 0 deletions

View file

@ -29,6 +29,7 @@
#include "wlr-output-power-management-unstable-v1-client.h"
#include "virtual-keyboard-unstable-v1-client.h"
#include "wlr-virtual-pointer-unstable-v1-client.h"
#include "xdg-activation-v1-client.h"
/* Minimal deterministic xdg toplevel test client: shows a solid-color shm
buffer and runs until killed. Used by test.sh to give the smoke test two
@ -130,6 +131,7 @@ static struct zwp_keyboard_shortcuts_inhibit_manager_v1 *kb_inhibit_mgr;
static struct zwlr_output_power_manager_v1 *power_mgr;
static struct zwp_virtual_keyboard_manager_v1 *virtual_kb_mgr;
static struct zwlr_virtual_pointer_manager_v1 *virtual_ptr_mgr;
static struct xdg_activation_v1 *activation_mgr;
static struct wl_output *wl_output;
struct foreign_state {
@ -774,6 +776,11 @@ static void registry_global(void *data, struct wl_registry *reg, uint32_t name,
virtual_ptr_mgr = wl_registry_bind(reg, name,
&zwlr_virtual_pointer_manager_v1_interface,
version < 1 ? version : 1);
} else if (strcmp(interface,
xdg_activation_v1_interface.name) == 0) {
activation_mgr = wl_registry_bind(reg, name,
&xdg_activation_v1_interface,
version < 1 ? version : 1);
}
}
@ -1574,6 +1581,144 @@ static void virtual_motion_run(void) {
wl_display_roundtrip(display);
}
/* --- Xdg-activation (xdg-activation-v1) ---
--activate [--activate-out <file>]: prove a launcher can focus an existing
inactive window by token. Opens win0 (maps focused), then win1 (maps and
steals focus so win0 goes inactive), fetches a token, activates win0's
surface, and writes "activated" once win0 receives an activated configure.
Exits after. */
static bool activate_mode;
static const char *activate_out;
static struct wl_surface *act_surf[2];
static struct xdg_surface *act_xdg[2];
static struct xdg_toplevel *act_top[2];
static struct wl_buffer *act_buf[2];
static bool act_active[2];
static char act_token[256];
static bool act_got_token;
static void act_note(const char *s) {
if (activate_out) write_outfile(activate_out, s, strlen(s));
}
static void act_make_buf(int i) {
if (act_buf[i]) return;
int W_ = 64, H_ = 64, stride = W_ * 4;
size_t size = (size_t)stride * (size_t)H_;
int fd;
struct wl_shm_pool *pool = make_pool(size, &fd);
if (!pool) return;
uint8_t *d = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (d != MAP_FAILED) {
memset(d, 0x80, size);
munmap(d, size);
}
close(fd);
act_buf[i] = wl_shm_pool_create_buffer(pool, 0, W_, H_, stride,
WL_SHM_FORMAT_XRGB8888);
wl_shm_pool_destroy(pool);
}
static void act_xdg_configure(void *data, struct xdg_surface *xs,
uint32_t serial) {
int i = (int)(intptr_t)data;
xdg_surface_ack_configure(xs, serial);
act_make_buf(i);
wl_surface_attach(act_surf[i], act_buf[i], 0, 0);
wl_surface_commit(act_surf[i]);
}
static const struct xdg_surface_listener act_xdg_listener = {
.configure = act_xdg_configure,
};
static void act_top_configure(void *data, struct xdg_toplevel *xp, int32_t w,
int32_t h, struct wl_array *states) {
(void)xp; (void)w; (void)h;
int i = (int)(intptr_t)data;
act_active[i] = false;
for (const uint32_t *s = states->data;
s < (const uint32_t *)((const char *)states->data + states->size);
s++) {
if (*s == (uint32_t)XDG_TOPLEVEL_STATE_ACTIVATED) {
act_active[i] = true;
break;
}
}
}
static const struct xdg_toplevel_listener act_top_listener = {
.configure = act_top_configure,
.close = xdg_toplevel_close,
};
static void act_token_done(void *data,
struct xdg_activation_token_v1 *tok,
const char *token) {
(void)data; (void)tok;
snprintf(act_token, sizeof(act_token), "%s", token ? token : "");
act_got_token = true;
}
static const struct xdg_activation_token_v1_listener act_token_listener = {
.done = act_token_done,
};
static void act_open(int i, const char *title) {
act_surf[i] = wl_compositor_create_surface(compositor);
act_xdg[i] = xdg_wm_base_get_xdg_surface(wm_base, act_surf[i]);
act_top[i] = xdg_surface_get_toplevel(act_xdg[i]);
xdg_toplevel_set_title(act_top[i], title);
xdg_surface_add_listener(act_xdg[i], &act_xdg_listener,
(void *)(intptr_t)i);
xdg_toplevel_add_listener(act_top[i], &act_top_listener,
(void *)(intptr_t)i);
wl_surface_commit(act_surf[i]);
}
static void activate_run(void) {
display = wl_display_connect(NULL);
if (!display) {
fprintf(stderr, "wltest: activate: cannot connect\n");
exit(1);
}
struct wl_registry *reg = wl_display_get_registry(display);
wl_registry_add_listener(reg, &registry_listener, NULL);
wl_display_roundtrip(display);
if (!activation_mgr) {
fprintf(stderr, "wltest: activate: xdg_activation_v1 missing\n");
exit(1);
}
xdg_wm_base_add_listener(wm_base, &wm_base_listener, NULL);
/* win0 maps focused; win1 maps and steals focus so win0 is inactive. */
act_open(0, "act0");
wl_display_roundtrip(display);
act_open(1, "act1");
wl_display_roundtrip(display);
/* Fetch a token (app_id hint only; no input serial needed). */
struct xdg_activation_token_v1 *tok =
xdg_activation_v1_get_activation_token(activation_mgr);
xdg_activation_token_v1_add_listener(tok, &act_token_listener, NULL);
xdg_activation_token_v1_set_app_id(tok, "wltest-act");
xdg_activation_token_v1_commit(tok);
while (!act_got_token) {
if (wl_display_dispatch(display) < 0) {
fprintf(stderr, "wltest: activate: no token\n");
exit(1);
}
}
/* Activate the inactive win0; wait until it reports activated. */
xdg_activation_v1_activate(activation_mgr, act_token, act_surf[0]);
for (;;) {
if (wl_display_dispatch(display) < 0)
break;
if (act_active[0]) {
act_note("activated");
break;
}
}
}
/* Pull the current offer and act on it. In --primary-demand mode write the
payload to the file and exit; in --primary-demand-expect mode keep waiting
until the payload matches the expected text (the host may already hold its
@ -1913,10 +2058,25 @@ int main(int argc, char **argv) {
} else if (strcmp(argv[i], "--virtual-click") == 0) {
virtual_click = true;
i += 1;
} else if (strcmp(argv[i], "--activate") == 0) {
activate_mode = true;
i += 1;
} else if (strcmp(argv[i], "--activate-out") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --activate-out needs a path\n");
return 1;
}
activate_out = argv[i + 1];
i += 2;
} else {
break;
}
}
/* Xdg-activation: focus an inactive window by token, then exit. */
if (activate_mode) {
activate_run();
return 0;
}
/* Virtual keyboard: synthesise keys, then exit. */
if (virtual_key_spec) {
virtual_key_run();