- wlr-foreign-toplevel-management-v1: per-window handles for taskbars with activate/close/maximize/fullscreen requests - ext-foreign-toplevel-list-v1: compositor-pushed handle list - ext-idle-notify-v1: client-requested idle notifications - idle-inhibit-v1: suppress idle while a surface is visible - wltest --foreign mode (taskbar client, state logging, --foreign-close) - wltest --idle mode (idle notification + inhibitor testing) - 5 smoke tests: globals, state roundtrip, close, idle notify, idle inhibit
1500 lines
No EOL
58 KiB
C
1500 lines
No EOL
58 KiB
C
#define _GNU_SOURCE
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <poll.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/mman.h>
|
|
#include <unistd.h>
|
|
|
|
#include <sys/shm.h>
|
|
#include <wayland-client.h>
|
|
#include "xdg-shell-client.h"
|
|
#include "cursor-shape-v1-client.h"
|
|
#include "pointer-constraints-unstable-v1-client.h"
|
|
#include "primary-selection-unstable-v1-client.h"
|
|
#include "relative-pointer-unstable-v1-client.h"
|
|
#include "wlr-layer-shell-unstable-v1-client.h"
|
|
#include "wlr-foreign-toplevel-management-client.h"
|
|
#include "ext-foreign-toplevel-list-v1-client.h"
|
|
#include "ext-idle-notify-v1-client.h"
|
|
#include "idle-inhibit-unstable-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
|
|
visually distinct, cheaply-backed tabs (a SIGUSR1 scene dump can then
|
|
assert which tab is actually displayed). */
|
|
|
|
static struct wl_display *display;
|
|
static struct wl_compositor *compositor;
|
|
static struct wl_shm *shm;
|
|
static struct xdg_wm_base *wm_base;
|
|
static struct wl_surface *surface;
|
|
static struct xdg_surface *xdg_surface;
|
|
static struct xdg_toplevel *toplevel;
|
|
static struct wl_buffer *buffer;
|
|
static struct wl_seat *seat;
|
|
|
|
/* --globals-out <path>: write the advertised global interface names to a
|
|
file (one per line) and exit, instead of showing a toplevel. The file
|
|
escapes kick's CLI-tab stdout redirection to /dev/null, letting tests
|
|
assert which protocols the standalone compositor advertises. */
|
|
static const char *globals_out;
|
|
|
|
/* --constrain {locked|confined} [+ --confine-rect X Y W H]: bind
|
|
pointer-constraints-v1 and relative-pointer-v1 and request a constraint
|
|
(with the given confining region) on the toplevel after it maps. */
|
|
enum { NO_CONSTRAINT, CONSTRAINT_LOCKED, CONSTRAINT_CONFINED };
|
|
static int constrain_mode;
|
|
static int confine_rect[4];
|
|
static bool have_confine_rect;
|
|
static struct zwp_pointer_constraints_v1 *pc_mgr;
|
|
static struct zwp_relative_pointer_manager_v1 *rel_mgr;
|
|
static struct zwp_relative_pointer_v1 *rel_ptr;
|
|
static struct zwp_locked_pointer_v1 *locked_ptr;
|
|
static struct zwp_confined_pointer_v1 *confined_ptr;
|
|
static struct wl_keyboard *keyboard;
|
|
static struct wl_pointer *pointer;
|
|
/* --touch-log: append every received touch event to this file so tests can
|
|
prove the compositor delivered touchscreen input end-to-end. */
|
|
static FILE *touch_log;
|
|
static struct wl_touch *touch;
|
|
|
|
static struct wp_cursor_shape_manager_v1 *shape_mgr;
|
|
static struct wp_cursor_shape_device_v1 *shape_dev;
|
|
static uint32_t cursor_shape; /* --cursor-shape value, 0 = none requested */
|
|
static bool shape_sent;
|
|
static bool fullscreen;
|
|
|
|
/* Primary selection: --primary-set <text> installs a source on the seat;
|
|
--primary-demand <file> pulls the first "text/plain" offer's content and
|
|
exits; --primary-demand-expect <file> <text> keeps pulling until the
|
|
payload matches <text> (the host may already hold its own primary at boot,
|
|
so a nested test must key on content, not on the first offer). Together
|
|
they exercise the zwp_primary_selection_device_manager_v1 round trip. */
|
|
static const char *primary_text;
|
|
static const char *primary_out;
|
|
static const char *primary_expect;
|
|
static struct zwp_primary_selection_device_manager_v1 *ps_mgr;
|
|
static struct zwp_primary_selection_device_v1 *ps_dev;
|
|
static struct zwp_primary_selection_offer_v1 *ps_offer;
|
|
static bool ps_need_pull;
|
|
static bool ps_set_done;
|
|
static const struct zwp_primary_selection_source_v1_listener ps_source_listener;
|
|
|
|
/* --layer mode: create a zwlr_layer_surface_v1 instead of an xdg toplevel.
|
|
This lets the smoke tests exercise the compositor's layer-shell work-area
|
|
computation (exclusive-zone vs zero-exclusive-zone) without depending on
|
|
an external panel. */
|
|
static bool layer_mode;
|
|
static struct zwlr_layer_shell_v1 *layer_mgr;
|
|
static struct zwlr_layer_surface_v1 *layer_surface;
|
|
static uint32_t layer_id = ZWLR_LAYER_SHELL_V1_LAYER_TOP;
|
|
static uint32_t layer_anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP;
|
|
static int32_t layer_exclusive;
|
|
static int layer_w, layer_h;
|
|
|
|
/* --foreign [--foreign-out <file>] [+ --foreign-close <app_id>]: act as the
|
|
taskbar side of the compositor. Binds wlr-foreign-toplevel-management and
|
|
ext-foreign-toplevel-list, logs every window's (app_id, title, activated,
|
|
maximized, fullscreen) to --foreign-out (one line per window, rewritten on
|
|
state changes), and when --foreign-close names an app_id sends a close
|
|
request for that window. Lets the smoke tests prove the compositor
|
|
advertises both globals, mirrors window state onto per-window handles, and
|
|
honors a taskbar's request_close from a DIFFERENT client than the window. */
|
|
static bool foreign_mode;
|
|
static const char *foreign_out;
|
|
static const char *foreign_close_app;
|
|
static bool foreign_sent_close_collect; /* have we seen the target window */
|
|
static FILE *foreign_fp;
|
|
static struct zwlr_foreign_toplevel_manager_v1 *ft_mgr;
|
|
static struct ext_foreign_toplevel_list_v1 *ext_list;
|
|
static struct ext_idle_notifier_v1 *idle_mgr;
|
|
static struct zwp_idle_inhibit_manager_v1 *inhib_mgr;
|
|
|
|
struct foreign_state {
|
|
char app_id[64];
|
|
char title[128];
|
|
bool activated, maximized, fullscreen;
|
|
};
|
|
|
|
static void foreign_write(struct foreign_state *st);
|
|
|
|
/* Rewrite the target handle's line in --foreign-out on every change. We key
|
|
on the trailing window list: because kick only ever has a couple of windows
|
|
in a test, a single tracked handle is enough. */
|
|
static struct foreign_base {
|
|
struct zwlr_foreign_toplevel_handle_v1 *handle;
|
|
struct ext_foreign_toplevel_handle_v1 *ext_handle;
|
|
struct foreign_state st;
|
|
bool ft_have, ext_have;
|
|
} foreign_cur;
|
|
|
|
/* --- wlr-foreign-toplevel management callbacks --- */
|
|
static void fth_title(void *data, struct zwlr_foreign_toplevel_handle_v1 *h,
|
|
const char *title) {
|
|
(void)data; (void)h;
|
|
snprintf(foreign_cur.st.title, sizeof(foreign_cur.st.title), "%s", title);
|
|
}
|
|
/* Issue the --foreign-close once the tracked handle's app_id matches the
|
|
target (the app_id arrives after on_toplevel, so this must be keyed off
|
|
the app_id event, not off the on_toplevel callback). */
|
|
static void maybe_foreign_close(void) {
|
|
if (foreign_close_app && !foreign_sent_close_collect && foreign_cur.handle &&
|
|
strcmp(foreign_cur.st.app_id, foreign_close_app) == 0) {
|
|
foreign_sent_close_collect = true;
|
|
zwlr_foreign_toplevel_handle_v1_close(foreign_cur.handle);
|
|
}
|
|
}
|
|
|
|
static void fth_app_id(void *data, struct zwlr_foreign_toplevel_handle_v1 *h,
|
|
const char *app_id) {
|
|
(void)data; (void)h;
|
|
snprintf(foreign_cur.st.app_id, sizeof(foreign_cur.st.app_id), "%s", app_id);
|
|
maybe_foreign_close();
|
|
}
|
|
static void fth_output_enter(void *data, struct zwlr_foreign_toplevel_handle_v1 *h,
|
|
struct wl_output *output) {
|
|
(void)data; (void)h; (void)output;
|
|
}
|
|
static void fth_output_leave(void *data, struct zwlr_foreign_toplevel_handle_v1 *h,
|
|
struct wl_output *output) {
|
|
(void)data; (void)h; (void)output;
|
|
}
|
|
static void fth_state(void *data, struct zwlr_foreign_toplevel_handle_v1 *h,
|
|
struct wl_array *state) {
|
|
(void)data; (void)h;
|
|
foreign_cur.st.activated = foreign_cur.st.maximized =
|
|
foreign_cur.st.fullscreen = false;
|
|
for (const uint32_t *s = state->data;
|
|
s < (const uint32_t *)((char *)state->data + state->size); s++) {
|
|
if (*s == ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_ACTIVATED)
|
|
foreign_cur.st.activated = true;
|
|
else if (*s == ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MAXIMIZED)
|
|
foreign_cur.st.maximized = true;
|
|
else if (*s == ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_FULLSCREEN)
|
|
foreign_cur.st.fullscreen = true;
|
|
}
|
|
}
|
|
static void fth_done(void *data, struct zwlr_foreign_toplevel_handle_v1 *h) {
|
|
(void)data; (void)h;
|
|
foreign_cur.ft_have = true;
|
|
foreign_write(&foreign_cur.st);
|
|
}
|
|
static void fth_closed(void *data, struct zwlr_foreign_toplevel_handle_v1 *h) {
|
|
(void)data; (void)h;
|
|
foreign_cur.handle = NULL;
|
|
/* The window went away: if we had to close a window the taskbar itself
|
|
now has nothing to track, and we can prove the round trip finished. */
|
|
}
|
|
static void fth_parent(void *data, struct zwlr_foreign_toplevel_handle_v1 *h,
|
|
struct zwlr_foreign_toplevel_handle_v1 *parent) {
|
|
(void)data; (void)h; (void)parent;
|
|
}
|
|
static const struct zwlr_foreign_toplevel_handle_v1_listener fth_listener = {
|
|
.title = fth_title,
|
|
.app_id = fth_app_id,
|
|
.output_enter = fth_output_enter,
|
|
.output_leave = fth_output_leave,
|
|
.state = fth_state,
|
|
.done = fth_done,
|
|
.closed = fth_closed,
|
|
.parent = fth_parent,
|
|
};
|
|
static void ftm_toplevel(void *data, struct zwlr_foreign_toplevel_manager_v1 *m,
|
|
struct zwlr_foreign_toplevel_handle_v1 *h) {
|
|
(void)data; (void)m;
|
|
foreign_cur.handle = h;
|
|
zwlr_foreign_toplevel_handle_v1_add_listener(h, &fth_listener, NULL);
|
|
}
|
|
static void ftm_finished(void *data, struct zwlr_foreign_toplevel_manager_v1 *m) {
|
|
(void)data; (void)m;
|
|
}
|
|
static const struct zwlr_foreign_toplevel_manager_v1_listener ftm_listener = {
|
|
.toplevel = ftm_toplevel,
|
|
.finished = ftm_finished,
|
|
};
|
|
|
|
/* --- ext-foreign-toplevel-list callbacks (compositor-pushed) --- */
|
|
static void exth_closed(void *data, struct ext_foreign_toplevel_handle_v1 *h) {
|
|
(void)data; (void)h;
|
|
foreign_cur.ext_handle = NULL;
|
|
}
|
|
static void exth_done(void *data, struct ext_foreign_toplevel_handle_v1 *h) {
|
|
(void)data; (void)h;
|
|
foreign_cur.ext_have = true;
|
|
foreign_write(&foreign_cur.st);
|
|
}
|
|
static void exth_title(void *data, struct ext_foreign_toplevel_handle_v1 *h,
|
|
const char *title) {
|
|
(void)data; (void)h;
|
|
snprintf(foreign_cur.st.title, sizeof(foreign_cur.st.title), "%s", title);
|
|
}
|
|
static void exth_app_id(void *data, struct ext_foreign_toplevel_handle_v1 *h,
|
|
const char *app_id) {
|
|
(void)data; (void)h;
|
|
snprintf(foreign_cur.st.app_id, sizeof(foreign_cur.st.app_id), "%s", app_id);
|
|
}
|
|
static void exth_identifier(void *data, struct ext_foreign_toplevel_handle_v1 *h,
|
|
const char *identifier) {
|
|
(void)data; (void)h; (void)identifier;
|
|
}
|
|
static const struct ext_foreign_toplevel_handle_v1_listener exth_listener = {
|
|
.closed = exth_closed,
|
|
.done = exth_done,
|
|
.title = exth_title,
|
|
.app_id = exth_app_id,
|
|
.identifier = exth_identifier,
|
|
};
|
|
static void extl_toplevel(void *data, struct ext_foreign_toplevel_list_v1 *l,
|
|
struct ext_foreign_toplevel_handle_v1 *h) {
|
|
(void)data; (void)l;
|
|
foreign_cur.ext_handle = h;
|
|
ext_foreign_toplevel_handle_v1_add_listener(h, &exth_listener, NULL);
|
|
}
|
|
static void extl_finished(void *data, struct ext_foreign_toplevel_list_v1 *l) {
|
|
(void)data; (void)l;
|
|
}
|
|
static const struct ext_foreign_toplevel_list_v1_listener extl_listener = {
|
|
.toplevel = extl_toplevel,
|
|
.finished = extl_finished,
|
|
};
|
|
|
|
/* Write (one line, re-created each change so the suite scopes the file) the
|
|
observed window and its state to --foreign-out. */
|
|
static void foreign_write(struct foreign_state *st) {
|
|
if (!foreign_fp) return;
|
|
ftruncate(fileno(foreign_fp), 0);
|
|
rewind(foreign_fp);
|
|
fprintf(foreign_fp,
|
|
"app_id=%s title=%s activated=%d maximized=%d fullscreen=%d\n",
|
|
st->app_id, st->title,
|
|
(int)st->activated, (int)st->maximized, (int)st->fullscreen);
|
|
fflush(foreign_fp);
|
|
}
|
|
|
|
static void foreign_run(void);
|
|
static const struct wl_registry_listener registry_listener;
|
|
static void foreign_run(void) {
|
|
display = wl_display_connect(NULL);
|
|
if (!display) {
|
|
fprintf(stderr, "wltest: foreign: cannot connect\n");
|
|
exit(1);
|
|
}
|
|
struct wl_registry *reg = wl_display_get_registry(display);
|
|
wl_registry_add_listener(reg, ®istry_listener, NULL);
|
|
wl_display_roundtrip(display);
|
|
if (!ft_mgr || !ext_list) {
|
|
fprintf(stderr, "wltest: foreign: manager/list globals missing\n");
|
|
exit(1);
|
|
}
|
|
zwlr_foreign_toplevel_manager_v1_add_listener(ft_mgr, &ftm_listener, NULL);
|
|
ext_foreign_toplevel_list_v1_add_listener(ext_list, &extl_listener, NULL);
|
|
if (foreign_out) {
|
|
foreign_fp = fopen(foreign_out, "w");
|
|
if (!foreign_fp) {
|
|
fprintf(stderr, "wltest: foreign: cannot write %s\n", foreign_out);
|
|
exit(1);
|
|
}
|
|
}
|
|
for (;;) {
|
|
if (wl_display_dispatch(display) < 0)
|
|
break;
|
|
}
|
|
if (foreign_fp) fclose(foreign_fp);
|
|
}
|
|
|
|
/* A primary-selection set_selection serial must be one the seat actually
|
|
gave THIS client (wlr_seat_client_validate_event_serial), so the setter
|
|
cannot hammer a byzantine constant. Defer the request until the first
|
|
keyboard/pointer event serial arrives, then use it. */
|
|
static void maybe_set_primary(uint32_t serial) {
|
|
if (!primary_text || ps_set_done || !ps_dev)
|
|
return;
|
|
ps_set_done = true;
|
|
struct zwp_primary_selection_source_v1 *s =
|
|
zwp_primary_selection_device_manager_v1_create_source(ps_mgr);
|
|
zwp_primary_selection_source_v1_offer(s, "text/plain;charset=utf-8");
|
|
zwp_primary_selection_source_v1_offer(s, "text/plain");
|
|
zwp_primary_selection_source_v1_add_listener(s, &ps_source_listener, NULL);
|
|
zwp_primary_selection_device_v1_set_selection(ps_dev, s, serial);
|
|
wl_display_flush(display);
|
|
}
|
|
|
|
static int W = 1280, H = 800;
|
|
static uint8_t PIXEL[4] = { 0xff, 0x00, 0x00, 0xff }; /* RGBA */
|
|
|
|
/* Optional second toplevel from the SAME client (--extra <hex>): lets the
|
|
smoke tests exercise the compositor's sub-window handling (a second
|
|
toplevel of an already-structured tab gets its own slot glued right after
|
|
its parent). */
|
|
static bool want_extra;
|
|
static bool extra_done;
|
|
static uint8_t EXTRA_PIXEL[4] = { 0x00, 0xff, 0x00, 0xff };
|
|
static int EX_W = 640, EX_H = 400;
|
|
static struct wl_surface *ex_surface;
|
|
static struct xdg_surface *ex_xdg;
|
|
static struct xdg_toplevel *ex_toplevel;
|
|
static struct wl_buffer *ex_buffer;
|
|
|
|
static void create_buffer(void);
|
|
static void extra_open(void);
|
|
static void request_constraint(void);
|
|
|
|
static void locked_locked(void *data, struct zwp_locked_pointer_v1 *lp) {
|
|
(void)data; (void)lp;
|
|
}
|
|
static void locked_unlocked(void *data, struct zwp_locked_pointer_v1 *lp) {
|
|
(void)data; (void)lp;
|
|
}
|
|
static void confined_confined(void *data, struct zwp_confined_pointer_v1 *cp) {
|
|
(void)data; (void)cp;
|
|
}
|
|
static void confined_unconfined(void *data, struct zwp_confined_pointer_v1 *cp) {
|
|
(void)data; (void)cp;
|
|
}
|
|
static const struct zwp_locked_pointer_v1_listener locked_listener = {
|
|
.locked = locked_locked,
|
|
.unlocked = locked_unlocked,
|
|
};
|
|
static const struct zwp_confined_pointer_v1_listener confined_listener = {
|
|
.confined = confined_confined,
|
|
.unconfined = confined_unconfined,
|
|
};
|
|
|
|
/* Keep the constraint request itself nullable: the compositor decides
|
|
whether to activate it (pointer must be over the surface). */
|
|
static void request_constraint(void) {
|
|
if (constrain_mode == CONSTRAINT_LOCKED && pc_mgr && pointer) {
|
|
locked_ptr = zwp_pointer_constraints_v1_lock_pointer(
|
|
pc_mgr, surface, pointer, NULL,
|
|
ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT);
|
|
if (locked_ptr)
|
|
zwp_locked_pointer_v1_add_listener(locked_ptr, &locked_listener,
|
|
NULL);
|
|
} else if (constrain_mode == CONSTRAINT_CONFINED && pc_mgr && pointer) {
|
|
confined_ptr = zwp_pointer_constraints_v1_confine_pointer(
|
|
pc_mgr, surface, pointer, NULL,
|
|
ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT);
|
|
if (confined_ptr) {
|
|
if (have_confine_rect) {
|
|
struct wl_region *r = wl_compositor_create_region(compositor);
|
|
wl_region_add(r, confine_rect[0], confine_rect[1],
|
|
confine_rect[2], confine_rect[3]);
|
|
zwp_confined_pointer_v1_set_region(confined_ptr, r);
|
|
wl_region_destroy(r);
|
|
}
|
|
zwp_confined_pointer_v1_add_listener(confined_ptr,
|
|
&confined_listener, NULL);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void keyboard_keymap(void *data, struct wl_keyboard *kb,
|
|
uint32_t format, int32_t fd, uint32_t size) {
|
|
(void)data; (void)kb; (void)format; (void)size;
|
|
close(fd);
|
|
}
|
|
|
|
static void keyboard_key(void *data, struct wl_keyboard *kb, uint32_t serial,
|
|
uint32_t time, uint32_t key, uint32_t state) {
|
|
(void)data; (void)kb; (void)time;
|
|
/* 'F' (evdev 33) toggles fullscreen, giving the smoke tests a real
|
|
client-initiated fullscreen request to feed the compositor. */
|
|
if (key == 33 && state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
|
fullscreen = !fullscreen;
|
|
if (fullscreen)
|
|
xdg_toplevel_set_fullscreen(toplevel, NULL);
|
|
else
|
|
xdg_toplevel_unset_fullscreen(toplevel);
|
|
}
|
|
/* 'X' (evdev 45) closes the --extra toplevel, letting the smoke tests
|
|
exercise a stacked sub-window going away while its tab lives on. */
|
|
if (key == 45 && state == WL_KEYBOARD_KEY_STATE_PRESSED &&
|
|
ex_toplevel) {
|
|
xdg_toplevel_destroy(ex_toplevel);
|
|
xdg_surface_destroy(ex_xdg);
|
|
wl_surface_destroy(ex_surface);
|
|
ex_toplevel = NULL;
|
|
ex_xdg = NULL;
|
|
ex_surface = NULL;
|
|
}
|
|
if (state == WL_KEYBOARD_KEY_STATE_PRESSED)
|
|
maybe_set_primary(serial);
|
|
}
|
|
|
|
static void keyboard_enter(void *data, struct wl_keyboard *kb, uint32_t serial,
|
|
struct wl_surface *srf, struct wl_array *keys) {
|
|
(void)data; (void)kb; (void)srf; (void)keys;
|
|
maybe_set_primary(serial);
|
|
}
|
|
|
|
static void keyboard_leave(void *data, struct wl_keyboard *kb, uint32_t serial,
|
|
struct wl_surface *srf) {
|
|
(void)data; (void)kb; (void)serial; (void)srf;
|
|
}
|
|
|
|
static void keyboard_modifiers(void *data, struct wl_keyboard *kb,
|
|
uint32_t serial, uint32_t mods_depressed,
|
|
uint32_t mods_latched, uint32_t mods_locked,
|
|
uint32_t group) {
|
|
(void)data; (void)kb; (void)serial; (void)mods_depressed;
|
|
(void)mods_latched; (void)mods_locked; (void)group;
|
|
}
|
|
|
|
static void keyboard_repeat_info(void *data, struct wl_keyboard *kb,
|
|
int32_t rate, int32_t delay) {
|
|
(void)data; (void)kb; (void)rate; (void)delay;
|
|
}
|
|
|
|
static const struct wl_keyboard_listener keyboard_listener = {
|
|
.keymap = keyboard_keymap,
|
|
.enter = keyboard_enter,
|
|
.leave = keyboard_leave,
|
|
.key = keyboard_key,
|
|
.modifiers = keyboard_modifiers,
|
|
.repeat_info = keyboard_repeat_info,
|
|
};
|
|
|
|
/* When --cursor-shape is given, request that shape on the first pointer
|
|
enter (like a real toolkit would), then leave it in place. The compositor
|
|
under test is expected to apply it (standalone) or forward it (nested). */
|
|
static void pointer_enter(void *data, struct wl_pointer *p, uint32_t serial,
|
|
struct wl_surface *srf, wl_fixed_t sx, wl_fixed_t sy) {
|
|
(void)data; (void)p; (void)srf; (void)sx; (void)sy;
|
|
maybe_set_primary(serial);
|
|
/* wl_display_dispatch does not flush queued requests, so push the shape
|
|
out immediately; otherwise the compositor only sees it after the next
|
|
roundtrip the client happens to do. */
|
|
if (cursor_shape && !shape_sent && shape_dev) {
|
|
wp_cursor_shape_device_v1_set_shape(shape_dev, serial, cursor_shape);
|
|
shape_sent = true;
|
|
wl_display_flush(display);
|
|
}
|
|
}
|
|
|
|
static void pointer_leave(void *data, struct wl_pointer *p, uint32_t serial,
|
|
struct wl_surface *srf) {
|
|
(void)data; (void)p; (void)serial; (void)srf;
|
|
}
|
|
|
|
static void pointer_motion(void *data, struct wl_pointer *p, uint32_t time,
|
|
wl_fixed_t sx, wl_fixed_t sy) {
|
|
(void)data; (void)p; (void)time; (void)sx; (void)sy;
|
|
}
|
|
|
|
static void pointer_frame(void *data, struct wl_pointer *p) {
|
|
(void)data; (void)p;
|
|
}
|
|
|
|
static void pointer_button(void *data, struct wl_pointer *p, uint32_t serial,
|
|
uint32_t time, uint32_t button, uint32_t state) {
|
|
(void)data; (void)p; (void)serial; (void)time; (void)button; (void)state;
|
|
}
|
|
|
|
static void pointer_axis(void *data, struct wl_pointer *p, uint32_t time,
|
|
uint32_t axis, wl_fixed_t value) {
|
|
(void)data; (void)p; (void)time; (void)axis; (void)value;
|
|
}
|
|
|
|
static void touch_log_line(const char *kind, uint32_t time, int32_t id,
|
|
wl_fixed_t sx, wl_fixed_t sy) {
|
|
if (!touch_log) return;
|
|
fprintf(touch_log, "%s %u %d %d %d\n", kind, time, (int)id,
|
|
(int)wl_fixed_to_double(sx), (int)wl_fixed_to_double(sy));
|
|
fflush(touch_log);
|
|
}
|
|
|
|
static void touch_down(void *data, struct wl_touch *t, uint32_t serial,
|
|
uint32_t time, struct wl_surface *surface,
|
|
int32_t id, wl_fixed_t sx, wl_fixed_t sy) {
|
|
(void)data; (void)t; (void)serial; (void)surface;
|
|
touch_log_line("down", time, id, sx, sy);
|
|
}
|
|
|
|
static void touch_motion(void *data, struct wl_touch *t, uint32_t time,
|
|
int32_t id, wl_fixed_t sx, wl_fixed_t sy) {
|
|
(void)data; (void)t;
|
|
touch_log_line("motion", time, id, sx, sy);
|
|
}
|
|
|
|
static void touch_up(void *data, struct wl_touch *t, uint32_t serial,
|
|
uint32_t time, int32_t id) {
|
|
(void)data; (void)t; (void)serial;
|
|
touch_log_line("up", time, id, 0, 0);
|
|
}
|
|
|
|
static void touch_frame(void *data, struct wl_touch *t) {
|
|
(void)data; (void)t;
|
|
}
|
|
|
|
static void touch_cancel(void *data, struct wl_touch *t) {
|
|
(void)data; (void)t;
|
|
if (touch_log) {
|
|
fprintf(touch_log, "cancel\n");
|
|
fflush(touch_log);
|
|
}
|
|
}
|
|
|
|
static const struct wl_touch_listener touch_listener = {
|
|
.down = touch_down,
|
|
.motion = touch_motion,
|
|
.up = touch_up,
|
|
.frame = touch_frame,
|
|
.cancel = touch_cancel,
|
|
};
|
|
|
|
static const struct wl_pointer_listener pointer_listener = {
|
|
.enter = pointer_enter,
|
|
.leave = pointer_leave,
|
|
.motion = pointer_motion,
|
|
.button = pointer_button,
|
|
.axis = pointer_axis,
|
|
.frame = pointer_frame,
|
|
};
|
|
|
|
static void xdg_wm_base_ping(void *data, struct xdg_wm_base *wm, uint32_t serial) {
|
|
(void)data;
|
|
xdg_wm_base_pong(wm, serial);
|
|
}
|
|
|
|
static const struct xdg_wm_base_listener wm_base_listener = {
|
|
.ping = xdg_wm_base_ping,
|
|
};
|
|
|
|
static void xdg_surface_configure(void *data, struct xdg_surface *xs, uint32_t serial) {
|
|
(void)data;
|
|
xdg_surface_ack_configure(xs, serial);
|
|
create_buffer();
|
|
wl_surface_attach(surface, buffer, 0, 0);
|
|
wl_surface_damage_buffer(surface, 0, 0, W, H);
|
|
wl_surface_commit(surface);
|
|
if (want_extra && !extra_done) {
|
|
extra_done = true;
|
|
extra_open();
|
|
}
|
|
}
|
|
|
|
static const struct xdg_surface_listener xdg_surface_listener = {
|
|
.configure = xdg_surface_configure,
|
|
};
|
|
|
|
static void layer_surface_configure(void *data,
|
|
struct zwlr_layer_surface_v1 *ls, uint32_t serial,
|
|
uint32_t w, uint32_t h) {
|
|
(void)data;
|
|
zwlr_layer_surface_v1_ack_configure(ls, serial);
|
|
if (w > 0 && h > 0) { W = (int)w; H = (int)h; }
|
|
else if (layer_w > 0 && layer_h > 0) { W = layer_w; H = layer_h; }
|
|
create_buffer();
|
|
wl_surface_attach(surface, buffer, 0, 0);
|
|
wl_surface_damage_buffer(surface, 0, 0, W, H);
|
|
wl_surface_commit(surface);
|
|
}
|
|
|
|
static void layer_surface_closed(void *data,
|
|
struct zwlr_layer_surface_v1 *ls) {
|
|
(void)data; (void)ls;
|
|
exit(0);
|
|
}
|
|
|
|
static const struct zwlr_layer_surface_v1_listener layer_listener = {
|
|
.configure = layer_surface_configure,
|
|
.closed = layer_surface_closed,
|
|
};
|
|
|
|
static void xdg_toplevel_configure(void *data, struct xdg_toplevel *xp, int32_t w,
|
|
int32_t h, struct wl_array *states) {
|
|
(void)data; (void)xp; (void)states;
|
|
if (w > 0 && h > 0) { W = w; H = h; }
|
|
}
|
|
|
|
static void xdg_toplevel_close(void *data, struct xdg_toplevel *xp) {
|
|
(void)data;
|
|
/* Closing the extra (second, tab-group) toplevel destroys just that
|
|
window so the client (and its main window) survive; closing the main
|
|
toplevel exits the client. The extra must be torn down completely
|
|
(toplevel + xdg_surface + surface), like the 'X' key path: leaving the
|
|
surface behind lets a late/queued configure ack hit a dead serial and
|
|
kill the whole connection. */
|
|
if (xp == ex_toplevel) {
|
|
if (ex_toplevel) {
|
|
xdg_toplevel_destroy(ex_toplevel);
|
|
xdg_surface_destroy(ex_xdg);
|
|
wl_surface_destroy(ex_surface);
|
|
ex_toplevel = NULL;
|
|
ex_xdg = NULL;
|
|
ex_surface = NULL;
|
|
}
|
|
return;
|
|
}
|
|
exit(0);
|
|
}
|
|
|
|
static const struct xdg_toplevel_listener toplevel_listener = {
|
|
.configure = xdg_toplevel_configure,
|
|
.close = xdg_toplevel_close,
|
|
};
|
|
|
|
static void registry_global(void *data, struct wl_registry *reg, uint32_t name,
|
|
const char *interface, uint32_t version) {
|
|
(void)data;
|
|
if (globals_out) {
|
|
/* Collecting mode: just remember names; they are written out after
|
|
the first roundtrip completes. */
|
|
FILE *f = fopen(globals_out, "a");
|
|
if (f) {
|
|
fprintf(f, "%s %u\n", interface, version);
|
|
fclose(f);
|
|
}
|
|
return;
|
|
}
|
|
if (strcmp(interface, wl_compositor_interface.name) == 0) {
|
|
compositor = wl_registry_bind(reg, name, &wl_compositor_interface,
|
|
version < 4 ? version : 4);
|
|
} else if (strcmp(interface, wl_shm_interface.name) == 0) {
|
|
shm = wl_registry_bind(reg, name, &wl_shm_interface,
|
|
version < 1 ? version : 1);
|
|
} else if (strcmp(interface, xdg_wm_base_interface.name) == 0) {
|
|
wm_base = wl_registry_bind(reg, name, &xdg_wm_base_interface,
|
|
version < 1 ? version : 1);
|
|
} else if (strcmp(interface, wl_seat_interface.name) == 0) {
|
|
seat = wl_registry_bind(reg, name, &wl_seat_interface,
|
|
version < 5 ? version : 5);
|
|
} else if (strcmp(interface,
|
|
wp_cursor_shape_manager_v1_interface.name) == 0) {
|
|
shape_mgr = wl_registry_bind(reg, name,
|
|
&wp_cursor_shape_manager_v1_interface,
|
|
version < 1 ? version : 1);
|
|
} else if (strcmp(interface,
|
|
zwp_pointer_constraints_v1_interface.name) == 0) {
|
|
pc_mgr = wl_registry_bind(reg, name,
|
|
&zwp_pointer_constraints_v1_interface,
|
|
version < 1 ? version : 1);
|
|
} else if (strcmp(interface,
|
|
zwp_relative_pointer_manager_v1_interface.name) == 0) {
|
|
rel_mgr = wl_registry_bind(reg, name,
|
|
&zwp_relative_pointer_manager_v1_interface,
|
|
version < 1 ? version : 1);
|
|
} else if (strcmp(interface,
|
|
zwp_primary_selection_device_manager_v1_interface.name) == 0) {
|
|
ps_mgr = wl_registry_bind(reg, name,
|
|
&zwp_primary_selection_device_manager_v1_interface,
|
|
version < 1 ? version : 1);
|
|
} else if (strcmp(interface,
|
|
zwlr_layer_shell_v1_interface.name) == 0) {
|
|
layer_mgr = wl_registry_bind(reg, name,
|
|
&zwlr_layer_shell_v1_interface,
|
|
version < 4 ? version : 4);
|
|
} else if (strcmp(interface,
|
|
zwlr_foreign_toplevel_manager_v1_interface.name) == 0) {
|
|
ft_mgr = wl_registry_bind(reg, name,
|
|
&zwlr_foreign_toplevel_manager_v1_interface,
|
|
version < 3 ? version : 3);
|
|
} else if (strcmp(interface,
|
|
ext_foreign_toplevel_list_v1_interface.name) == 0) {
|
|
ext_list = wl_registry_bind(reg, name,
|
|
&ext_foreign_toplevel_list_v1_interface,
|
|
version < 1 ? version : 1);
|
|
} else if (strcmp(interface,
|
|
ext_idle_notifier_v1_interface.name) == 0) {
|
|
idle_mgr = wl_registry_bind(reg, name,
|
|
&ext_idle_notifier_v1_interface,
|
|
version < 1 ? version : 1);
|
|
} else if (strcmp(interface,
|
|
zwp_idle_inhibit_manager_v1_interface.name) == 0) {
|
|
inhib_mgr = wl_registry_bind(reg, name,
|
|
&zwp_idle_inhibit_manager_v1_interface,
|
|
version < 1 ? version : 1);
|
|
}
|
|
}
|
|
|
|
static void registry_global_remove(void *data, struct wl_registry *reg,
|
|
uint32_t name) {
|
|
(void)data; (void)reg; (void)name;
|
|
}
|
|
|
|
static const struct wl_registry_listener registry_listener = {
|
|
.global = registry_global,
|
|
.global_remove = registry_global_remove,
|
|
};
|
|
|
|
static struct wl_shm_pool *make_pool(size_t size, int *fd_out) {
|
|
char name[] = "/tmp/wltest-XXXXXX";
|
|
int fd = mkstemp(name);
|
|
if (fd < 0) {
|
|
fprintf(stderr, "wltest: mkstemp failed\n");
|
|
return NULL;
|
|
}
|
|
unlink(name);
|
|
if (ftruncate(fd, (off_t)size) < 0) {
|
|
fprintf(stderr, "wltest: ftruncate failed\n");
|
|
close(fd);
|
|
return NULL;
|
|
}
|
|
*fd_out = fd;
|
|
return wl_shm_create_pool(shm, fd, (int32_t)size);
|
|
}
|
|
|
|
static void create_buffer(void) {
|
|
/* Recreate at the current W/H on every configure: a test client that
|
|
silently re-commits its old-size buffer makes compositor resize and
|
|
fullscreen handling untestable. The previous wl_buffer is left alive
|
|
(a few KB of shm per resize -- wltest is short-lived): destroying it
|
|
here would race the compositor's use of the last committed frame and
|
|
kill our own data-access path for buffers still on screen. */
|
|
size_t stride = (size_t)W * 4;
|
|
size_t size = stride * (size_t)H;
|
|
int fd;
|
|
struct wl_shm_pool *pool = make_pool(size, &fd);
|
|
if (!pool) {
|
|
fprintf(stderr, "wltest: shm pool failed\n");
|
|
exit(1);
|
|
}
|
|
uint8_t *data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
|
if (data == MAP_FAILED) {
|
|
fprintf(stderr, "wltest: mmap failed\n");
|
|
exit(1);
|
|
}
|
|
for (int y = 0; y < H; y++) {
|
|
uint8_t *row = data + (size_t)y * stride;
|
|
for (int x = 0; x < W; x++) {
|
|
row[x*4+0] = PIXEL[2]; /* B */
|
|
row[x*4+1] = PIXEL[1]; /* G */
|
|
row[x*4+2] = PIXEL[0]; /* R */
|
|
row[x*4+3] = PIXEL[3]; /* A */
|
|
}
|
|
}
|
|
munmap(data, size);
|
|
close(fd);
|
|
buffer = wl_shm_pool_create_buffer(pool, 0, W, H, (int32_t)stride,
|
|
WL_SHM_FORMAT_XRGB8888);
|
|
wl_shm_pool_destroy(pool);
|
|
}
|
|
|
|
/* ---- the extra (second, same-process) toplevel ---- */
|
|
|
|
static void extra_toplevel_configure(void *data, struct xdg_toplevel *xp,
|
|
int32_t w, int32_t h, struct wl_array *states) {
|
|
(void)data; (void)xp; (void)w; (void)h; (void)states;
|
|
}
|
|
|
|
static const struct xdg_toplevel_listener extra_toplevel_listener = {
|
|
.configure = extra_toplevel_configure,
|
|
.close = xdg_toplevel_close,
|
|
};
|
|
|
|
static void extra_create_buffer(void) {
|
|
if (ex_buffer) return;
|
|
size_t stride = (size_t)EX_W * 4;
|
|
size_t size = stride * (size_t)EX_H;
|
|
int fd;
|
|
struct wl_shm_pool *pool = make_pool(size, &fd);
|
|
if (!pool) {
|
|
fprintf(stderr, "wltest: extra shm pool failed\n");
|
|
exit(1);
|
|
}
|
|
uint8_t *data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
|
if (data == MAP_FAILED) {
|
|
fprintf(stderr, "wltest: extra mmap failed\n");
|
|
exit(1);
|
|
}
|
|
for (int y = 0; y < EX_H; y++) {
|
|
uint8_t *row = data + (size_t)y * stride;
|
|
for (int x = 0; x < EX_W; x++) {
|
|
row[x*4+0] = EXTRA_PIXEL[2]; /* B */
|
|
row[x*4+1] = EXTRA_PIXEL[1]; /* G */
|
|
row[x*4+2] = EXTRA_PIXEL[0]; /* R */
|
|
row[x*4+3] = EXTRA_PIXEL[3]; /* A */
|
|
}
|
|
}
|
|
munmap(data, size);
|
|
close(fd);
|
|
ex_buffer = wl_shm_pool_create_buffer(pool, 0, EX_W, EX_H,
|
|
(int32_t)stride, WL_SHM_FORMAT_XRGB8888);
|
|
wl_shm_pool_destroy(pool);
|
|
}
|
|
|
|
static void extra_xdg_configure(void *data, struct xdg_surface *xs, uint32_t serial) {
|
|
(void)data;
|
|
xdg_surface_ack_configure(xs, serial);
|
|
extra_create_buffer();
|
|
wl_surface_attach(ex_surface, ex_buffer, 0, 0);
|
|
wl_surface_damage_buffer(ex_surface, 0, 0, EX_W, EX_H);
|
|
wl_surface_commit(ex_surface);
|
|
}
|
|
|
|
static const struct xdg_surface_listener extra_xdg_listener = {
|
|
.configure = extra_xdg_configure,
|
|
};
|
|
|
|
static void extra_open(void) {
|
|
ex_surface = wl_compositor_create_surface(compositor);
|
|
ex_xdg = xdg_wm_base_get_xdg_surface(wm_base, ex_surface);
|
|
ex_toplevel = xdg_surface_get_toplevel(ex_xdg);
|
|
xdg_toplevel_set_title(ex_toplevel, "extra");
|
|
xdg_surface_add_listener(ex_xdg, &extra_xdg_listener, NULL);
|
|
xdg_toplevel_add_listener(ex_toplevel, &extra_toplevel_listener, NULL);
|
|
wl_surface_commit(ex_surface);
|
|
}
|
|
|
|
static const struct { uint32_t shape; const char *name; } SHAPES[] = {
|
|
{ WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT, "default" },
|
|
{ WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_TEXT, "text" },
|
|
{ WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_POINTER, "pointer" },
|
|
{ WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_CROSSHAIR, "crosshair" },
|
|
};
|
|
|
|
static void ps_source_send(void *data, struct zwp_primary_selection_source_v1 *s,
|
|
const char *mime, int32_t fd) {
|
|
(void)data; (void)s;
|
|
fprintf(stderr, "wltest: ps source send mime=%s\n", mime);
|
|
size_t n = primary_text ? strlen(primary_text) : 0;
|
|
if (n > 0 && write(fd, primary_text, n) < 0) {
|
|
/* best effort; the reader will see a short/empty transfer */
|
|
}
|
|
close(fd);
|
|
}
|
|
|
|
static void ps_source_cancelled(void *data,
|
|
struct zwp_primary_selection_source_v1 *s) {
|
|
(void)data; (void)s;
|
|
}
|
|
|
|
static const struct zwp_primary_selection_source_v1_listener ps_source_listener = {
|
|
.send = ps_source_send,
|
|
.cancelled = ps_source_cancelled,
|
|
};
|
|
|
|
static char *pull_primary(size_t *out_len);
|
|
|
|
static void ps_offer_offer(void *data, struct zwp_primary_selection_offer_v1 *o,
|
|
const char *mime) {
|
|
(void)data; (void)o;
|
|
fprintf(stderr, "wltest: ps offer mime=%s\n", mime);
|
|
if (!primary_out || ps_need_pull)
|
|
return;
|
|
if (strcmp(mime, "text/plain") == 0) {
|
|
/* Deferred to the main loop: pull_primary() dispatches the display
|
|
to let the setter answer, which cannot nest inside this callback. */
|
|
ps_need_pull = true;
|
|
}
|
|
}
|
|
|
|
static const struct zwp_primary_selection_offer_v1_listener ps_offer_listener = {
|
|
.offer = ps_offer_offer,
|
|
};
|
|
|
|
static void ps_device_data_offer(void *data,
|
|
struct zwp_primary_selection_device_v1 *d,
|
|
struct zwp_primary_selection_offer_v1 *offer) {
|
|
(void)data; (void)d;
|
|
fprintf(stderr, "wltest: ps data_offer\n");
|
|
ps_offer = offer;
|
|
zwp_primary_selection_offer_v1_add_listener(offer, &ps_offer_listener, NULL);
|
|
}
|
|
|
|
static void ps_device_selection(void *data,
|
|
struct zwp_primary_selection_device_v1 *d,
|
|
struct zwp_primary_selection_offer_v1 *offer) {
|
|
(void)data; (void)d; (void)offer;
|
|
}
|
|
|
|
static const struct zwp_primary_selection_device_v1_listener ps_device_listener = {
|
|
.data_offer = ps_device_data_offer,
|
|
.selection = ps_device_selection,
|
|
};
|
|
|
|
/* Receive the selection over a pipe. Reading can't be driven by display
|
|
events alone: the compositor forwards the fd to the owning *setter* tab
|
|
via its `send` event, so the data arrives on the pipe with no display
|
|
event of ours to wake a wl_display_dispatch(). Poll both descriptors and
|
|
only dispatch when the display socket actually has events.
|
|
Returns the payload (malloc'd) and its length; never returns until the
|
|
stream is complete (EOF). */
|
|
static char *pull_primary(size_t *out_len) {
|
|
struct zwp_primary_selection_offer_v1 *offer = ps_offer;
|
|
int pfd[2];
|
|
if (pipe(pfd) != 0) {
|
|
fprintf(stderr, "wltest: primary pipe failed\n");
|
|
exit(1);
|
|
}
|
|
zwp_primary_selection_offer_v1_receive(offer, "text/plain", pfd[1]);
|
|
close(pfd[1]);
|
|
wl_display_flush(display);
|
|
int disp_fd = wl_display_get_fd(display);
|
|
char *acc = NULL;
|
|
size_t acc_len = 0, acc_cap = 0;
|
|
char buf[4096];
|
|
for (;;) {
|
|
struct pollfd pfds[2];
|
|
pfds[0].fd = pfd[0];
|
|
pfds[0].events = POLLIN;
|
|
pfds[1].fd = disp_fd;
|
|
pfds[1].events = POLLIN;
|
|
int r = poll(pfds, 2, 3000);
|
|
if (r == 0) {
|
|
/* A stalled source (e.g. a dead pre-test host primary) delivers
|
|
neither data nor EOF. Give up on it so the caller can keep
|
|
dispatching and pick up any newer offer. */
|
|
break;
|
|
}
|
|
if (r < 0) {
|
|
if (errno == EINTR)
|
|
continue;
|
|
break;
|
|
}
|
|
if (pfds[0].revents & (POLLIN | POLLHUP)) {
|
|
ssize_t n = read(pfd[0], buf, sizeof buf);
|
|
if (n > 0) {
|
|
if (acc_len + (size_t)n > acc_cap) {
|
|
acc_cap = (acc_cap ? acc_cap * 2 : 1024) + (size_t)n;
|
|
acc = realloc(acc, acc_cap);
|
|
if (!acc) {
|
|
fprintf(stderr, "wltest: primary alloc failed\n");
|
|
exit(1);
|
|
}
|
|
}
|
|
memcpy(acc + acc_len, buf, (size_t)n);
|
|
acc_len += (size_t)n;
|
|
continue;
|
|
}
|
|
if (n == 0)
|
|
break;
|
|
if (errno == EINTR)
|
|
continue;
|
|
if (errno == EAGAIN || errno == EWOULDBLOCK)
|
|
continue;
|
|
break;
|
|
}
|
|
if (pfds[1].revents & (POLLIN | POLLHUP | POLLERR)) {
|
|
if (wl_display_dispatch(display) < 0)
|
|
break;
|
|
/* A newer selection displaced the one being pulled: abandon this
|
|
stale pipe and let the caller pull the fresh offer. */
|
|
if (ps_need_pull)
|
|
break;
|
|
}
|
|
}
|
|
close(pfd[0]);
|
|
*out_len = acc_len;
|
|
return acc;
|
|
}
|
|
|
|
static void write_outfile(const char *path, const char *data, size_t len) {
|
|
int fd = open(path, O_CREAT | O_TRUNC | O_WRONLY, 0600);
|
|
if (fd < 0) {
|
|
fprintf(stderr, "wltest: cannot open %s\n", path);
|
|
exit(1);
|
|
}
|
|
ssize_t w = write(fd, data ? data : "", len);
|
|
(void)w;
|
|
close(fd);
|
|
}
|
|
|
|
/* --- Idle (ext-idle-notify + idle-inhibit) ---
|
|
--idle <ms> [--idle-out <file>] [+ --idle-inhibit]: act as a plain window
|
|
client that also binds ext-idle-notify, asks for an idle notification after
|
|
<ms>ms of no input, and writes "idle" to --idle-out when the compositor
|
|
fires it (or "resumed" when activity returns). With --idle-inhibit the
|
|
client creates an idle-inhibit-v1 inhibitor on its toplevel, proving the
|
|
compositor suppresses the idled event while inhibited. */
|
|
static bool idle_mode;
|
|
static int idle_timeout_ms;
|
|
static const char *idle_out;
|
|
static bool idle_inhibit;
|
|
static struct ext_idle_notification_v1 *idle_notif;
|
|
static struct zwp_idle_inhibitor_v1 *idle_inhib;
|
|
|
|
static char idled_status[] = "idle";
|
|
static size_t idled_status_len;
|
|
static void idle_notif_idled(void *data, struct ext_idle_notification_v1 *n) {
|
|
(void)data; (void)n;
|
|
if (idle_out) write_outfile(idle_out, idled_status, idled_status_len);
|
|
}
|
|
static void idle_notif_resumed(void *data, struct ext_idle_notification_v1 *n) {
|
|
(void)data; (void)n;
|
|
if (idle_out) {
|
|
const char *r = "resumed";
|
|
write_outfile(idle_out, r, strlen(r));
|
|
}
|
|
}
|
|
static const struct ext_idle_notification_v1_listener idle_notif_listener = {
|
|
.idled = idle_notif_idled,
|
|
.resumed = idle_notif_resumed,
|
|
};
|
|
|
|
static const struct wl_registry_listener registry_listener;
|
|
static void idle_run(void) {
|
|
idled_status_len = strlen(idled_status);
|
|
display = wl_display_connect(NULL);
|
|
if (!display) {
|
|
fprintf(stderr, "wltest: idle: cannot connect\n");
|
|
exit(1);
|
|
}
|
|
struct wl_registry *reg = wl_display_get_registry(display);
|
|
wl_registry_add_listener(reg, ®istry_listener, NULL);
|
|
wl_display_roundtrip(display);
|
|
if (!idle_mgr) {
|
|
fprintf(stderr, "wltest: idle: ext_idle_notifier_v1 not advertised\n");
|
|
exit(1);
|
|
}
|
|
/* Need a surface for the idle-inhibit inhibitor and a seat for the
|
|
notification. Draw a small buffer so the window is present. */
|
|
surface = wl_compositor_create_surface(compositor);
|
|
xdg_surface = xdg_wm_base_get_xdg_surface(wm_base, surface);
|
|
toplevel = xdg_surface_get_toplevel(xdg_surface);
|
|
xdg_toplevel_set_title(toplevel, "wltest-idle");
|
|
xdg_wm_base_add_listener(wm_base, &wm_base_listener, NULL);
|
|
xdg_surface_add_listener(xdg_surface, &xdg_surface_listener, NULL);
|
|
xdg_toplevel_add_listener(toplevel, &toplevel_listener, NULL);
|
|
/* Commit an empty surface and let the configure handler attach the real
|
|
buffer + commit (xdg_surface_configure does that). */
|
|
wl_surface_commit(surface);
|
|
wl_display_roundtrip(display);
|
|
if (seat) {
|
|
idle_notif = ext_idle_notifier_v1_get_idle_notification(
|
|
idle_mgr, (uint32_t)idle_timeout_ms, seat);
|
|
ext_idle_notification_v1_add_listener(idle_notif, &idle_notif_listener, NULL);
|
|
}
|
|
if (idle_inhibit && inhib_mgr)
|
|
idle_inhib = zwp_idle_inhibit_manager_v1_create_inhibitor(inhib_mgr, surface);
|
|
for (;;) {
|
|
if (wl_display_dispatch(display) < 0)
|
|
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
|
|
own primary selection at boot, before the test's setter runs). */
|
|
static void demand_primary_once(void) {
|
|
ps_need_pull = false;
|
|
size_t len = 0;
|
|
char *payload = pull_primary(&len);
|
|
if (primary_expect) {
|
|
if (len == strlen(primary_expect) &&
|
|
memcmp(payload, primary_expect, len) == 0) {
|
|
fprintf(stderr, "wltest: ps matched expected text\n");
|
|
write_outfile(primary_out, payload, len);
|
|
free(payload);
|
|
exit(0);
|
|
}
|
|
fprintf(stderr, "wltest: ps got %zu bytes (not the expected text)\n",
|
|
len);
|
|
free(payload);
|
|
return; /* keep waiting for the setter's offer */
|
|
}
|
|
write_outfile(primary_out, payload, len);
|
|
free(payload);
|
|
exit(0);
|
|
}
|
|
|
|
static void print_usage(const char *argv0) {
|
|
fprintf(stderr, "usage: %s [--app-id <id>] [--extra #RRGGBB] "
|
|
"[--cursor-shape name] [--globals-out <file>] "
|
|
"[--constrain locked|confined] [--confine-rect X Y W H] "
|
|
"[--primary-set <text>] [--primary-demand <file>] "
|
|
"[--touch-log <file>] "
|
|
"[--layer <bg|bottom|top|overlay>] [--layer-anchor <top|bottom|left|right>] "
|
|
"[--exclusive-zone <int>] [--layer-size <W> <H>] "
|
|
"[--foreign [--foreign-out <file>] [--foreign-close <app_id>]] "
|
|
"[--idle <ms> [--idle-out <file>] [--idle-inhibit]] "
|
|
"#RRGGBB [WIDTHxHEIGHT]\n"
|
|
" %s shows a solid-color xdg_toplevel, e.g. #ff0000\n"
|
|
" --extra also opens a second toplevel from the same client\n"
|
|
" --cursor-shape requests that cursor-shape-v1 shape on "
|
|
"pointer enter (default|text|pointer|crosshair)\n"
|
|
" --globals-out lists advertised globals to <file> and exits\n"
|
|
" --constrain requests a pointer constraint (locked/confined)\n"
|
|
" --confine-rect sets the confined pointer's region\n"
|
|
" --primary-set installs <text> as the primary selection\n"
|
|
" --primary-demand pulls the selection into <file> and exits\n"
|
|
" --primary-demand-expect pulls until the payload equals "
|
|
"<text>, then writes it to <file> and exits\n"
|
|
" --layer creates a wlr-layer-shell surface (zwp_layer_surface_v1)\n"
|
|
" --layer-anchor may be repeated to OR anchors (default: top)\n"
|
|
" --exclusive-zone sets the exclusive zone (default: 0)\n"
|
|
" --layer-size sets the requested width/height (default: 0 0)\n"
|
|
" --foreign binds the taskbar protocols (wlr-foreign-toplevel\n"
|
|
" + ext-foreign-toplevel-list); no window is opened. Writes\n"
|
|
" each window's state to --foreign-out and, with\n"
|
|
" --foreign-close <app_id>, closes that window\n"
|
|
" --idle asks ext-idle-notify for an idle event after <ms>ms of\n"
|
|
" no input, writing \"idle\" (or \"resumed\") to --idle-out;\n"
|
|
" --idle-inhibit additionally holds an idle-inhibit killer\n",
|
|
argv0, argv0);
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
const char *app_id = NULL;
|
|
const char *touch_log_path = NULL;
|
|
int i = 1;
|
|
while (i < argc) {
|
|
if (strcmp(argv[i], "--app-id") == 0) {
|
|
if (i + 1 >= argc) {
|
|
fprintf(stderr, "wltest: --app-id needs a value\n");
|
|
return 1;
|
|
}
|
|
app_id = argv[i + 1];
|
|
i += 2;
|
|
} else if (strcmp(argv[i], "--extra") == 0) {
|
|
if (i + 1 >= argc) {
|
|
fprintf(stderr, "wltest: --extra needs a value\n");
|
|
return 1;
|
|
}
|
|
const char *x = argv[i + 1];
|
|
if (*x == '#') x++;
|
|
if (strlen(x) != 6 || sscanf(x, "%2hhx%2hhx%2hhx",
|
|
&EXTRA_PIXEL[0], &EXTRA_PIXEL[1], &EXTRA_PIXEL[2]) != 3) {
|
|
fprintf(stderr, "wltest: bad extra color '%s'\n", argv[i + 1]);
|
|
return 1;
|
|
}
|
|
want_extra = true;
|
|
i += 2;
|
|
} else if (strcmp(argv[i], "--cursor-shape") == 0) {
|
|
if (i + 1 >= argc) {
|
|
fprintf(stderr, "wltest: --cursor-shape needs a value "
|
|
"(default|text|pointer|crosshair)\n");
|
|
return 1;
|
|
}
|
|
const char *s = argv[i + 1];
|
|
bool found = false;
|
|
for (size_t k = 0; k < sizeof(SHAPES) / sizeof(SHAPES[0]); k++)
|
|
if (strcmp(s, SHAPES[k].name) == 0) {
|
|
cursor_shape = SHAPES[k].shape;
|
|
found = true;
|
|
break;
|
|
}
|
|
if (!found) {
|
|
fprintf(stderr, "wltest: bad --cursor-shape '%s' "
|
|
"(default|text|pointer|crosshair)\n", s);
|
|
return 1;
|
|
}
|
|
i += 2;
|
|
} else if (strcmp(argv[i], "--globals-out") == 0) {
|
|
if (i + 1 >= argc) {
|
|
fprintf(stderr, "wltest: --globals-out needs a path\n");
|
|
return 1;
|
|
}
|
|
globals_out = argv[i + 1];
|
|
i += 2;
|
|
} else if (strcmp(argv[i], "--constrain") == 0) {
|
|
if (i + 1 >= argc) {
|
|
fprintf(stderr, "wltest: --constrain needs a value\n");
|
|
return 1;
|
|
}
|
|
const char *m = argv[i + 1];
|
|
if (strcmp(m, "locked") == 0) {
|
|
constrain_mode = CONSTRAINT_LOCKED;
|
|
} else if (strcmp(m, "confined") == 0) {
|
|
constrain_mode = CONSTRAINT_CONFINED;
|
|
} else {
|
|
fprintf(stderr, "wltest: bad --constrain '%s'\n", m);
|
|
return 1;
|
|
}
|
|
i += 2;
|
|
} else if (strcmp(argv[i], "--primary-set") == 0) {
|
|
if (i + 1 >= argc) {
|
|
fprintf(stderr, "wltest: --primary-set needs a value\n");
|
|
return 1;
|
|
}
|
|
primary_text = argv[i + 1];
|
|
i += 2;
|
|
} else if (strcmp(argv[i], "--primary-demand") == 0) {
|
|
if (i + 1 >= argc) {
|
|
fprintf(stderr, "wltest: --primary-demand needs a path\n");
|
|
return 1;
|
|
}
|
|
primary_out = argv[i + 1];
|
|
i += 2;
|
|
} else if (strcmp(argv[i], "--touch-log") == 0) {
|
|
if (i + 1 >= argc) {
|
|
fprintf(stderr,
|
|
"wltest: --touch-log needs a file argument\n");
|
|
return 1;
|
|
}
|
|
touch_log_path = argv[i + 1];
|
|
i += 2;
|
|
} else if (strcmp(argv[i], "--primary-demand-expect") == 0) {
|
|
if (i + 2 >= argc) {
|
|
fprintf(stderr, "wltest: --primary-demand-expect needs a "
|
|
"path and expected text\n");
|
|
return 1;
|
|
}
|
|
primary_out = argv[i + 1];
|
|
primary_expect = argv[i + 2];
|
|
i += 3;
|
|
} else if (strcmp(argv[i], "--confine-rect") == 0) {
|
|
if (i + 4 >= argc) {
|
|
fprintf(stderr, "wltest: --confine-rect needs X Y W H\n");
|
|
return 1;
|
|
}
|
|
for (int k = 0; k < 4; k++) {
|
|
confine_rect[k] = atoi(argv[i + 1 + k]);
|
|
if (confine_rect[k] < 0) {
|
|
fprintf(stderr, "wltest: bad --confine-rect\n");
|
|
return 1;
|
|
}
|
|
}
|
|
have_confine_rect = true;
|
|
i += 5;
|
|
} else if (strcmp(argv[i], "--layer") == 0) {
|
|
if (i + 1 >= argc) {
|
|
fprintf(stderr, "wltest: --layer needs a value "
|
|
"(bg|bottom|top|overlay)\n");
|
|
return 1;
|
|
}
|
|
layer_mode = true;
|
|
const char *l = argv[i + 1];
|
|
if (strcmp(l, "bg") == 0 || strcmp(l, "background") == 0)
|
|
layer_id = ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND;
|
|
else if (strcmp(l, "bottom") == 0)
|
|
layer_id = ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM;
|
|
else if (strcmp(l, "top") == 0)
|
|
layer_id = ZWLR_LAYER_SHELL_V1_LAYER_TOP;
|
|
else if (strcmp(l, "overlay") == 0)
|
|
layer_id = ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY;
|
|
else {
|
|
fprintf(stderr, "wltest: bad --layer '%s'\n", l);
|
|
return 1;
|
|
}
|
|
i += 2;
|
|
} else if (strcmp(argv[i], "--layer-anchor") == 0) {
|
|
if (i + 1 >= argc) {
|
|
fprintf(stderr, "wltest: --layer-anchor needs a value "
|
|
"(top|bottom|left|right)\n");
|
|
return 1;
|
|
}
|
|
const char *a = argv[i + 1];
|
|
uint32_t bit;
|
|
if (strcmp(a, "top") == 0)
|
|
bit = ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP;
|
|
else if (strcmp(a, "bottom") == 0)
|
|
bit = ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM;
|
|
else if (strcmp(a, "left") == 0)
|
|
bit = ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT;
|
|
else if (strcmp(a, "right") == 0)
|
|
bit = ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT;
|
|
else {
|
|
fprintf(stderr, "wltest: bad --layer-anchor '%s'\n", a);
|
|
return 1;
|
|
}
|
|
layer_anchor |= bit;
|
|
i += 2;
|
|
} else if (strcmp(argv[i], "--exclusive-zone") == 0) {
|
|
if (i + 1 >= argc) {
|
|
fprintf(stderr, "wltest: --exclusive-zone needs a value\n");
|
|
return 1;
|
|
}
|
|
layer_exclusive = (int32_t)atoi(argv[i + 1]);
|
|
i += 2;
|
|
} else if (strcmp(argv[i], "--layer-size") == 0) {
|
|
if (i + 2 >= argc) {
|
|
fprintf(stderr, "wltest: --layer-size needs W and H\n");
|
|
return 1;
|
|
}
|
|
layer_w = atoi(argv[i + 1]);
|
|
layer_h = atoi(argv[i + 2]);
|
|
i += 3;
|
|
} else if (strcmp(argv[i], "--foreign") == 0) {
|
|
foreign_mode = true;
|
|
i += 1;
|
|
} else if (strcmp(argv[i], "--foreign-out") == 0) {
|
|
if (i + 1 >= argc) {
|
|
fprintf(stderr, "wltest: --foreign-out needs a path\n");
|
|
return 1;
|
|
}
|
|
foreign_out = argv[i + 1];
|
|
i += 2;
|
|
} else if (strcmp(argv[i], "--foreign-close") == 0) {
|
|
if (i + 1 >= argc) {
|
|
fprintf(stderr, "wltest: --foreign-close needs an app_id\n");
|
|
return 1;
|
|
}
|
|
foreign_close_app = argv[i + 1];
|
|
i += 2;
|
|
} else if (strcmp(argv[i], "--idle") == 0) {
|
|
if (i + 1 >= argc) {
|
|
fprintf(stderr, "wltest: --idle needs a timeout in ms\n");
|
|
return 1;
|
|
}
|
|
idle_mode = true;
|
|
idle_timeout_ms = atoi(argv[i + 1]);
|
|
if (idle_timeout_ms <= 0) {
|
|
fprintf(stderr, "wltest: bad --idle timeout\n");
|
|
return 1;
|
|
}
|
|
i += 2;
|
|
} else if (strcmp(argv[i], "--idle-out") == 0) {
|
|
if (i + 1 >= argc) {
|
|
fprintf(stderr, "wltest: --idle-out needs a path\n");
|
|
return 1;
|
|
}
|
|
idle_out = argv[i + 1];
|
|
i += 2;
|
|
} else if (strcmp(argv[i], "--idle-inhibit") == 0) {
|
|
idle_inhibit = true;
|
|
i += 1;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
/* Taskbar mode: no color/size argument. Run as a foreign-toplevel
|
|
listener and never create a window. */
|
|
if (foreign_mode) {
|
|
foreign_run();
|
|
return 0;
|
|
}
|
|
/* Idle mode: a window client that additionally exercises ext-idle-notify
|
|
(+idle-inhibit). It builds its own solid-color toplevel, so it takes
|
|
no trailing color/size argument. */
|
|
if (idle_mode) {
|
|
idle_run();
|
|
return 0;
|
|
}
|
|
if (globals_out) {
|
|
/* Advertised-globals mode: the color/size args are ignored. */
|
|
display = wl_display_connect(NULL);
|
|
if (!display) {
|
|
fprintf(stderr, "wltest: cannot connect (WAYLAND_DISPLAY=%s)\n",
|
|
getenv("WAYLAND_DISPLAY") ? getenv("WAYLAND_DISPLAY") : "unset");
|
|
return 1;
|
|
}
|
|
struct wl_registry *reg = wl_display_get_registry(display);
|
|
wl_registry_add_listener(reg, ®istry_listener, NULL);
|
|
wl_display_roundtrip(display);
|
|
wl_display_roundtrip(display);
|
|
wl_display_disconnect(display);
|
|
return 0;
|
|
}
|
|
if (i >= argc || argv[i][0] != '#') {
|
|
print_usage(argv[0]);
|
|
return 1;
|
|
}
|
|
const char *hex = argv[i] + 1;
|
|
if (strlen(hex) != 6 || sscanf(hex, "%2hhx%2hhx%2hhx",
|
|
&PIXEL[0], &PIXEL[1], &PIXEL[2]) != 3) {
|
|
fprintf(stderr, "wltest: bad color '%s'\n", argv[i]);
|
|
return 1;
|
|
}
|
|
if (i + 2 < argc) {
|
|
W = atoi(argv[i + 1]);
|
|
H = atoi(argv[i + 2]);
|
|
if (W <= 0 || H <= 0) {
|
|
fprintf(stderr, "wltest: bad size\n");
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
display = wl_display_connect(NULL);
|
|
if (!display) {
|
|
fprintf(stderr, "wltest: cannot connect (WAYLAND_DISPLAY=%s)\n",
|
|
getenv("WAYLAND_DISPLAY") ? getenv("WAYLAND_DISPLAY") : "unset");
|
|
return 1;
|
|
}
|
|
|
|
struct wl_registry *registry = wl_display_get_registry(display);
|
|
wl_registry_add_listener(registry, ®istry_listener, NULL);
|
|
wl_display_roundtrip(display);
|
|
|
|
if (!compositor || !shm) {
|
|
fprintf(stderr, "wltest: compositor/shm missing\n");
|
|
return 1;
|
|
}
|
|
|
|
/* Layer mode: create a wlr_layer_surface instead of an xdg toplevel. */
|
|
if (layer_mode) {
|
|
if (!layer_mgr) {
|
|
fprintf(stderr, "wltest: wlr_layer_shell_v1 not supported\n");
|
|
return 1;
|
|
}
|
|
/* PIXEL[] and W/H were already parsed from the trailing args. */
|
|
|
|
surface = wl_compositor_create_surface(compositor);
|
|
layer_surface = zwlr_layer_shell_v1_get_layer_surface(
|
|
layer_mgr, surface, NULL, layer_id, "wltest");
|
|
zwlr_layer_surface_v1_set_anchor(layer_surface, layer_anchor);
|
|
zwlr_layer_surface_v1_set_exclusive_zone(layer_surface, layer_exclusive);
|
|
if (layer_w > 0 || layer_h > 0)
|
|
zwlr_layer_surface_v1_set_size(layer_surface,
|
|
(uint32_t)layer_w, (uint32_t)layer_h);
|
|
zwlr_layer_surface_v1_set_keyboard_interactivity(layer_surface,
|
|
ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE);
|
|
zwlr_layer_surface_v1_add_listener(layer_surface, &layer_listener, NULL);
|
|
wl_surface_commit(surface);
|
|
wl_display_roundtrip(display);
|
|
for (;;) {
|
|
if (wl_display_dispatch(display) < 0)
|
|
break;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
if (!wm_base) {
|
|
fprintf(stderr, "wltest: xdg_wm_base missing\n");
|
|
return 1;
|
|
}
|
|
|
|
surface = wl_compositor_create_surface(compositor);
|
|
xdg_surface = xdg_wm_base_get_xdg_surface(wm_base, surface);
|
|
toplevel = xdg_surface_get_toplevel(xdg_surface);
|
|
xdg_toplevel_set_title(toplevel, argv[i]);
|
|
if (app_id)
|
|
xdg_toplevel_set_app_id(toplevel, app_id);
|
|
|
|
xdg_wm_base_add_listener(wm_base, &wm_base_listener, NULL);
|
|
xdg_surface_add_listener(xdg_surface, &xdg_surface_listener, NULL);
|
|
xdg_toplevel_add_listener(toplevel, &toplevel_listener, NULL);
|
|
|
|
if (seat) {
|
|
keyboard = wl_seat_get_keyboard(seat);
|
|
wl_keyboard_add_listener(keyboard, &keyboard_listener, NULL);
|
|
pointer = wl_seat_get_pointer(seat);
|
|
wl_pointer_add_listener(pointer, &pointer_listener, NULL);
|
|
/* --touch-log: fetch the touchscreen (inert when the seat has none)
|
|
and record every event so tests can prove delivery end-to-end. */
|
|
if (touch_log_path && !touch_log) {
|
|
touch_log = fopen(touch_log_path, "w");
|
|
if (!touch_log) {
|
|
fprintf(stderr, "wltest: cannot write touch log %s\n",
|
|
touch_log_path);
|
|
return 1;
|
|
}
|
|
touch = wl_seat_get_touch(seat);
|
|
wl_touch_add_listener(touch, &touch_listener, NULL);
|
|
}
|
|
if (shape_mgr)
|
|
shape_dev = wp_cursor_shape_manager_v1_get_pointer(shape_mgr, pointer);
|
|
if (rel_mgr)
|
|
rel_ptr = zwp_relative_pointer_manager_v1_get_relative_pointer(rel_mgr, pointer);
|
|
}
|
|
|
|
/* Primary selection: the setter installs a source; the demander just
|
|
listens and pulls on offer. get_device is what hooks the device into
|
|
the seat's set_primary_selection fan-out, so both sides need it. The
|
|
setter's request itself is deferred to maybe_set_primary() so it can
|
|
attach a seat-granted serial. */
|
|
if (ps_mgr && seat) {
|
|
ps_dev = zwp_primary_selection_device_manager_v1_get_device(ps_mgr, seat);
|
|
zwp_primary_selection_device_v1_add_listener(ps_dev, &ps_device_listener, NULL);
|
|
}
|
|
|
|
if (constrain_mode != NO_CONSTRAINT)
|
|
request_constraint();
|
|
|
|
wl_surface_commit(surface);
|
|
wl_display_roundtrip(display);
|
|
|
|
for (;;) {
|
|
if (ps_need_pull && primary_out)
|
|
demand_primary_once(); /* returns unless satisfied */
|
|
if (wl_display_dispatch(display) < 0)
|
|
break;
|
|
}
|
|
return 0;
|
|
} |