kick/tests/wltest.c

4570 lines
179 KiB
C
Raw Normal View History

#define _GNU_SOURCE
/* Sanitizer-gate defaults: baked in so they survive the test harness's
env -i spawns (which would otherwise lose ASAN_OPTIONS). Compiled out
of normal builds entirely. */
#if defined(__SANITIZE_ADDRESS__)
const char *__asan_default_options(void) {
return "detect_leaks=0:halt_on_error=1";
}
#endif
#if defined(__SANITIZE_UNDEFINED__)
const char *__ubsan_default_options(void) {
return "halt_on_error=1:print_stacktrace=1";
}
#endif
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <time.h>
#include <unistd.h>
#include <sys/shm.h>
#include <wayland-client.h>
#include <xkbcommon/xkbcommon.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"
#include "ext-session-lock-v1-client.h"
#include "ext-image-copy-capture-v1-client.h"
#include "ext-image-capture-source-v1-client.h"
#include "keyboard-shortcuts-inhibit-unstable-v1-client.h"
#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"
#include "text-input-unstable-v3-client.h"
#include "input-method-unstable-v2-client.h"
#include "content-type-v1-client.h"
#include "single-pixel-buffer-v1-client.h"
#include "viewporter-client.h"
#include "wlr-gamma-control-unstable-v1-client.h"
#include "xdg-system-bell-v1-client.h"
#include "xdg-dialog-v1-client.h"
#include "pointer-gestures-unstable-v1-client.h"
#include "tearing-control-v1-client.h"
#include "fractional-scale-v1-client.h"
#include "xdg-toplevel-icon-v1-client.h"
#include "wlr-output-management-unstable-v1-client.h"
#include "xdg-toplevel-tag-v1-client.h"
#include "color-representation-v1-client.h"
#include "ext-data-control-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;
/* --content-type tags the surface; --single-pixel backs it with a 1x1
buffer instead of shm (both need to be visible to create_buffer). */
static int content_type_arg = -1;
static bool single_pixel_mode;
/* --tag-set <tag> [--tag-desc <desc>]: label the toplevel via
xdg-toplevel-tag-v1 before the first commit. --color-probe: attach a
wp_color_representation_surface_v1 to the window surface, declare the
default premultiplied alpha mode, and write "done" to --color-out once
the manager reports its supported features. */
static const char *tag_arg;
static const char *tag_desc_arg;
static bool color_probe;
static const char *color_out;
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;
/* Forward declaration for the 'D' key handler below: the text-input
object is defined with the rest of the text-input mode further down. */
static struct zwp_text_input_v3 *ti_obj;
/* --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;
/* --pointer-out: append every received pointer enter/motion/button to this
file so tests can prove virtual-pointer delivery end-to-end. */
static FILE *pointer_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;
/* --req-out <file>: log maximized=0/1 on every toplevel configure, so
tests can prove configure state after maximize/unmaximize requests.
--ignore-close: swallow xdg_toplevel.close instead of exiting. */
static const char *req_out;
static bool ignore_close;
/* --popup-reposition: reposition P1 after the chain maps (needs wm_base
v3+, see the registry bind). */
static bool popup_reposition;
/* 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;
/* Clipboard and primary selection over ext-data-control-v1:
--clipboard-set <text> publishes a clipboard source on the seat;
--clipboard-demand-expect <file> <text> pulls clipboard offers until the
payload matches; --dc-primary-set / --dc-primary-demand-expect do the
same for the primary selection over the same device; --dc-clear unsets
both and exits. Unlike the classic wl_data_device path (which needs a
seat-granted input serial, so only a focused tab can set), data-control
sets need no serial at all -- which is what the smoke tests use against
kick sockets: stock wl-clipboard only speaks the legacy
zwlr_data_control protocol kick no longer offers, and its wl_data_device
fallback waits for a focus serial a surfaceless tool never receives. */
static const char *clip_text;
static const char *clip_out;
static const char *clip_expect;
static const char *dc_prim_text;
static const char *dc_prim_out;
static const char *dc_prim_expect;
static bool dc_clear;
static struct ext_data_control_manager_v1 *dc_mgr;
static struct ext_data_control_device_v1 *dc_dev;
static struct ext_data_control_offer_v1 *dc_offer;
static struct ext_data_control_offer_v1 *dc_prim_offer;
static struct ext_data_control_offer_v1 *dc_last_offer;
static bool dc_last_text;
static bool dc_need_pull;
static bool dc_prim_need_pull;
static bool dc_set_done;
static bool dc_prim_set_done;
static bool dc_cleared;
/* --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 */
/* Taskbar-side state requests: --foreign-activate/--foreign-maximize/
--foreign-fullscreen <app_id> send that request for the matching window
once its app_id arrives, proving the compositor honors taskbar-driven
activation/maximization/fullscreen from a different client. */
static const char *foreign_activate_app;
static const char *foreign_maximize_app;
static const char *foreign_fullscreen_app;
static bool foreign_sent_activate, foreign_sent_maximize,
foreign_sent_fullscreen;
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;
static struct ext_session_lock_manager_v1 *lock_mgr;
static struct ext_image_copy_capture_manager_v1 *sc_mgr;
static struct ext_output_image_capture_source_manager_v1 *sc_out_mgr;
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 xdg_wm_dialog_v1 *dialog_mgr;
static struct zwp_pointer_gestures_v1 *gestures_mgr;
static struct xdg_toplevel_icon_manager_v1 *toplevel_icon_mgr;
static struct zwlr_output_manager_v1 *output_mgr;
static struct wp_tearing_control_manager_v1 *tearing_mgr;
static struct wp_fractional_scale_manager_v1 *fractional_mgr;
static struct zwp_text_input_manager_v3 *text_input_mgr;
static struct zwp_input_method_manager_v2 *input_method_mgr;
static struct wp_content_type_manager_v1 *content_type_mgr;
static struct wp_single_pixel_buffer_manager_v1 *single_pixel_mgr;
static struct wp_viewporter *viewporter;
static struct wp_viewport *viewport;
static struct zwlr_gamma_control_manager_v1 *gamma_mgr;
static struct xdg_system_bell_v1 *bell_mgr;
static struct xdg_toplevel_tag_manager_v1 *tag_mgr;
static struct wp_color_representation_manager_v1 *color_repr_mgr;
static void color_repr_bind_listener(void);
static struct wl_output *wl_output;
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;
snprintf(foreign_cur.st.app_id, sizeof(foreign_cur.st.app_id), "%s", app_id);
maybe_foreign_close();
/* Taskbar requests go on the handle whose app_id event just arrived,
so they hit the right window no matter how many tabs are up. */
if (foreign_activate_app && !foreign_sent_activate && seat &&
strcmp(foreign_cur.st.app_id, foreign_activate_app) == 0) {
foreign_sent_activate = true;
zwlr_foreign_toplevel_handle_v1_activate(h, seat);
}
if (foreign_maximize_app && !foreign_sent_maximize &&
strcmp(foreign_cur.st.app_id, foreign_maximize_app) == 0) {
foreign_sent_maximize = true;
zwlr_foreign_toplevel_handle_v1_set_maximized(h);
}
/* Fullscreen goes via maybe_foreign_fullscreen (this handle's state
event, once mapped), not here: see below. */
}
static void maybe_foreign_fullscreen(struct zwlr_foreign_toplevel_handle_v1 *h) {
if (foreign_fullscreen_app && !foreign_sent_fullscreen &&
strcmp(foreign_cur.st.app_id, foreign_fullscreen_app) == 0 &&
foreign_cur.st.maximized) {
foreign_sent_fullscreen = true;
zwlr_foreign_toplevel_handle_v1_set_fullscreen(h, NULL);
}
}
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;
}
maybe_foreign_fullscreen(h);
}
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, &registry_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;
/* --dialog-modal marks the main toplevel modal; --extra-modal marks the
extra one. --extra-delay <ms> sleeps before opening the extra window
so boot settles first (lets a modal steal a settled tab). */
static bool dialog_modal;
static struct xdg_dialog_v1 *dialog_obj;
static bool extra_modal;
static int extra_delay_ms;
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);
}
/* Client-initiated window-management requests, for the compositor's
request_maximize/minimize/move/resize handlers: kick keeps every
tab maximized and ignores minimize/move/resize. */
if (state == WL_KEYBOARD_KEY_STATE_PRESSED && toplevel) {
if (key == 50) /* 'M': ask to be maximized */
xdg_toplevel_set_maximized(toplevel);
else if (key == 22) /* 'U': ask to un-maximize (must be refused) */
xdg_toplevel_unset_maximized(toplevel);
else if (key == 49) /* 'N': ask to minimize (must be ignored) */
xdg_toplevel_set_minimized(toplevel);
else if (key == 47 && seat) /* 'V': interactive move (ignored) */
xdg_toplevel_move(toplevel, seat, serial);
else if (key == 19 && seat) /* 'R': interactive resize (ignored) */
xdg_toplevel_resize(toplevel, seat, serial,
XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT);
}
/* '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;
}
/* 'G' (evdev 34) destroys the xdg-dialog object, letting tests prove
the compositor unwires a modal dialog mid-session. */
if (key == 34 && state == WL_KEYBOARD_KEY_STATE_PRESSED && dialog_obj) {
xdg_dialog_v1_destroy(dialog_obj);
dialog_obj = NULL;
}
/* 'D' (evdev 32) disables the text-input field, letting tests prove
the compositor relays the disable to the input method. */
if (key == 32 && state == WL_KEYBOARD_KEY_STATE_PRESSED && ti_obj) {
zwp_text_input_v3_disable(ti_obj);
zwp_text_input_v3_commit(ti_obj);
}
/* 'C' (evdev 46) drops the pointer constraint, letting tests prove
the compositor tears the active constraint down mid-session. */
if (key == 46 && state == WL_KEYBOARD_KEY_STATE_PRESSED) {
if (locked_ptr) {
zwp_locked_pointer_v1_destroy(locked_ptr);
locked_ptr = NULL;
}
if (confined_ptr) {
zwp_confined_pointer_v1_destroy(confined_ptr);
confined_ptr = 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);
if (pointer_log) {
fprintf(pointer_log, "enter %d %d\n",
(int)wl_fixed_to_double(sx), (int)wl_fixed_to_double(sy));
fflush(pointer_log);
}
/* 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;
if (pointer_log) {
fprintf(pointer_log, "motion %d %d\n",
(int)wl_fixed_to_double(sx), (int)wl_fixed_to_double(sy));
fflush(pointer_log);
}
}
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;
if (pointer_log) {
fprintf(pointer_log, "button %u %u\n", button, state);
fflush(pointer_log);
}
}
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();
if (single_pixel_mode && viewport)
wp_viewport_set_destination(viewport, W, H);
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;
if (extra_delay_ms > 0) {
/* Let boot settle (main window mapped, tabs crowned) before
the second window arrives; flush first so the main commit
is not held back by the sleep. */
wl_display_flush(display);
usleep((useconds_t)extra_delay_ms * 1000);
}
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;
if (w > 0 && h > 0) { W = w; H = h; }
if (req_out) {
/* --req-out: log configure state, so tests can prove the
compositor forced maximization back on after an unset request
and applied a taskbar-driven fullscreen request. */
bool max = false, fs = false;
uint32_t *st;
wl_array_for_each(st, states) {
if (*st == XDG_TOPLEVEL_STATE_MAXIMIZED) max = true;
if (*st == XDG_TOPLEVEL_STATE_FULLSCREEN) fs = true;
}
FILE *f = fopen(req_out, "a");
if (f) {
fprintf(f, "maximized=%d fullscreen=%d\n", (int)max, (int)fs);
fclose(f);
}
}
}
static void xdg_toplevel_close(void *data, struct xdg_toplevel *xp) {
(void)data;
/* --ignore-close: swallow the compositor's close request instead of
exiting, so tests can prove the SIGKILL escalation fires for a
client that ignores both SIGTERM and the close event. */
if (ignore_close)
return;
/* 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) {
/* xdg_popup.reposition needs wm_base (and hence popup) version
3+, but the default v1 bind is kept for every other mode so no
existing test changes behavior. */
int wm_want = popup_reposition ? 3 : 1;
wm_base = wl_registry_bind(reg, name, &xdg_wm_base_interface,
version < wm_want ? version : wm_want);
} 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,
ext_data_control_manager_v1_interface.name) == 0) {
dc_mgr = wl_registry_bind(reg, name,
&ext_data_control_manager_v1_interface,
version < 2 ? version : 2);
} 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);
} else if (strcmp(interface,
ext_session_lock_manager_v1_interface.name) == 0) {
lock_mgr = wl_registry_bind(reg, name,
&ext_session_lock_manager_v1_interface,
version < 1 ? version : 1);
} else if (strcmp(interface, wl_output_interface.name) == 0) {
if (!wl_output)
wl_output = wl_registry_bind(reg, name, &wl_output_interface,
version < 2 ? version : 2);
} else if (strcmp(interface,
ext_output_image_capture_source_manager_v1_interface.name) == 0) {
sc_out_mgr = wl_registry_bind(reg, name,
&ext_output_image_capture_source_manager_v1_interface,
version < 1 ? version : 1);
} else if (strcmp(interface,
ext_image_copy_capture_manager_v1_interface.name) == 0) {
sc_mgr = wl_registry_bind(reg, name,
&ext_image_copy_capture_manager_v1_interface,
version < 1 ? version : 1);
} else if (strcmp(interface,
zwp_keyboard_shortcuts_inhibit_manager_v1_interface.name) == 0) {
kb_inhibit_mgr = wl_registry_bind(reg, name,
&zwp_keyboard_shortcuts_inhibit_manager_v1_interface,
version < 1 ? version : 1);
} else if (strcmp(interface,
zwlr_output_power_manager_v1_interface.name) == 0) {
power_mgr = wl_registry_bind(reg, name,
&zwlr_output_power_manager_v1_interface,
version < 1 ? version : 1);
} else if (strcmp(interface,
zwp_virtual_keyboard_manager_v1_interface.name) == 0) {
virtual_kb_mgr = wl_registry_bind(reg, name,
&zwp_virtual_keyboard_manager_v1_interface,
version < 1 ? version : 1);
} else if (strcmp(interface,
zwlr_virtual_pointer_manager_v1_interface.name) == 0) {
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);
} else if (strcmp(interface, xdg_wm_dialog_v1_interface.name) == 0) {
dialog_mgr = wl_registry_bind(reg, name, &xdg_wm_dialog_v1_interface,
version < 1 ? version : 1);
} else if (strcmp(interface,
zwp_pointer_gestures_v1_interface.name) == 0) {
/* v3 carries hold gestures alongside swipe/pinch. */
gestures_mgr = wl_registry_bind(reg, name,
&zwp_pointer_gestures_v1_interface,
version < 3 ? version : 3);
} else if (strcmp(interface,
xdg_toplevel_icon_manager_v1_interface.name) == 0) {
toplevel_icon_mgr = wl_registry_bind(reg, name,
&xdg_toplevel_icon_manager_v1_interface,
version < 1 ? version : 1);
} else if (strcmp(interface,
zwlr_output_manager_v1_interface.name) == 0) {
output_mgr = wl_registry_bind(reg, name,
&zwlr_output_manager_v1_interface,
version < 4 ? version : 4);
} else if (strcmp(interface,
xdg_toplevel_tag_manager_v1_interface.name) == 0) {
tag_mgr = wl_registry_bind(reg, name,
&xdg_toplevel_tag_manager_v1_interface,
version < 1 ? version : 1);
} else if (strcmp(interface,
wp_color_representation_manager_v1_interface.name) == 0) {
color_repr_mgr = wl_registry_bind(reg, name,
&wp_color_representation_manager_v1_interface,
version < 1 ? version : 1);
color_repr_bind_listener();
} else if (strcmp(interface,
wp_tearing_control_manager_v1_interface.name) == 0) {
tearing_mgr = wl_registry_bind(reg, name,
&wp_tearing_control_manager_v1_interface,
version < 1 ? version : 1);
} else if (strcmp(interface,
wp_fractional_scale_manager_v1_interface.name) == 0) {
fractional_mgr = wl_registry_bind(reg, name,
&wp_fractional_scale_manager_v1_interface,
version < 1 ? version : 1);
} else if (strcmp(interface,
zwp_text_input_manager_v3_interface.name) == 0) {
text_input_mgr = wl_registry_bind(reg, name,
&zwp_text_input_manager_v3_interface,
version < 1 ? version : 1);
} else if (strcmp(interface,
zwp_input_method_manager_v2_interface.name) == 0) {
input_method_mgr = wl_registry_bind(reg, name,
&zwp_input_method_manager_v2_interface,
version < 1 ? version : 1);
} else if (strcmp(interface,
wp_content_type_manager_v1_interface.name) == 0) {
content_type_mgr = wl_registry_bind(reg, name,
&wp_content_type_manager_v1_interface,
version < 1 ? version : 1);
} else if (strcmp(interface,
wp_single_pixel_buffer_manager_v1_interface.name) == 0) {
single_pixel_mgr = wl_registry_bind(reg, name,
&wp_single_pixel_buffer_manager_v1_interface,
version < 1 ? version : 1);
} else if (strcmp(interface, wp_viewporter_interface.name) == 0) {
viewporter = wl_registry_bind(reg, name, &wp_viewporter_interface,
version < 1 ? version : 1);
} else if (strcmp(interface,
zwlr_gamma_control_manager_v1_interface.name) == 0) {
gamma_mgr = wl_registry_bind(reg, name,
&zwlr_gamma_control_manager_v1_interface,
version < 1 ? version : 1);
} else if (strcmp(interface, xdg_system_bell_v1_interface.name) == 0) {
bell_mgr = wl_registry_bind(reg, name,
&xdg_system_bell_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) {
if (single_pixel_mode) {
/* 1x1 premultiplied solid color; the compositor scales it to the
surface. Follows the shm path's deliberate leak: the previous
buffer stays alive while the compositor may still show it. */
if (!single_pixel_mgr) {
fprintf(stderr, "wltest: single-pixel: manager missing\n");
exit(1);
}
uint32_t a8 = PIXEL[3];
uint32_t r8 = (uint32_t)PIXEL[0] * a8 / 255;
uint32_t g8 = (uint32_t)PIXEL[1] * a8 / 255;
uint32_t b8 = (uint32_t)PIXEL[2] * a8 / 255;
/* 64-bit: r8 * 0xffffffff overflows 32 bits (255 * 0xffffffff
wraps to 0xffffff01, i.e. nearly black). */
buffer = wp_single_pixel_buffer_manager_v1_create_u32_rgba_buffer(
single_pixel_mgr, (uint32_t)((uint64_t)r8 * 0xffffffffu / 255),
(uint32_t)((uint64_t)g8 * 0xffffffffu / 255),
(uint32_t)((uint64_t)b8 * 0xffffffffu / 255),
(uint32_t)((uint64_t)a8 * 0xffffffffu / 255));
return;
}
/* 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);
if (extra_modal) {
if (!dialog_mgr) {
fprintf(stderr, "wltest: extra-modal: xdg_wm_dialog_v1 missing\n");
exit(1);
}
struct xdg_dialog_v1 *d =
xdg_wm_dialog_v1_get_xdg_dialog(dialog_mgr, ex_toplevel);
xdg_dialog_v1_set_modal(d);
}
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);
}
/* Serve one data-control send: write the setter text (carried as the
listener data) and close, like wl-copy -f does. */
static void dc_source_send(void *data, struct ext_data_control_source_v1 *s,
const char *mime, int32_t fd) {
(void)s;
fprintf(stderr, "wltest: dc source send mime=%s\n", mime);
const char *text = data ? data : "";
size_t n = strlen(text);
if (n > 0 && write(fd, text, n) < 0) {
/* best effort; the reader will see a short/empty transfer */
}
close(fd);
}
/* A cancelled source (someone else took the selection) ends the setter,
the way wl-copy -f exits when its offer is stolen. */
static void dc_source_cancelled(void *data,
struct ext_data_control_source_v1 *s) {
(void)data; (void)s;
fprintf(stderr, "wltest: dc source cancelled\n");
exit(0);
}
static const struct ext_data_control_source_v1_listener dc_source_listener = {
.send = dc_source_send,
.cancelled = dc_source_cancelled,
};
static void dc_offer_mime(void *data, struct ext_data_control_offer_v1 *o,
const char *mime) {
(void)data;
fprintf(stderr, "wltest: dc offer mime=%s\n", mime);
if (o == dc_last_offer && strcmp(mime, "text/plain") == 0)
dc_last_text = true;
}
static const struct ext_data_control_offer_v1_listener dc_offer_listener = {
.offer = dc_offer_mime,
};
static void dc_device_data_offer(void *data,
struct ext_data_control_device_v1 *d,
struct ext_data_control_offer_v1 *offer) {
(void)data; (void)d;
fprintf(stderr, "wltest: dc data_offer\n");
dc_last_offer = offer;
dc_last_text = false;
ext_data_control_offer_v1_add_listener(offer, &dc_offer_listener, NULL);
}
static void dc_device_selection(void *data,
struct ext_data_control_device_v1 *d,
struct ext_data_control_offer_v1 *offer) {
(void)data; (void)d;
dc_offer = offer;
if (clip_out && dc_last_text && !dc_need_pull)
dc_need_pull = true;
}
static void dc_device_primary(void *data,
struct ext_data_control_device_v1 *d,
struct ext_data_control_offer_v1 *offer) {
(void)data; (void)d;
dc_prim_offer = offer;
if (dc_prim_out && dc_last_text && !dc_prim_need_pull)
dc_prim_need_pull = true;
}
static const struct ext_data_control_device_v1_listener dc_device_listener = {
.data_offer = dc_device_data_offer,
.selection = dc_device_selection,
.primary_selection = dc_device_primary,
};
/* Receive a data-control offer over a pipe, with the same no-event-wakeup
polling as pull_primary: the setter answers on the pipe with no display
event of ours. Abandons the pipe when a newer offer of either selection
arrives mid-pull. */
static char *pull_dc_offer(struct ext_data_control_offer_v1 *offer,
size_t *out_len) {
int pfd[2];
if (pipe(pfd) != 0) {
fprintf(stderr, "wltest: dc pipe failed\n");
exit(1);
}
ext_data_control_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 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: dc 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 (dc_need_pull || dc_prim_need_pull)
break;
}
}
close(pfd[0]);
*out_len = acc_len;
return acc;
}
/* Pull the current data-control offer and act on it, mirroring
demand_primary_once: in --clipboard-demand-expect mode keep waiting until
the payload matches (a live host may hold its own selection at boot, so
key on content, not on the first offer). is_primary selects the
clipboard vs primary offer, out path and expected text. */
static void demand_dc_once(bool is_primary) {
struct ext_data_control_offer_v1 *offer =
is_primary ? dc_prim_offer : dc_offer;
const char *out = is_primary ? dc_prim_out : clip_out;
const char *expect = is_primary ? dc_prim_expect : clip_expect;
if (is_primary)
dc_prim_need_pull = false;
else
dc_need_pull = false;
size_t len = 0;
char *payload = offer ? pull_dc_offer(offer, &len) : NULL;
if (!payload) {
payload = calloc(1, 1);
len = 0;
}
if (expect) {
if (len == strlen(expect) && memcmp(payload, expect, len) == 0) {
fprintf(stderr, "wltest: dc matched expected text\n");
write_outfile(out, payload, len);
free(payload);
exit(0);
}
fprintf(stderr, "wltest: dc got %zu bytes (not the expected text)\n",
len);
free(payload);
if (len == 0) {
/* Empty pull against a fresh mirror offer: the compositor is
still prefetching the payload, and no further offer event
will come to retrigger us. Poll briefly for the racing
fetch before going back to event-driven waits. */
for (int i = 0; i < 50; i++) {
struct timespec ts = { .tv_sec = 0, .tv_nsec = 100000000 };
nanosleep(&ts, NULL);
offer = is_primary ? dc_prim_offer : dc_offer;
if (!offer)
continue;
payload = pull_dc_offer(offer, &len);
if (len == strlen(expect) &&
memcmp(payload, expect, len) == 0) {
fprintf(stderr, "wltest: dc matched expected text\n");
write_outfile(out, payload, len);
free(payload);
exit(0);
}
free(payload);
if (len > 0)
break;
}
}
return; /* keep waiting for the setter's offer */
}
write_outfile(out, payload, len);
free(payload);
exit(0);
}
/* Publish the data-control sources. No input serial is needed (that is
the point of data-control), so unlike maybe_set_primary this runs once
the device exists instead of waiting for a seat-granted serial. */
static void dc_set_selections(void) {
if (!dc_dev)
return;
if (clip_text && !dc_set_done) {
dc_set_done = true;
struct ext_data_control_source_v1 *s =
ext_data_control_manager_v1_create_data_source(dc_mgr);
ext_data_control_source_v1_offer(s, "text/plain;charset=utf-8");
ext_data_control_source_v1_offer(s, "text/plain");
ext_data_control_source_v1_add_listener(s, &dc_source_listener,
(void *)clip_text);
ext_data_control_device_v1_set_selection(dc_dev, s);
wl_display_flush(display);
}
if (dc_prim_text && !dc_prim_set_done) {
dc_prim_set_done = true;
struct ext_data_control_source_v1 *s =
ext_data_control_manager_v1_create_data_source(dc_mgr);
ext_data_control_source_v1_offer(s, "text/plain;charset=utf-8");
ext_data_control_source_v1_offer(s, "text/plain");
ext_data_control_source_v1_add_listener(s, &dc_source_listener,
(void *)dc_prim_text);
ext_data_control_device_v1_set_primary_selection(dc_dev, s);
wl_display_flush(display);
}
if (dc_clear && !dc_cleared) {
dc_cleared = true;
ext_data_control_device_v1_set_selection(dc_dev, NULL);
ext_data_control_device_v1_set_primary_selection(dc_dev, NULL);
wl_display_flush(display);
/* Make sure the server processed the unsets before we go away. */
wl_display_roundtrip(display);
exit(0);
}
}
/* --- 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;
/* --idle-inhibit-release <ms>: destroy the inhibitor <ms> after startup
(the compositor must drop the inhibition and go on to fire idle), while
the client itself stays up. */
static int idle_inhibit_release_ms = -1;
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, &registry_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);
if (idle_inhibit_release_ms >= 0) {
/* Timed release needs periodic wakeups, so poll instead of
blocking in dispatch (plain --idle-inhibit keeps the old
blocking loop untouched). */
time_t t0 = time(NULL);
for (;;) {
if (idle_inhib &&
(time(NULL) - t0) * 1000 >= idle_inhibit_release_ms) {
zwp_idle_inhibitor_v1_destroy(idle_inhib);
idle_inhib = NULL;
}
if (wl_display_dispatch_pending(display) < 0)
break;
struct pollfd pfd = { .fd = -1 };
poll(&pfd, 0, 200);
if (wl_display_roundtrip(display) < 0)
break;
}
return;
}
for (;;) {
if (wl_display_dispatch(display) < 0)
break;
}
}
/* --- Session lock (ext-session-lock-v1) ---
--lock [--lock-out <file>]: act as the swaylock side of the compositor.
Binds ext_session_lock_manager_v1, calls lock, waits for the compositor's
locked event, then creates a lock surface on the single wl_output and draws
a solid buffer on it (proving the compositor blanked the session and
accepted the surface). Writes "locked" then "surface-config" to --lock-out
as each stage completes; the strongest observable is the locked event the
compositor only sends once it has set up the blanked session.
--lock-second-surface additionally opens a rogue second lock surface on
the same lock (noting "second-surface"), proving the compositor survives
the extra surface and its teardown. */
static bool lock_mode;
static const char *lock_out;
static bool lock_unlock_after;
static bool lock_unlocked;
static bool lock_second_surface;
static bool lock_second_created;
static struct ext_session_lock_v1 *lock;
static struct ext_session_lock_surface_v1 *lock_surface;
static struct wl_surface *lock_wl_surface;
static bool lock_conf_acked;
static bool lock_received_locked;
static void lock_note(const char *s) {
if (lock_out) write_outfile(lock_out, s, strlen(s));
}
/* Forward declaration: lock_locked (called on the locked event) sets up the
lock surface and subscribes to its configure callback. */
static const struct ext_session_lock_surface_v1_listener lock_surface_listener;
/* Type for lock-window shm buffers: solid black, so the screen is visibly
blanked while the session is locked. */
static void lock_fill_buffer(void) {
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) return;
uint8_t *data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (data != MAP_FAILED) {
memset(data, 0, size); /* opaque black (XRGB) */
munmap(data, size);
}
close(fd);
struct wl_buffer *b = wl_shm_pool_create_buffer(pool, 0, W, H,
(int32_t)stride, WL_SHM_FORMAT_XRGB8888);
wl_shm_pool_destroy(pool);
wl_surface_attach(lock_wl_surface, b, 0, 0);
wl_surface_commit(lock_wl_surface);
}
static void lock_locked(void *data, struct ext_session_lock_v1 *l) {
(void)data; (void)l;
lock_received_locked = true;
lock_note("locked");
/* The compositor accepted the lock; create a private surface on the
output so the session is visibly blanked and the compositor gets a
surface to focus. */
if (W == 0) W = 64;
if (H == 0) H = 64;
lock_wl_surface = wl_compositor_create_surface(compositor);
if (!lock_wl_surface || !wl_output) {
lock_note("surface-fail");
return;
}
lock_surface = ext_session_lock_v1_get_lock_surface(lock, lock_wl_surface,
wl_output);
ext_session_lock_surface_v1_add_listener(lock_surface,
&lock_surface_listener, NULL);
}
static void lock_finished(void *data, struct ext_session_lock_v1 *l) {
(void)data; (void)l;
lock_note("finished");
/* --lock-unlock mode is done once the compositor confirms the teardown:
exit so the test can assert the round trip completed. */
if (lock_unlock_after)
exit(0);
}
static const struct ext_session_lock_v1_listener lock_listener = {
.locked = lock_locked,
.finished = lock_finished,
};
static void lock_surface_configure(void *data,
struct ext_session_lock_surface_v1 *l,
uint32_t serial, uint32_t w, uint32_t h) {
(void)data;
if (w > 0 && h > 0) { W = (int)w; H = (int)h; }
lock_conf_acked = true;
ext_session_lock_surface_v1_ack_configure(l, serial);
lock_fill_buffer();
lock_note("surface-config");
/* Rogue-client probe: a second lock surface on the same lock. Created
once, before any unlock, so the compositor must survive the extra
new_surface plus the teardown of both. The first surface's protocol
object is then dropped while the second stays live, so its destroy
runs against a live lock. */
if (lock_second_surface && !lock_second_created) {
lock_second_created = true;
struct wl_surface *s2 = wl_compositor_create_surface(compositor);
if (s2 && wl_output) {
struct ext_session_lock_surface_v1 *ls2 =
ext_session_lock_v1_get_lock_surface(lock, s2, wl_output);
if (ls2) {
ext_session_lock_surface_v1_add_listener(ls2,
&lock_surface_listener, NULL);
lock_note("second-surface");
/* Also on stderr (the session log at KICK_LOG=2): the
out file only keeps the last note, so a later
unlocked-exit would hide this milestone from polls. */
fprintf(stderr, "wltest: second lock surface created\n");
ext_session_lock_surface_v1_destroy(lock_surface);
lock_surface = ls2;
lock_note("first-destroyed");
}
}
}
if (lock_unlock_after && !lock_unlocked) {
lock_unlocked = true;
ext_session_lock_v1_unlock_and_destroy(lock);
/* The unlock reply is the unblanked screen itself
(unlock_and_destroy leaves no proxy for a finished event):
flush the request, let the compositor tear down, then exit so
the test sees the round trip end. */
wl_display_flush(display);
sleep(2);
lock_note("unlocked-exit");
exit(0);
}
}
static const struct ext_session_lock_surface_v1_listener lock_surface_listener = {
.configure = lock_surface_configure,
};
static void lock_run(void) {
display = wl_display_connect(NULL);
if (!display) {
fprintf(stderr, "wltest: lock: 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 (!lock_mgr) {
fprintf(stderr, "wltest: lock: ext_session_lock_manager_v1 not advertised\n");
exit(1);
}
lock = ext_session_lock_manager_v1_lock(lock_mgr);
ext_session_lock_v1_add_listener(lock, &lock_listener, NULL);
for (;;) {
if (wl_display_dispatch(display) < 0)
break;
}
}
/* --- Screen capture (ext-image-copy-capture-v1 / ext-image-capture-source-v1) ---
--screencopy [--screencopy-out <file>]: ask the compositor to capture the
single output into a fresh shm buffer the same size as the current target
window (the red test surface), then write "ready-<w>x<h>/<1st-byte>".
Proves kick advertises the capture globals and that wlroots can copy a
rendered output frame into the client's buffer: the captured geometry
matching the output (the maximized red window) is what confirms the
composited frame was delivered. The trailing byte is a peek at the first
captured pixel for debugging only, not asserted. */
static bool screencopy_mode;
static const char *screencopy_out;
static struct ext_image_capture_source_v1 *sc_source;
static struct ext_image_copy_capture_session_v1 *sc_session;
static struct ext_image_copy_capture_frame_v1 *sc_frame;
static bool sc_have_constraints, sc_ready, sc_failed;
static uint32_t sc_fmt, sc_w, sc_h, sc_stride;
static uint8_t *sc_map;
static void sc_note(const char *s) {
if (screencopy_out) write_outfile(screencopy_out, s, strlen(s));
}
/* The session constraint events give us the target geometry and a usable
shm format; buffered until the done event tells us the batch is complete. */
static void sc_session_on_buffer_size(void *data,
struct ext_image_copy_capture_session_v1 *s,
uint32_t width, uint32_t height) {
(void)data; (void)s;
sc_w = width; sc_h = height;
}
static void sc_session_on_shm_format(void *data,
struct ext_image_copy_capture_session_v1 *s,
uint32_t format) {
(void)data; (void)s;
if (sc_fmt == 0)
sc_fmt = format;
}
static void sc_session_on_dmabuf_device(void *data,
struct ext_image_copy_capture_session_v1 *s,
struct wl_array *device) {
(void)data; (void)s; (void)device;
}
static void sc_session_on_dmabuf_format(void *data,
struct ext_image_copy_capture_session_v1 *s,
uint32_t format, struct wl_array *modifiers) {
(void)data; (void)s; (void)format; (void)modifiers;
}
static void sc_session_on_done(void *data,
struct ext_image_copy_capture_session_v1 *s) {
(void)data; (void)s;
sc_have_constraints = true;
}
static void sc_session_on_stopped(void *data,
struct ext_image_copy_capture_session_v1 *s) {
(void)data; (void)s;
sc_failed = true;
sc_note("stopped");
}
static const struct ext_image_copy_capture_session_v1_listener sc_session_listener = {
.buffer_size = sc_session_on_buffer_size,
.shm_format = sc_session_on_shm_format,
.dmabuf_device = sc_session_on_dmabuf_device,
.dmabuf_format = sc_session_on_dmabuf_format,
.done = sc_session_on_done,
.stopped = sc_session_on_stopped,
};
static void sc_start_capture(void);
static void sc_frame_on_ready(void *data, struct ext_image_copy_capture_frame_v1 *f) {
(void)data; (void)f;
sc_ready = true;
/* The trailing "/<byte>" is a peek at the first captured pixel for
debugging (the software cursor may sit over the top-left corner, so it
is not asserted). The ",black" / ",content" suffix scans every pixel's
RGB: all-zero means the compositor delivered a truly blank frame (used
by the output-power test to prove the screen went black). */
unsigned first = sc_map ? sc_map[2] : 0;
bool black = true;
if (sc_map) {
for (uint32_t y = 0; y < sc_h && black; y++) {
uint8_t *row = sc_map + (size_t)y * sc_stride;
for (uint32_t x = 0; x < sc_w; x++) {
if (row[x*4+0] || row[x*4+1] || row[x*4+2]) {
black = false;
break;
}
}
}
}
char buf[64];
snprintf(buf, sizeof(buf), "ready-%ux%u/%02x,%s", sc_w, sc_h, first,
black ? "black" : "content");
sc_note(buf);
}
static void sc_frame_on_failed(void *data, struct ext_image_copy_capture_frame_v1 *f,
uint32_t reason) {
(void)data; (void)f; (void)reason;
sc_failed = true;
sc_note("failed");
}
static void sc_frame_on_transform(void *data, struct ext_image_copy_capture_frame_v1 *f,
uint32_t transform) {
(void)data; (void)f; (void)transform;
}
static void sc_frame_on_damage(void *data, struct ext_image_copy_capture_frame_v1 *f,
int32_t x, int32_t y, int32_t width, int32_t height) {
(void)data; (void)f; (void)x; (void)y; (void)width; (void)height;
}
static void sc_frame_on_presentation_time(void *data, struct ext_image_copy_capture_frame_v1 *f,
uint32_t s_hi, uint32_t s_lo, uint32_t ns) {
(void)data; (void)f; (void)s_hi; (void)s_lo; (void)ns;
}
static const struct ext_image_copy_capture_frame_v1_listener sc_frame_listener = {
.transform = sc_frame_on_transform,
.damage = sc_frame_on_damage,
.presentation_time = sc_frame_on_presentation_time,
.ready = sc_frame_on_ready,
.failed = sc_frame_on_failed,
};
/* Once the constraints are known, allocate the shm buffer, attach it to a
fresh frame, damage it fully and ask for a capture. */
static void sc_start_capture(void) {
if (!sc_have_constraints || sc_w == 0 || sc_h == 0 || sc_fmt == 0)
return;
sc_stride = sc_w * 4;
size_t size = (size_t)sc_stride * sc_h;
int fd;
struct wl_shm_pool *pool = make_pool(size, &fd);
if (!pool) { sc_note("shm-fail"); sc_failed = true; return; }
sc_map = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (sc_map == MAP_FAILED) {
sc_map = NULL;
close(fd);
wl_shm_pool_destroy(pool);
sc_note("mmap-fail");
sc_failed = true;
return;
}
struct wl_buffer *buf = wl_shm_pool_create_buffer(pool, 0, (int32_t)sc_w,
(int32_t)sc_h,
(int32_t)sc_stride,
sc_fmt);
wl_shm_pool_destroy(pool);
sc_frame = ext_image_copy_capture_session_v1_create_frame(sc_session);
ext_image_copy_capture_frame_v1_add_listener(sc_frame, &sc_frame_listener, NULL);
ext_image_copy_capture_frame_v1_attach_buffer(sc_frame, buf);
ext_image_copy_capture_frame_v1_damage_buffer(sc_frame, 0, 0,
(int32_t)sc_w, (int32_t)sc_h);
ext_image_copy_capture_frame_v1_capture(sc_frame);
}
static void screencopy_run(void) {
display = wl_display_connect(NULL);
if (!display) {
fprintf(stderr, "wltest: screencopy: 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 (!sc_mgr || !sc_out_mgr || !wl_output) {
fprintf(stderr, "wltest: screencopy: global not advertised\n");
exit(1);
}
/* The test window (the red target) is spawned separately and fills the
output; capture the whole output. */
sc_source = ext_output_image_capture_source_manager_v1_create_source(
sc_out_mgr, wl_output);
sc_session = ext_image_copy_capture_manager_v1_create_session(sc_mgr,
sc_source, 0);
ext_image_copy_capture_session_v1_add_listener(sc_session,
&sc_session_listener, NULL);
for (;;) {
if (wl_display_dispatch(display) < 0)
break;
if (sc_have_constraints && !sc_frame)
sc_start_capture();
if (sc_ready || sc_failed)
break;
}
}
/* --- Keyboard shortcuts inhibit (zwp_keyboard_shortcuts_inhibit_v1) ---
--inhibit [--inhibit-out <file>]: act as a fullscreen game that grabs the
compositor's shortcuts. Creates a normal window (so it takes keyboard
focus), then inhibits shortcuts on its surface+seat. Writes "active" to
--inhibit-out when the compositor honors it (proving kick activates the
inhibitor for the focused surface), "inactive" if focus moves away. */
static bool inhibit_mode;
static const char *inhibit_out;
static struct zwp_keyboard_shortcuts_inhibitor_v1 *kb_inhibitor;
static void kb_inhibit_note(const char *s) {
if (inhibit_out) write_outfile(inhibit_out, s, strlen(s));
}
static void kb_inhibitor_active(void *data,
struct zwp_keyboard_shortcuts_inhibitor_v1 *inhib) {
(void)data; (void)inhib;
kb_inhibit_note("active");
}
static void kb_inhibitor_inactive(void *data,
struct zwp_keyboard_shortcuts_inhibitor_v1 *inhib) {
(void)data; (void)inhib;
kb_inhibit_note("inactive");
}
static const struct zwp_keyboard_shortcuts_inhibitor_v1_listener kb_inhibitor_listener = {
.active = kb_inhibitor_active,
.inactive = kb_inhibitor_inactive,
};
static void inhibit_run(void) {
display = wl_display_connect(NULL);
if (!display) {
fprintf(stderr, "wltest: inhibit: 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 (!kb_inhibit_mgr || !seat) {
fprintf(stderr, "wltest: inhibit: manager or seat not advertised\n");
exit(1);
}
/* Need a mapped window to hold keyboard focus: draw a small buffer so
the compositor focuses it, then inhibit on its surface+seat. */
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-inhibit");
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);
wl_surface_commit(surface);
wl_display_roundtrip(display);
kb_inhibitor = zwp_keyboard_shortcuts_inhibit_manager_v1_inhibit_shortcuts(
kb_inhibit_mgr, surface, seat);
zwp_keyboard_shortcuts_inhibitor_v1_add_listener(kb_inhibitor,
&kb_inhibitor_listener, NULL);
for (;;) {
if (wl_display_dispatch(display) < 0)
break;
}
}
/* --- Pointer gestures (touchpad swipe/pinch/hold) ---
--gesture-log <file>: bind the gestures manager for the seat's pointer
and append every received gesture event ("swipe begin fingers=3" ...).
The compositor only routes gestures to the client holding seat pointer
focus, so the driver must warp the pointer onto this window first. */
static const char *gesture_log_path;
static FILE *gesture_log;
static void gesture_note(const char *fmt, ...) {
if (!gesture_log)
return;
va_list ap;
va_start(ap, fmt);
vfprintf(gesture_log, fmt, ap);
va_end(ap);
fputc('\n', gesture_log);
fflush(gesture_log);
}
static void gesture_swipe_begin(void *data,
struct zwp_pointer_gesture_swipe_v1 *sw, uint32_t serial,
uint32_t time, struct wl_surface *srf, uint32_t fingers) {
(void)data; (void)sw; (void)serial; (void)time; (void)srf;
gesture_note("swipe begin fingers=%u", fingers);
}
static void gesture_swipe_update(void *data,
struct zwp_pointer_gesture_swipe_v1 *sw, uint32_t time,
wl_fixed_t dx, wl_fixed_t dy) {
(void)data; (void)sw; (void)time;
gesture_note("swipe update dx=%g dy=%g",
wl_fixed_to_double(dx), wl_fixed_to_double(dy));
}
static void gesture_swipe_end(void *data,
struct zwp_pointer_gesture_swipe_v1 *sw, uint32_t serial,
uint32_t time, int32_t cancelled) {
(void)data; (void)sw; (void)serial; (void)time;
gesture_note("swipe end cancelled=%u", cancelled);
}
static const struct zwp_pointer_gesture_swipe_v1_listener swipe_listener = {
.begin = gesture_swipe_begin,
.update = gesture_swipe_update,
.end = gesture_swipe_end,
};
static void gesture_pinch_begin(void *data,
struct zwp_pointer_gesture_pinch_v1 *pi, uint32_t serial,
uint32_t time, struct wl_surface *srf, uint32_t fingers) {
(void)data; (void)pi; (void)serial; (void)time; (void)srf;
gesture_note("pinch begin fingers=%u", fingers);
}
static void gesture_pinch_update(void *data,
struct zwp_pointer_gesture_pinch_v1 *pi, uint32_t time,
wl_fixed_t dx, wl_fixed_t dy, wl_fixed_t scale,
wl_fixed_t rotation) {
(void)data; (void)pi; (void)time;
gesture_note("pinch update dx=%g dy=%g scale=%g rotation=%g",
wl_fixed_to_double(dx), wl_fixed_to_double(dy),
wl_fixed_to_double(scale), wl_fixed_to_double(rotation));
}
static void gesture_pinch_end(void *data,
struct zwp_pointer_gesture_pinch_v1 *pi, uint32_t serial,
uint32_t time, int32_t cancelled) {
(void)data; (void)pi; (void)serial; (void)time;
gesture_note("pinch end cancelled=%u", cancelled);
}
static const struct zwp_pointer_gesture_pinch_v1_listener pinch_listener = {
.begin = gesture_pinch_begin,
.update = gesture_pinch_update,
.end = gesture_pinch_end,
};
static void gesture_hold_begin(void *data,
struct zwp_pointer_gesture_hold_v1 *ho, uint32_t serial,
uint32_t time, struct wl_surface *srf, uint32_t fingers) {
(void)data; (void)ho; (void)serial; (void)time; (void)srf;
gesture_note("hold begin fingers=%u", fingers);
}
static void gesture_hold_end(void *data,
struct zwp_pointer_gesture_hold_v1 *ho, uint32_t serial,
uint32_t time, int32_t cancelled) {
(void)data; (void)ho; (void)serial; (void)time;
gesture_note("hold end cancelled=%u", cancelled);
}
static const struct zwp_pointer_gesture_hold_v1_listener hold_listener = {
.begin = gesture_hold_begin,
.end = gesture_hold_end,
};
/* Bind all three gesture kinds for the seat pointer once it exists. */
static void gesture_bind_all(void) {
if (!gestures_mgr || !pointer || !gesture_log_path)
return;
if (!gesture_log) {
gesture_log = fopen(gesture_log_path, "w");
if (!gesture_log) {
fprintf(stderr, "wltest: cannot write gesture log %s\n",
gesture_log_path);
exit(1);
}
}
struct zwp_pointer_gesture_swipe_v1 *sw =
zwp_pointer_gestures_v1_get_swipe_gesture(gestures_mgr, pointer);
zwp_pointer_gesture_swipe_v1_add_listener(sw, &swipe_listener, NULL);
struct zwp_pointer_gesture_pinch_v1 *pi =
zwp_pointer_gestures_v1_get_pinch_gesture(gestures_mgr, pointer);
zwp_pointer_gesture_pinch_v1_add_listener(pi, &pinch_listener, NULL);
struct zwp_pointer_gesture_hold_v1 *ho =
zwp_pointer_gestures_v1_get_hold_gesture(gestures_mgr, pointer);
zwp_pointer_gesture_hold_v1_add_listener(ho, &hold_listener, NULL);
}
/* --- Output management (wlr-output-management) ---
--output-mgmt --output-mgmt-out <file> [--output-mgmt-size WxH]:
list the head (name + current size) into <file>; with a size, apply
it as a custom mode first and record before/after/applied.
--output-mgmt-test probes instead of applying (records "tested").
No window is opened. */
static bool output_mgmt_mode;
static bool output_mgmt_test_only;
static const char *output_mgmt_out;static int output_mgmt_w, output_mgmt_h;
static bool output_mgmt_want_size;
static struct zwlr_output_head_v1 *om_head;
static char om_name[64];
static bool om_applied;
static struct {
struct zwlr_output_mode_v1 *res;
int w, h;
} om_modes[32];
static int om_nmodes;
static struct zwlr_output_mode_v1 *om_current;
static const struct zwlr_output_head_v1_listener om_head_listener;
static const struct zwlr_output_mode_v1_listener om_mode_listener;
static void om_note(const char *fmt, ...) {
if (!output_mgmt_out)
return;
FILE *f = fopen(output_mgmt_out, "a");
if (!f)
return;
va_list ap;
va_start(ap, fmt);
vfprintf(f, fmt, ap);
va_end(ap);
fputc('\n', f);
fclose(f);
}
static int om_mode_size(struct zwlr_output_mode_v1 *res, int *w, int *h) {
for (int i = 0; i < om_nmodes; i++)
if (om_modes[i].res == res) {
*w = om_modes[i].w;
*h = om_modes[i].h;
return 0;
}
return -1;
}
static void om_report_head(const char *tag) {
int w = 0, h = 0;
if (om_mode_size(om_current, &w, &h) == 0)
om_note("%s %s %dx%d", tag, om_name[0] ? om_name : "?", w, h);
else
om_note("%s %s ?", tag, om_name[0] ? om_name : "?");
}
static void om_config_succeeded(void *data,
struct zwlr_output_configuration_v1 *cfg) {
(void)data; (void)cfg;
om_note(output_mgmt_test_only ? "tested" : "applied");
if (output_mgmt_test_only)
exit(0);
om_applied = true;
}
static void om_config_failed(void *data,
struct zwlr_output_configuration_v1 *cfg) {
(void)data; (void)cfg;
om_note("failed");
exit(0);
}
static void om_config_cancelled(void *data,
struct zwlr_output_configuration_v1 *cfg) {
(void)data; (void)cfg;
/* Superseded by a newer configuration (e.g. a concurrent modeset):
wait for the fresh done and retry with its serial, bounded. */
static int retries;
if (++retries > 3) {
om_note("cancelled");
exit(1);
}
om_applied = false;
}
static const struct zwlr_output_configuration_v1_listener om_config_listener = {
.succeeded = om_config_succeeded,
.failed = om_config_failed,
.cancelled = om_config_cancelled,
};
static void om_mgr_head(void *data, struct zwlr_output_manager_v1 *mgr,
struct zwlr_output_head_v1 *head) {
(void)data; (void)mgr;
if (!om_head) {
om_head = head;
zwlr_output_head_v1_add_listener(head, &om_head_listener, NULL);
}
}
static void om_head_name(void *data, struct zwlr_output_head_v1 *head,
const char *name) {
(void)data; (void)head;
snprintf(om_name, sizeof(om_name), "%s", name ? name : "");
}
static void om_noop_head_str(void *data, struct zwlr_output_head_v1 *head,
const char *s) {
(void)data; (void)head; (void)s;
}
static void om_noop_head_u32(void *data, struct zwlr_output_head_v1 *head,
uint32_t v) {
(void)data; (void)head; (void)v;
}
static void om_noop_head_i32x2(void *data, struct zwlr_output_head_v1 *head,
int32_t a, int32_t b) {
(void)data; (void)head; (void)a; (void)b;
}
static void om_noop_head_i32(void *data, struct zwlr_output_head_v1 *head,
int32_t v) {
(void)data; (void)head; (void)v;
}
static void om_head_mode(void *data, struct zwlr_output_head_v1 *head,
struct zwlr_output_mode_v1 *mode) {
(void)data; (void)head;
if (om_nmodes < 32) {
om_modes[om_nmodes].res = mode;
om_modes[om_nmodes].w = 0;
om_modes[om_nmodes].h = 0;
om_nmodes++;
zwlr_output_mode_v1_add_listener(mode, &om_mode_listener, NULL);
}
}
static void om_head_current_mode(void *data,
struct zwlr_output_head_v1 *head,
struct zwlr_output_mode_v1 *mode) {
(void)data; (void)head;
om_current = mode;
}
static void om_head_finished(void *data, struct zwlr_output_head_v1 *head) {
(void)data; (void)head;
}
static const struct zwlr_output_head_v1_listener om_head_listener = {
.name = om_head_name,
.description = om_noop_head_str,
.physical_size = om_noop_head_i32x2,
.mode = om_head_mode,
.enabled = om_noop_head_i32,
.current_mode = om_head_current_mode,
.position = om_noop_head_i32x2,
.transform = om_noop_head_i32,
.scale = om_noop_head_i32,
.finished = om_head_finished,
.make = om_noop_head_str,
.model = om_noop_head_str,
.serial_number = om_noop_head_str,
.adaptive_sync = om_noop_head_u32,
};
static void om_mode_size_ev(void *data, struct zwlr_output_mode_v1 *mode,
int32_t w, int32_t h) {
(void)data;
for (int i = 0; i < om_nmodes; i++)
if (om_modes[i].res == mode) {
om_modes[i].w = w;
om_modes[i].h = h;
return;
}
}
static void om_noop_mode_i32(void *data, struct zwlr_output_mode_v1 *mode,
int32_t v) {
(void)data; (void)mode; (void)v;
}
static void om_noop_mode_void(void *data, struct zwlr_output_mode_v1 *mode) {
(void)data; (void)mode;
}
static const struct zwlr_output_mode_v1_listener om_mode_listener = {
.size = om_mode_size_ev,
.refresh = om_noop_mode_i32,
.preferred = om_noop_mode_void,
.finished = om_noop_mode_void,
};
static void om_mgr_done(void *data, struct zwlr_output_manager_v1 *mgr,
uint32_t serial) {
(void)data; (void)mgr;
if (output_mgmt_want_size && !om_applied && om_head) {
int w = 0, h = 0;
if (om_mode_size(om_current, &w, &h) == 0 &&
(w != output_mgmt_w || h != output_mgmt_h)) {
om_report_head("before");
struct zwlr_output_configuration_v1 *cfg =
zwlr_output_manager_v1_create_configuration(output_mgr,
serial);
zwlr_output_configuration_v1_add_listener(cfg,
&om_config_listener, NULL);
struct zwlr_output_configuration_head_v1 *chead =
zwlr_output_configuration_v1_enable_head(cfg, om_head);
zwlr_output_configuration_head_v1_set_custom_mode(
chead, output_mgmt_w, output_mgmt_h, 0);
if (output_mgmt_test_only)
zwlr_output_configuration_v1_test(cfg);
else
zwlr_output_configuration_v1_apply(cfg);
wl_display_flush(display);
return;
}
}
om_report_head(om_applied ? "after" : "head");
exit(0);
}
static void om_mgr_finished(void *data,
struct zwlr_output_manager_v1 *mgr) {
(void)data; (void)mgr;
}
static const struct zwlr_output_manager_v1_listener om_mgr_listener = {
.head = om_mgr_head,
.done = om_mgr_done,
.finished = om_mgr_finished,
};
static void output_mgmt_run(void) {
display = wl_display_connect(NULL);
if (!display) {
fprintf(stderr, "wltest: output-mgmt: 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 (!output_mgr) {
fprintf(stderr, "wltest: output-mgmt: manager missing\n");
exit(1);
}
zwlr_output_manager_v1_add_listener(output_mgr, &om_mgr_listener, NULL);
wl_display_roundtrip(display);
wl_display_roundtrip(display);
for (;;) {
if (wl_display_dispatch(display) < 0)
break;
}
}
/* --- Toplevel icon ---
--icon <#RRGGBB> [SIZE]: upload a SIZE×SIZE (default 8) solid-color
icon buffer and set it on the toplevel via xdg-toplevel-icon-v1. */static uint8_t ICON_PIXEL[4] = { 0x00, 0xff, 0x00, 0xff }; /* RGBA */
static int icon_size;
static bool want_icon;
static void icon_upload(void) {
if (!toplevel_icon_mgr || !shm) {
fprintf(stderr, "wltest: icon: manager or shm missing\n");
exit(1);
}
int S = icon_size > 0 ? icon_size : 8;
size_t stride = (size_t)S * 4, size = stride * (size_t)S;
int fd;
struct wl_shm_pool *pool = make_pool(size, &fd);
if (!pool) {
fprintf(stderr, "wltest: icon: shm pool failed\n");
exit(1);
}
uint8_t *d = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED,
fd, 0);
if (d == MAP_FAILED) {
fprintf(stderr, "wltest: icon: mmap failed\n");
exit(1);
}
for (int i = 0; i < S * S; i++) {
d[i*4+0] = ICON_PIXEL[2]; /* B */
d[i*4+1] = ICON_PIXEL[1]; /* G */
d[i*4+2] = ICON_PIXEL[0]; /* R */
d[i*4+3] = ICON_PIXEL[3]; /* A */
}
munmap(d, size);
close(fd);
struct wl_buffer *ib = wl_shm_pool_create_buffer(pool, 0, S, S,
(int32_t)stride, WL_SHM_FORMAT_XRGB8888);
wl_shm_pool_destroy(pool);
struct xdg_toplevel_icon_v1 *icon =
xdg_toplevel_icon_manager_v1_create_icon(toplevel_icon_mgr);
xdg_toplevel_icon_v1_add_buffer(icon, ib, 1);
xdg_toplevel_icon_manager_v1_set_icon(toplevel_icon_mgr, toplevel,
icon);
wl_display_flush(display);
}
/* --- Tearing + fractional scale ---
--tearing <vsync|async>: request that presentation hint on the toplevel
(the compositor drives its commit's async-flip flag from the shown
surface). --fractional-out <file>: write the compositor's
preferred_scale for the surface and exit. */
static int tearing_arg = -1;
static const char *fractional_out;
static void fractional_scale_ev(void *data,
struct wp_fractional_scale_v1 *fs, uint32_t scale) {
(void)data; (void)fs;
char s[32];
snprintf(s, sizeof(s), "%u", scale);
if (fractional_out) write_outfile(fractional_out, s, strlen(s));
exit(0);
}
static const struct wp_fractional_scale_v1_listener fractional_listener = {
.preferred_scale = fractional_scale_ev,
};
/* --- Color representation (wp_color_representation_manager_v1) ---
--color-probe [--color-out <file>]: bind the manager for the normal
window's surface, declare the default premultiplied alpha mode, and log
every supported alpha mode / coefficients+range pair the compositor
advertises. On done the client writes the accumulated lines plus "done"
(write_outfile truncates, so the lines accumulate in memory first) and
exits; the smoke test asserts the file holds at least one alpha mode
plus done. */
static char color_acc[512];
static size_t color_acc_len;
static void color_repr_note(const char *s) {
size_t n = strlen(s);
if (color_acc_len + n + 1 < sizeof(color_acc)) {
memcpy(color_acc + color_acc_len, s, n);
color_acc_len += n;
color_acc[color_acc_len++] = '\n';
color_acc[color_acc_len] = '\0';
}
}
static void color_repr_alpha_mode(void *data,
struct wp_color_representation_manager_v1 *mgr, uint32_t mode) {
(void)data; (void)mgr;
char s[32];
snprintf(s, sizeof(s), "alpha=%u", mode);
color_repr_note(s);
}
static void color_repr_coeffs(void *data,
struct wp_color_representation_manager_v1 *mgr, uint32_t coeffs,
uint32_t range) {
(void)data; (void)mgr;
char s[48];
snprintf(s, sizeof(s), "coeffs=%u range=%u", coeffs, range);
color_repr_note(s);
}
static void color_repr_done(void *data,
struct wp_color_representation_manager_v1 *mgr) {
(void)data; (void)mgr;
/* The manager posts done on bind for every client; only the probe
run exits on it -- normal windows just note it and carry on. */
if (!(color_probe || color_out))
return;
color_repr_note("done");
if (color_out) write_outfile(color_out, color_acc, color_acc_len);
exit(0);
}
static const struct wp_color_representation_manager_v1_listener
color_repr_listener = {
.supported_alpha_mode = color_repr_alpha_mode,
.supported_coefficients_and_ranges = color_repr_coeffs,
.done = color_repr_done,
};
/* Attach the probe listener at bind time: the manager posts its
supported-features burst (plus done) on bind, so a listener attached
after the registry roundtrip would miss it. */
static void color_repr_bind_listener(void) {
if (color_repr_mgr)
wp_color_representation_manager_v1_add_listener(color_repr_mgr,
&color_repr_listener, NULL);
}
/* --- Gamma control (wlr-gamma-control-unstable-v1) ---
--gamma --gamma-out <file>: ask for the output's gamma control. The
compositor answers gamma_size (a LUT exists, e.g. DRM) or failed (no
LUT, e.g. headless); either line is written to <file> and the client
exits. The smoke test asserts the headless failure path stays clean. */
static bool gamma_mode;
static const char *gamma_out;
static void gamma_size(void *data, struct zwlr_gamma_control_v1 *gc,
uint32_t size) {
(void)data; (void)gc;
char s[32];
snprintf(s, sizeof(s), "size=%u", size);
if (gamma_out) write_outfile(gamma_out, s, strlen(s));
exit(0);
}
static void gamma_failed(void *data, struct zwlr_gamma_control_v1 *gc) {
(void)data; (void)gc;
if (gamma_out) write_outfile(gamma_out, "failed", 6);
exit(0);
}
static const struct zwlr_gamma_control_v1_listener gamma_listener = {
.gamma_size = gamma_size,
.failed = gamma_failed,
};
static void gamma_run(void) {
display = wl_display_connect(NULL);
if (!display) {
fprintf(stderr, "wltest: gamma: 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 (!gamma_mgr || !wl_output) {
fprintf(stderr, "wltest: gamma: manager or output missing\n");
exit(1);
}
struct zwlr_gamma_control_v1 *gc =
zwlr_gamma_control_manager_v1_get_gamma_control(gamma_mgr, wl_output);
zwlr_gamma_control_v1_add_listener(gc, &gamma_listener, NULL);
wl_display_roundtrip(display);
for (;;) {
if (wl_display_dispatch(display) < 0)
break;
}
}
/* --- System bell (xdg-system-bell-v1) ---
--bell: ring the compositor's bell, then exit. The smoke test waits
for the compositor's [bell] ring line. */
static bool bell_mode;
static void bell_run(void) {
display = wl_display_connect(NULL);
if (!display) {
fprintf(stderr, "wltest: bell: 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 (!bell_mgr) {
fprintf(stderr, "wltest: bell: manager missing\n");
exit(1);
}
xdg_system_bell_v1_ring(bell_mgr, NULL);
wl_display_flush(display);
/* One round trip so a protocol error would surface before exiting. */
wl_display_roundtrip(display);
}
/* --- Output power management (zwlr_output_power_management_v1) ---
--power <on|off>: ask the compositor to blank (off) or unblank (on) the
single output, then exit. The smoke test pairs this with --screencopy to
prove the screen actually goes black and comes back. */
static const char *power_arg;
static void power_run(void) {
display = wl_display_connect(NULL);
if (!display) {
fprintf(stderr, "wltest: power: 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 (!power_mgr || !wl_output) {
fprintf(stderr, "wltest: power: manager or output not advertised\n");
exit(1);
}
struct zwlr_output_power_v1 *power =
zwlr_output_power_manager_v1_get_output_power(power_mgr, wl_output);
uint32_t mode = (power_arg && strcmp(power_arg, "off") == 0) ?
ZWLR_OUTPUT_POWER_V1_MODE_OFF : ZWLR_OUTPUT_POWER_V1_MODE_ON;
zwlr_output_power_v1_set_mode(power, mode);
wl_display_roundtrip(display);
}
/* --- Virtual keyboard (zwp_virtual_keyboard_v1) ---
--virtual-key <code:state,...>: synthesise key events through a virtual
keyboard (no keymap sent; the compositor installs its own). E.g.
"56:1,103:1,103:0,56:0" presses Alt+Up (tab-cycle) and releases.
--virtual-modifiers <mask> sends an explicit modifier state (depressed
mask) with no keys. Exits after flushing. */
static const char *virtual_key_spec;
static uint32_t virtual_mods;
static bool virtual_mods_set;
static void virtual_key_run(void) {
display = wl_display_connect(NULL);
if (!display) {
fprintf(stderr, "wltest: virtual-key: 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 (!virtual_kb_mgr || !seat) {
fprintf(stderr, "wltest: virtual-key: manager or seat missing\n");
exit(1);
}
struct zwp_virtual_keyboard_v1 *vkb =
zwp_virtual_keyboard_manager_v1_create_virtual_keyboard(
virtual_kb_mgr, seat);
/* wlroots rejects keys until a keymap is defined; compile a stock us
keymap via xkbcommon and hand it over (format 1 = XKB_V1). */
struct xkb_context *xkb_ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
struct xkb_rule_names names = {
.rules = "evdev", .model = "pc105", .layout = "us",
.variant = "", .options = "",
};
struct xkb_keymap *xkb_map =
xkb_keymap_new_from_names(xkb_ctx, &names,
XKB_KEYMAP_COMPILE_NO_FLAGS);
if (xkb_map) {
char *str = xkb_keymap_get_as_string(xkb_map,
XKB_KEYMAP_USE_ORIGINAL_FORMAT);
if (str) {
size_t len = strlen(str) + 1;
int kfd = memfd_create("wltest-keymap", MFD_CLOEXEC);
if (kfd >= 0) {
if (ftruncate(kfd, (off_t)len) == 0) {
if (write(kfd, str, len) == (ssize_t)len)
zwp_virtual_keyboard_v1_keymap(vkb, 1, kfd,
(uint32_t)len);
}
close(kfd);
}
free(str);
}
xkb_keymap_unref(xkb_map);
}
xkb_context_unref(xkb_ctx);
char *spec = strdup(virtual_key_spec ? virtual_key_spec : "");
uint32_t time = 1000;
if (virtual_mods_set)
zwp_virtual_keyboard_v1_modifiers(vkb, virtual_mods, 0, 0, 0);
for (char *tok = strtok(spec, ","); tok; tok = strtok(NULL, ",")) {
unsigned code = 0, state = 0;
if (sscanf(tok, "%u:%u", &code, &state) == 2)
zwp_virtual_keyboard_v1_key(vkb, time, code, state);
time += 10;
}
free(spec);
wl_display_roundtrip(display);
}
/* --- Virtual pointer (zwlr_virtual_pointer_v1) ---
--virtual-motion <dx> <dy> [--virtual-click]: move the pointer relatively
by (dx,dy); --virtual-abs <x> <y> moves absolutely to the (x,y) fraction
of the output instead. With --virtual-click also press+release left
button there. Exits after flushing. */
static double virtual_dx, virtual_dy;
static double virtual_abs_x = -1.0, virtual_abs_y = -1.0;
static bool virtual_click;
static void virtual_motion_run(void) {
display = wl_display_connect(NULL);
if (!display) {
fprintf(stderr, "wltest: virtual-motion: 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 (!virtual_ptr_mgr || !seat) {
fprintf(stderr, "wltest: virtual-motion: manager or seat missing\n");
exit(1);
}
struct zwlr_virtual_pointer_v1 *vptr =
zwlr_virtual_pointer_manager_v1_create_virtual_pointer(
virtual_ptr_mgr, seat);
if (virtual_abs_x >= 0.0 && virtual_abs_y >= 0.0)
zwlr_virtual_pointer_v1_motion_absolute(vptr, 1000,
wl_fixed_from_double(virtual_abs_x * 1000.0),
wl_fixed_from_double(virtual_abs_y * 1000.0), 1000, 1000);
else
zwlr_virtual_pointer_v1_motion(vptr, 1000,
wl_fixed_from_double(virtual_dx),
wl_fixed_from_double(virtual_dy));
if (virtual_click) {
/* BTN_LEFT down/up at the new position. */
zwlr_virtual_pointer_v1_button(vptr, 1010, 272, 1);
zwlr_virtual_pointer_v1_button(vptr, 1020, 272, 0);
}
zwlr_virtual_pointer_v1_frame(vptr);
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;
}
}
}
/* --- Nested xdg popups ---
--popup [--popup-hold <s>]: open a toplevel with a popup P1 on it and a
nested sub-popup P2 on P1 (menu->submenu shape), each with a real buffer
so all three map. Holds <s> seconds (default: until killed), then exits.
The driver kills this client mid-chain and opens a fresh popup to prove
the compositor tore the whole popup tree down without dangling scene
references. */
static bool popup_mode;
static int popup_hold = -1;
/* --popup-out <file>: pop_popup_repositioned logs the answer here. */
static const char *popup_out;
static struct wl_surface *pop_surf[3];
static struct xdg_surface *pop_xdg[3];
static struct xdg_toplevel *pop_top;
static struct xdg_popup *pop_popup[2];
static struct wl_buffer *pop_buf[3];
/* Set by pop_popup_repositioned once the compositor answers a reposition. */
static bool pop_repositioned;
static void pop_make_buf(int i, int w, int h, uint8_t shade) {
if (pop_buf[i]) return;
int 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) {
for (int y = 0; y < h; y++)
for (int x = 0; x < w; x++) {
d[(y * stride) + x * 4 + 0] = shade;
d[(y * stride) + x * 4 + 1] = shade;
d[(y * stride) + x * 4 + 2] = shade;
d[(y * stride) + x * 4 + 3] = 0xff;
}
munmap(d, size);
}
close(fd);
pop_buf[i] = wl_shm_pool_create_buffer(pool, 0, w, h, stride,
WL_SHM_FORMAT_XRGB8888);
wl_shm_pool_destroy(pool);
}
static void pop_xdg_configure(void *data, struct xdg_surface *xs,
uint32_t serial) {
int i = (int)(intptr_t)data;
static const int ws[3] = { 64, 200, 150 };
static const int hs[3] = { 64, 100, 80 };
xdg_surface_ack_configure(xs, serial);
pop_make_buf(i, ws[i], hs[i], (uint8_t)(0x40 + i * 0x30));
wl_surface_attach(pop_surf[i], pop_buf[i], 0, 0);
wl_surface_commit(pop_surf[i]);
}
static const struct xdg_surface_listener pop_xdg_listener = {
.configure = pop_xdg_configure,
};
static void pop_top_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 pop_top_listener = {
.configure = pop_top_configure,
.close = xdg_toplevel_close,
};
static void pop_popup_configure(void *data, struct xdg_popup *p,
int32_t x, int32_t y, int32_t w, int32_t h) {
(void)data; (void)p; (void)x; (void)y; (void)w; (void)h;
}
static void pop_popup_done(void *data, struct xdg_popup *p) {
(void)data; (void)p;
}
static void pop_popup_repositioned(void *data, struct xdg_popup *p,
uint32_t token) {
(void)data; (void)p; (void)token;
pop_repositioned = true;
if (popup_out) {
FILE *f = fopen(popup_out, "a");
if (f) {
fprintf(f, "repositioned token=%u\n", token);
fclose(f);
}
}
}
static const struct xdg_popup_listener pop_popup_listener = {
.configure = pop_popup_configure,
.popup_done = pop_popup_done,
.repositioned = pop_popup_repositioned,
};
static void pop_open_popup(int i, struct xdg_surface *parent, int w, int h) {
pop_surf[i] = wl_compositor_create_surface(compositor);
pop_xdg[i] = xdg_wm_base_get_xdg_surface(wm_base, pop_surf[i]);
struct xdg_positioner *pos =
xdg_wm_base_create_positioner(wm_base);
xdg_positioner_set_size(pos, w, h);
xdg_positioner_set_anchor_rect(pos, 0, 0, 10, 10);
xdg_positioner_set_anchor(pos, XDG_POSITIONER_ANCHOR_BOTTOM_LEFT);
xdg_positioner_set_gravity(pos, XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT);
xdg_positioner_set_constraint_adjustment(
pos, XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X |
XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y);
pop_popup[i - 1] = xdg_surface_get_popup(pop_xdg[i], parent, pos);
xdg_positioner_destroy(pos);
xdg_surface_add_listener(pop_xdg[i], &pop_xdg_listener,
(void *)(intptr_t)i);
xdg_popup_add_listener(pop_popup[i - 1], &pop_popup_listener,
(void *)(intptr_t)i);
wl_surface_commit(pop_surf[i]);
}
static void popup_run(void) {
display = wl_display_connect(NULL);
if (!display) {
fprintf(stderr, "wltest: popup: 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);
xdg_wm_base_add_listener(wm_base, &wm_base_listener, NULL);
/* Toplevel first so the popups have a mapped parent chain. */
pop_surf[0] = wl_compositor_create_surface(compositor);
pop_xdg[0] = xdg_wm_base_get_xdg_surface(wm_base, pop_surf[0]);
pop_top = xdg_surface_get_toplevel(pop_xdg[0]);
xdg_toplevel_set_title(pop_top, "wltest-popup-parent");
xdg_surface_add_listener(pop_xdg[0], &pop_xdg_listener,
(void *)(intptr_t)0);
xdg_toplevel_add_listener(pop_top, &pop_top_listener, NULL);
wl_surface_commit(pop_surf[0]);
wl_display_roundtrip(display);
pop_open_popup(1, pop_xdg[0], 200, 100);
wl_display_roundtrip(display);
/* A nested popup is only configured once its parent is mapped: let P1
settle server-side before creating P2 on it. */
sleep(1);
wl_display_roundtrip(display);
pop_open_popup(2, pop_xdg[1], 150, 80);
/* The final popup batch has no roundtrip after it; flush so the server
actually receives get_popup before the hold loop (which only polls). */
wl_display_flush(display);
if (popup_reposition) {
/* Ask the compositor to reposition P1, then wait for its
repositioned answer: exit 0 proves the server ran the
reposition path, exit 1 means it never answered. */
struct xdg_positioner *rpos =
xdg_wm_base_create_positioner(wm_base);
xdg_positioner_set_size(rpos, 200, 100);
xdg_positioner_set_anchor_rect(rpos, 0, 0, 10, 10);
xdg_positioner_set_anchor(rpos, XDG_POSITIONER_ANCHOR_TOP_LEFT);
xdg_positioner_set_gravity(rpos, XDG_POSITIONER_GRAVITY_TOP_RIGHT);
xdg_positioner_set_constraint_adjustment(
rpos, XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X |
XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y);
xdg_popup_reposition(pop_popup[0], rpos, 7);
xdg_positioner_destroy(rpos);
wl_surface_commit(pop_surf[1]);
time_t t0 = time(NULL);
while (!pop_repositioned && time(NULL) - t0 < 5) {
if (wl_display_dispatch_pending(display) < 0)
break;
if (wl_display_roundtrip(display) < 0)
break;
}
exit(pop_repositioned ? 0 : 1);
}
if (popup_hold < 0) {
for (;;) {
if (wl_display_dispatch(display) < 0)
break;
}
} else {
/* Hold the chain open, servicing the connection: a plain
dispatch_pending+poll never READS new events (pending only drains
the already-read queue), so late configures would stall forever.
Periodic roundtrips read, dispatch (buffer attach on configure)
and flush. */
time_t t0 = time(NULL);
while (time(NULL) - t0 < popup_hold) {
if (wl_display_roundtrip(display) < 0)
break;
struct pollfd pfd = { .fd = -1 };
poll(&pfd, 0, 200);
}
}
}
/* 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);
if (len == 0) {
/* Empty pull against a fresh mirror offer: the compositor is
still prefetching the payload, and no further offer event
will come to retrigger us. Poll briefly for the racing
fetch before going back to event-driven waits. */
for (int i = 0; i < 50; i++) {
struct timespec ts = { .tv_sec = 0, .tv_nsec = 100000000 };
nanosleep(&ts, NULL);
payload = pull_primary(&len);
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);
}
free(payload);
if (len > 0)
break;
}
}
return; /* keep waiting for the setter's offer */
}
write_outfile(primary_out, payload, len);
free(payload);
exit(0);
}
/* --- Text input + input method (OSK/IME) ---
--text-input --text-input-out <file>: open a normal toplevel that also
owns a zwp_text_input_v3 field. The field is enabled (surrounding text
"hello", terminal content-type, cursor rectangle) as soon as the client
runs; every commit_string/preedit_string from the compositor is appended
to <file> ("commit:<text>") and the client exits once one arrives.
--input-method --im-commit <text> --im-out <file>: act as the IME side.
On activate it sends commit_string(<text>) + commit; "active" is written
on activate and "done" on done. Together the two modes prove the
compositor relays field -> IME -> field end to end. */
static bool text_input_mode;
static const char *text_input_out;
static bool ti_enabled;
static void ti_note_commit(const char *kind, const char *text) {
if (!text_input_out)
return;
FILE *f = fopen(text_input_out, "a");
if (f) {
fprintf(f, "%s:%s\n", kind, text ? text : "");
fclose(f);
}
}
static void ti_enter(void *data, struct zwp_text_input_v3 *ti,
struct wl_surface *srf) {
(void)data; (void)ti; (void)srf;
}
static void ti_leave(void *data, struct zwp_text_input_v3 *ti,
struct wl_surface *srf) {
(void)data; (void)ti; (void)srf;
}
static void ti_preedit(void *data, struct zwp_text_input_v3 *ti,
const char *text, int32_t begin, int32_t end) {
(void)data; (void)ti; (void)begin; (void)end;
ti_note_commit("preedit", text);
}
static void ti_commit_string(void *data, struct zwp_text_input_v3 *ti,
const char *text) {
(void)data; (void)ti;
ti_note_commit("commit", text);
/* Proved end-to-end delivery; the driver reads the file. */
exit(0);
}
static void ti_delete(void *data, struct zwp_text_input_v3 *ti,
uint32_t before, uint32_t after) {
(void)data; (void)ti; (void)before; (void)after;
}
static void ti_done(void *data, struct zwp_text_input_v3 *ti,
uint32_t serial) {
(void)data; (void)ti; (void)serial;
}
static const struct zwp_text_input_v3_listener ti_listener = {
.enter = ti_enter,
.leave = ti_leave,
.preedit_string = ti_preedit,
.commit_string = ti_commit_string,
.delete_surrounding_text = ti_delete,
.done = ti_done,
};
/* Enable the field: may run before the compositor entered us (focus lands
later); the compositor relays on focus change regardless. */
static void ti_enable_field(void) {
if (!ti_obj || ti_enabled)
return;
ti_enabled = true;
zwp_text_input_v3_enable(ti_obj);
zwp_text_input_v3_set_surrounding_text(ti_obj, "hello", 5, 5);
zwp_text_input_v3_set_content_type(ti_obj,
ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE,
ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TERMINAL);
zwp_text_input_v3_set_cursor_rectangle(ti_obj, 0, 0, 8, 16);
zwp_text_input_v3_commit(ti_obj);
wl_display_flush(display);
}
static bool input_method_mode;
static const char *im_commit_text;
static const char *im_out;
static bool im_grab;
static bool im_popup;
static bool im_exit_on_deactivate;
static bool im_got_activate;
static struct zwp_input_method_v2 *im_obj;
static bool im_done;
static uint32_t im_serial;
static void im_note(const char *s) {
if (!im_out)
return;
FILE *f = fopen(im_out, "a");
if (f) {
fprintf(f, "%s\n", s);
fclose(f);
}
}
static void im_activate(void *data, struct zwp_input_method_v2 *im) {
(void)data; (void)im;
im_note("active");
im_got_activate = true;
/* The commit must carry the latest done serial (which counts past done
events), so the actual commit_string + commit goes out from the done
handler below -- on every done, so each newly focused field gets its
string. */
}
static void im_deactivate(void *data, struct zwp_input_method_v2 *im) {
(void)data; (void)im;
im_note("inactive");
/* Event-driven exit for the popup/grab teardown test: the focused
field is gone (it exits after logging its commit), so leaving now
drops the popup and the method mid-session. Gated on having been
active so a pre-focus deactivate never exits early. */
if (im_exit_on_deactivate && im_got_activate)
exit(0);
}
static void im_surrounding(void *data, struct zwp_input_method_v2 *im,
const char *text, uint32_t cursor,
uint32_t anchor) {
(void)data; (void)im; (void)text; (void)cursor; (void)anchor;
}
static void im_cause(void *data, struct zwp_input_method_v2 *im,
uint32_t cause) {
(void)data; (void)im; (void)cause;
}
static void im_content(void *data, struct zwp_input_method_v2 *im,
uint32_t hint, uint32_t purpose) {
(void)data; (void)im; (void)hint; (void)purpose;
}
static void im_done_ev(void *data, struct zwp_input_method_v2 *im) {
(void)data; (void)im;
/* The commit serial counts past done events (see input-method XML). */
im_serial++;
if (!im_done) {
im_done = true;
im_note("done");
}
if (im_commit_text) {
zwp_input_method_v2_commit_string(im_obj, im_commit_text);
zwp_input_method_v2_commit(im_obj, im_serial);
wl_display_flush(display);
}
}
static void im_unavailable(void *data, struct zwp_input_method_v2 *im) {
(void)data; (void)im;
im_note("unavailable");
}
static const struct zwp_input_method_v2_listener im_listener = {
.activate = im_activate,
.deactivate = im_deactivate,
.surrounding_text = im_surrounding,
.text_change_cause = im_cause,
.content_type = im_content,
.done = im_done_ev,
.unavailable = im_unavailable,
};
static void input_method_run(void) {
display = wl_display_connect(NULL);
if (!display) {
fprintf(stderr, "wltest: input-method: 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 (!input_method_mgr || !seat) {
fprintf(stderr, "wltest: input-method: manager or seat missing\n");
exit(1);
}
im_obj = zwp_input_method_manager_v2_get_input_method(input_method_mgr,
seat);
zwp_input_method_v2_add_listener(im_obj, &im_listener, NULL);
if (im_grab) {
/* Take the keyboard grab: the compositor routes grabbed keys to
the IME (and logs it). Grab events need no listener here. */
zwp_input_method_v2_grab_keyboard(im_obj);
}
if (im_popup) {
/* Candidate popup: a bare surface with a committed shm buffer
taking the popup role. */
if (!compositor || !shm) {
fprintf(stderr, "wltest: input-method: compositor/shm missing\n");
exit(1);
}
struct wl_surface *psurf = wl_compositor_create_surface(compositor);
int fd;
struct wl_shm_pool *pool = make_pool(64 * 64 * 4, &fd);
if (!pool) {
fprintf(stderr, "wltest: input-method: shm pool failed\n");
exit(1);
}
uint8_t *px = mmap(NULL, 64 * 64 * 4, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
if (px == MAP_FAILED) {
fprintf(stderr, "wltest: input-method: mmap failed\n");
exit(1);
}
memset(px, 0xff, 64 * 64 * 4);
munmap(px, 64 * 64 * 4);
close(fd);
struct wl_buffer *pbuf = wl_shm_pool_create_buffer(pool, 0, 64, 64,
64 * 4, WL_SHM_FORMAT_XRGB8888);
wl_shm_pool_destroy(pool);
wl_surface_attach(psurf, pbuf, 0, 0);
wl_surface_damage(psurf, 0, 0, 64, 64);
zwp_input_method_v2_get_input_popup_surface(im_obj, psurf);
wl_surface_commit(psurf);
}
wl_display_roundtrip(display);
for (;;) {
if (wl_display_dispatch(display) < 0)
break;
}
}
static void print_usage(const char *argv0) {
fprintf(stderr, "usage: %s [--app-id <id>] [--title <text>] [--extra #RRGGBB] "
"[--cursor-shape name] [--globals-out <file>] "
"[--constrain locked|confined] [--confine-rect X Y W H] "
"[--primary-set <text>] [--primary-demand <file>] "
"[--primary-demand-expect <file> <text>] "
"[--clipboard-set <text>] "
"[--clipboard-demand-expect <file> <text>] "
"[--dc-primary-set <text>] "
"[--dc-primary-demand-expect <file> <text>] [--dc-clear] "
"[--touch-log <file>] [--gesture-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]] "
"[--text-input [--text-input-out <file>]] "
"[--input-method [--im-commit <text>] [--im-out <file>] "
"[--im-grab] [--im-popup] [--im-exit-on-deactivate]] "
"[--content-type <none|photo|video|game>] [--single-pixel] "
"[--tag-set <tag> [--tag-desc <desc>]] "
"[--color-probe [--color-out <file>]] "
"[--power <on|off>] [--gamma [--gamma-out <file>]] [--bell] "
"[--dialog-modal] [--extra-modal] [--extra-delay <ms>] "
"[--tearing <vsync|async>] [--fractional-out <file>] "
"[--icon <#RRGGBB> [SIZE]] "
"[--output-mgmt [--output-mgmt-out <file>] "
"[--output-mgmt-size <W>x<H>]] "
"#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"
" --clipboard-set publishes <text> as the clipboard over\n"
" ext-data-control-v1 (no input serial needed, so no\n"
" focus is required); exits when the source is cancelled\n"
" --clipboard-demand-expect pulls clipboard offers over\n"
" ext-data-control-v1 until the payload equals <text>,\n"
" then writes it to <file> and exits\n"
" --dc-primary-set / --dc-primary-demand-expect do the same\n"
" for the primary selection over ext-data-control-v1;\n"
" --dc-clear unsets both selections 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"
" a zero width/height needs the opposite edge anchors\n"
" (left+right / top+bottom) or the compositor kills\n"
" the client per protocol",
" --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"
" --foreign-activate/--foreign-maximize/\n"
" --foreign-fullscreen <app_id> send that taskbar\n"
" request for the matching 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"
" --idle-inhibit-release <ms> drops it after <ms>\n"
" --text-input owns a text field on the toplevel and logs\n"
" commit/preedit strings to --text-input-out, exiting on\n"
" the first commit (IME -> field proof)\n"
" --input-method acts as the IME: on activate it commits\n"
" --im-commit, logging activate/done to --im-out\n"
" --content-type tags the toplevel surface (photo/video/\n"
" game) via wp_content_type_v1\n"
" --single-pixel backs the toplevel with a 1x1\n"
" single-pixel buffer instead of shm\n"
" --tag-set labels the toplevel via xdg-toplevel-tag-v1\n"
" (with --tag-desc for the translated description)\n"
" --color-probe declares the surface alpha mode via\n"
" wp_color_representation_v1 and writes the\n"
" advertised features plus done to --color-out\n"
" --gamma asks for gamma control, writing gamma_size or\n"
" failed to --gamma-out and exiting\n"
" --dialog-modal marks the toplevel modal via xdg-dialog;\n"
" --extra-modal marks the --extra window modal instead;\n"
" --extra-delay sleeps <ms> before opening it\n"
" --tearing requests that presentation hint (vsync|async)\n"
" on the toplevel; --fractional-out writes the\n"
" compositor's preferred_scale and exits\n"
" --icon uploads a SIZE×SIZE (default 8) solid-color\n"
" icon and sets it via xdg-toplevel-icon-v1\n"
" --req-out logs maximized=0/1 on every toplevel\n"
" configure; keys M/U/N/V/R send maximize/unmaximize\n"
" /minimize/move/resize requests to the compositor;\n"
" --ignore-close swallows xdg_toplevel.close (for the\n"
" SIGKILL-escalation test)\n"
" --popup-reposition repositions P1 after the chain maps\n"
" and exits 0 on the repositioned event (1 on timeout),\n"
" logging it to --popup-out when given\n"
" --output-mgmt lists the output head (name + size)\n"
" to --output-mgmt-out, applying --output-mgmt-size\n"
" first when given (--output-mgmt-test probes it\n"
" instead, recording \"tested\")",
argv0, argv0);
}
int main(int argc, char **argv) {
/* A yanked pull (demander re-pulling mid-read while sources swap)
must fail the write, not kill the serving tab with SIGPIPE. */
signal(SIGPIPE, SIG_IGN);
const char *app_id = NULL;
const char *title_override = NULL;
const char *touch_log_path = NULL;
const char *pointer_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], "--title") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --title needs a value\n");
return 1;
}
title_override = 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], "--dialog-modal") == 0) {
dialog_modal = true;
i += 1;
} else if (strcmp(argv[i], "--extra-modal") == 0) {
extra_modal = true;
want_extra = true;
i += 1;
} else if (strcmp(argv[i], "--extra-delay") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --extra-delay needs ms\n");
return 1;
}
extra_delay_ms = atoi(argv[i + 1]);
if (extra_delay_ms < 0) {
fprintf(stderr, "wltest: bad --extra-delay\n");
return 1;
}
i += 2;
} else if (strcmp(argv[i], "--tearing") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --tearing needs vsync|async\n");
return 1;
}
const char *t = argv[i + 1];
if (strcmp(t, "vsync") == 0)
tearing_arg = WP_TEARING_CONTROL_V1_PRESENTATION_HINT_VSYNC;
else if (strcmp(t, "async") == 0)
tearing_arg = WP_TEARING_CONTROL_V1_PRESENTATION_HINT_ASYNC;
else {
fprintf(stderr, "wltest: bad --tearing '%s'\n", t);
return 1;
}
i += 2;
} else if (strcmp(argv[i], "--fractional-out") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --fractional-out needs a path\n");
return 1;
}
fractional_out = argv[i + 1];
i += 2;
} else if (strcmp(argv[i], "--output-mgmt") == 0) {
output_mgmt_mode = true;
i += 1;
} else if (strcmp(argv[i], "--output-mgmt-out") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --output-mgmt-out needs a path\n");
return 1;
}
output_mgmt_out = argv[i + 1];
i += 2;
} else if (strcmp(argv[i], "--output-mgmt-size") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --output-mgmt-size needs WxH\n");
return 1;
}
if (sscanf(argv[i + 1], "%dx%d", &output_mgmt_w,
&output_mgmt_h) != 2 ||
output_mgmt_w <= 0 || output_mgmt_h <= 0) {
fprintf(stderr, "wltest: bad --output-mgmt-size '%s'\n",
argv[i + 1]);
return 1;
}
output_mgmt_want_size = true;
i += 2;
} else if (strcmp(argv[i], "--output-mgmt-test") == 0) {
output_mgmt_test_only = true;
i += 1;
} else if (strcmp(argv[i], "--icon") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --icon needs a color\n");
return 1;
}
const char *x = argv[i + 1];
if (*x == '#') x++;
if (strlen(x) != 6 || sscanf(x, "%2hhx%2hhx%2hhx",
&ICON_PIXEL[0], &ICON_PIXEL[1], &ICON_PIXEL[2]) != 3) {
fprintf(stderr, "wltest: bad icon color '%s'\n", argv[i + 1]);
return 1;
}
i += 2;
/* Optional SIZE: numeric and neither a flag nor the main
window's #color (a trailing WxH would misparse, so --icon
is meant for plain color windows). */
if (i < argc && argv[i][0] != '-' && argv[i][0] != '#') {
icon_size = atoi(argv[i]);
if (icon_size <= 0 || icon_size > 256) {
fprintf(stderr, "wltest: bad icon size '%s'\n", argv[i]);
return 1;
}
i += 1;
}
want_icon = true;
} 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], "--pointer-out") == 0) {
if (i + 1 >= argc) {
fprintf(stderr,
"wltest: --pointer-out needs a file argument\n");
return 1;
}
pointer_log_path = argv[i + 1];
i += 2;
} else if (strcmp(argv[i], "--gesture-log") == 0) {
if (i + 1 >= argc) {
fprintf(stderr,
"wltest: --gesture-log needs a file argument\n");
return 1;
}
gesture_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], "--clipboard-set") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --clipboard-set needs a value\n");
return 1;
}
clip_text = argv[i + 1];
i += 2;
} else if (strcmp(argv[i], "--clipboard-demand-expect") == 0) {
if (i + 2 >= argc) {
fprintf(stderr, "wltest: --clipboard-demand-expect needs a "
"path and expected text\n");
return 1;
}
clip_out = argv[i + 1];
clip_expect = argv[i + 2];
i += 3;
} else if (strcmp(argv[i], "--dc-primary-set") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --dc-primary-set needs a value\n");
return 1;
}
dc_prim_text = argv[i + 1];
i += 2;
} else if (strcmp(argv[i], "--dc-primary-demand-expect") == 0) {
if (i + 2 >= argc) {
fprintf(stderr, "wltest: --dc-primary-demand-expect needs a "
"path and expected text\n");
return 1;
}
dc_prim_out = argv[i + 1];
dc_prim_expect = argv[i + 2];
i += 3;
} else if (strcmp(argv[i], "--dc-clear") == 0) {
dc_clear = true;
i += 1;
} 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], "--foreign-activate") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --foreign-activate needs an app_id\n");
return 1;
}
foreign_activate_app = argv[i + 1];
i += 2;
} else if (strcmp(argv[i], "--foreign-maximize") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --foreign-maximize needs an app_id\n");
return 1;
}
foreign_maximize_app = argv[i + 1];
i += 2;
} else if (strcmp(argv[i], "--foreign-fullscreen") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --foreign-fullscreen needs an app_id\n");
return 1;
}
foreign_fullscreen_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 if (strcmp(argv[i], "--idle-inhibit-release") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --idle-inhibit-release needs ms\n");
return 1;
}
idle_inhibit_release_ms = atoi(argv[i + 1]);
idle_inhibit = true;
i += 2;
} else if (strcmp(argv[i], "--lock") == 0) {
lock_mode = true;
i += 1;
} else if (strcmp(argv[i], "--lock-out") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --lock-out needs a path\n");
return 1;
}
lock_out = argv[i + 1];
i += 2;
} else if (strcmp(argv[i], "--lock-unlock") == 0) {
lock_unlock_after = true;
i += 1;
} else if (strcmp(argv[i], "--lock-second-surface") == 0) {
lock_second_surface = true;
i += 1;
} else if (strcmp(argv[i], "--screencopy") == 0) {
screencopy_mode = true;
i += 1;
} else if (strcmp(argv[i], "--screencopy-out") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --screencopy-out needs a path\n");
return 1;
}
screencopy_out = argv[i + 1];
i += 2;
} else if (strcmp(argv[i], "--inhibit") == 0) {
inhibit_mode = true;
i += 1;
} else if (strcmp(argv[i], "--inhibit-out") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --inhibit-out needs a path\n");
return 1;
}
inhibit_out = argv[i + 1];
i += 2;
} else if (strcmp(argv[i], "--power") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --power needs on|off\n");
return 1;
}
power_arg = argv[i + 1];
i += 2;
} else if (strcmp(argv[i], "--gamma") == 0) {
gamma_mode = true;
i += 1;
} else if (strcmp(argv[i], "--gamma-out") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --gamma-out needs a path\n");
return 1;
}
gamma_out = argv[i + 1];
i += 2;
} else if (strcmp(argv[i], "--bell") == 0) {
bell_mode = true;
i += 1;
} else if (strcmp(argv[i], "--virtual-key") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --virtual-key needs code:state,...\n");
return 1;
}
virtual_key_spec = argv[i + 1];
i += 2;
} else if (strcmp(argv[i], "--virtual-motion") == 0) {
if (i + 2 >= argc) {
fprintf(stderr, "wltest: --virtual-motion needs dx dy\n");
return 1;
}
virtual_dx = atof(argv[i + 1]);
virtual_dy = atof(argv[i + 2]);
i += 3;
} else if (strcmp(argv[i], "--virtual-click") == 0) {
virtual_click = true;
i += 1;
} else if (strcmp(argv[i], "--virtual-abs") == 0) {
if (i + 2 >= argc) {
fprintf(stderr, "wltest: --virtual-abs needs x y fractions\n");
return 1;
}
virtual_abs_x = atof(argv[i + 1]);
virtual_abs_y = atof(argv[i + 2]);
i += 3;
} else if (strcmp(argv[i], "--virtual-modifiers") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --virtual-modifiers needs a mask\n");
return 1;
}
virtual_mods = (uint32_t)strtoul(argv[i + 1], NULL, 0);
virtual_mods_set = true;
i += 2;
} 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 if (strcmp(argv[i], "--popup") == 0) {
popup_mode = true;
i += 1;
} else if (strcmp(argv[i], "--popup-hold") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --popup-hold needs seconds\n");
return 1;
}
popup_hold = atoi(argv[i + 1]);
i += 2;
} else if (strcmp(argv[i], "--popup-reposition") == 0) {
popup_reposition = true;
popup_mode = true;
i += 1;
} else if (strcmp(argv[i], "--popup-out") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --popup-out needs a path\n");
return 1;
}
popup_out = argv[i + 1];
i += 2;
} else if (strcmp(argv[i], "--req-out") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --req-out needs a path\n");
return 1;
}
req_out = argv[i + 1];
i += 2;
} else if (strcmp(argv[i], "--ignore-close") == 0) {
ignore_close = true;
i += 1;
} else if (strcmp(argv[i], "--text-input") == 0) {
text_input_mode = true;
i += 1;
} else if (strcmp(argv[i], "--text-input-out") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --text-input-out needs a path\n");
return 1;
}
text_input_out = argv[i + 1];
i += 2;
} else if (strcmp(argv[i], "--input-method") == 0) {
input_method_mode = true;
i += 1;
} else if (strcmp(argv[i], "--im-commit") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --im-commit needs text\n");
return 1;
}
im_commit_text = argv[i + 1];
i += 2;
} else if (strcmp(argv[i], "--im-out") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --im-out needs a path\n");
return 1;
}
im_out = argv[i + 1];
i += 2;
} else if (strcmp(argv[i], "--im-grab") == 0) {
im_grab = true;
i += 1;
} else if (strcmp(argv[i], "--im-popup") == 0) {
im_popup = true;
i += 1;
} else if (strcmp(argv[i], "--im-exit-on-deactivate") == 0) {
im_exit_on_deactivate = true;
i += 1;
} else if (strcmp(argv[i], "--content-type") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --content-type needs a value "
"(none|photo|video|game)\n");
return 1;
}
const char *ct = argv[i + 1];
if (strcmp(ct, "none") == 0)
content_type_arg = WP_CONTENT_TYPE_V1_TYPE_NONE;
else if (strcmp(ct, "photo") == 0)
content_type_arg = WP_CONTENT_TYPE_V1_TYPE_PHOTO;
else if (strcmp(ct, "video") == 0)
content_type_arg = WP_CONTENT_TYPE_V1_TYPE_VIDEO;
else if (strcmp(ct, "game") == 0)
content_type_arg = WP_CONTENT_TYPE_V1_TYPE_GAME;
else {
fprintf(stderr, "wltest: bad --content-type '%s'\n", ct);
return 1;
}
i += 2;
} else if (strcmp(argv[i], "--single-pixel") == 0) {
single_pixel_mode = true;
i += 1;
} else if (strcmp(argv[i], "--tag-set") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --tag-set needs a tag\n");
return 1;
}
tag_arg = argv[i + 1];
i += 2;
} else if (strcmp(argv[i], "--tag-desc") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --tag-desc needs a description\n");
return 1;
}
tag_desc_arg = argv[i + 1];
i += 2;
} else if (strcmp(argv[i], "--color-probe") == 0) {
color_probe = true;
i += 1;
} else if (strcmp(argv[i], "--color-out") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --color-out needs a path\n");
return 1;
}
color_out = argv[i + 1];
i += 2;
} else {
break;
}
}
/* Input-method mode: no color/size argument. Act as the IME side of
zwp_input_method_v2 and never create a window. */
if (input_method_mode) {
input_method_run();
return 0;
}
/* Nested popups: toplevel + popup + sub-popup chain, then exit/hold. */
if (popup_mode) {
popup_run();
return 0;
}
/* 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_mods_set) {
virtual_key_run();
return 0;
}
/* Virtual pointer: motion (+optional click) is requested via
--virtual-motion, absolute warp via --virtual-abs (either alone
suffices); --virtual-click alone is meaningless. */
if (virtual_dx != 0 || virtual_dy != 0 || virtual_click ||
(virtual_abs_x >= 0.0 && virtual_abs_y >= 0.0)) {
virtual_motion_run();
return 0;
}
/* Output-power mode: blank/unblank the output, then exit. */
if (power_arg) {
power_run();
return 0;
}
/* Gamma mode: no color/size argument. Ask for gamma control and
report gamma_size/failed, then exit. */
if (gamma_mode) {
gamma_run();
return 0;
}
/* Bell mode: no color/size argument. Ring the compositor's bell,
then exit. */
if (bell_mode) {
bell_run();
return 0;
}
/* Output-management mode: no color/size argument. List the head
and optionally apply a custom size, then exit. */
if (output_mgmt_mode) {
output_mgmt_run();
return 0;
}
/* Shortcuts-inhibit mode: act as a game window that grabs the
compositor's shortcuts. Creates its own focused window. */
if (inhibit_mode) {
inhibit_run();
return 0;
}
/* Screen-capture mode: no color/size argument. Capture the compositor's
rendered output into a shm buffer and report the frame geometry. */
if (screencopy_mode) {
screencopy_run();
return 0;
}
/* Session-lock mode: no color/size argument. Act as the swaylock side
of ext-session-lock-v1 and never open a normal toplevel. */
if (lock_mode) {
lock_run();
return 0;
}
/* 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, &registry_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, &registry_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, title_override ? title_override : 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);
/* --gesture-log: bind swipe/pinch/hold for this pointer so tests
can prove gesture delivery end-to-end. */
gesture_bind_all();
/* --pointer-out: record every pointer enter/motion/button so tests
can prove virtual-pointer delivery end-to-end. */
if (pointer_log_path && !pointer_log) {
pointer_log = fopen(pointer_log_path, "w");
if (!pointer_log) {
fprintf(stderr, "wltest: cannot write pointer log %s\n",
pointer_log_path);
return 1;
}
}
/* --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);
}
/* Data-control clipboard/primary: one device covers both selections;
publishing needs no input serial, so it runs as soon as the device
exists. */
if (dc_mgr && seat) {
dc_dev = ext_data_control_manager_v1_get_data_device(dc_mgr, seat);
ext_data_control_device_v1_add_listener(dc_dev, &dc_device_listener,
NULL);
dc_set_selections();
} else if (clip_text || clip_out || dc_prim_text || dc_prim_out ||
dc_clear) {
fprintf(stderr, "wltest: data-control: manager or seat missing\n");
return 1;
}
/* Text input: own a field on this window; it is enabled once the
window is up (see below). The compositor enters it on keyboard
focus and relays it to the IME. */
if (text_input_mode) {
if (!text_input_mgr || !seat) {
fprintf(stderr, "wltest: text-input: manager or seat missing\n");
return 1;
}
ti_obj = zwp_text_input_manager_v3_get_text_input(text_input_mgr,
seat);
zwp_text_input_v3_add_listener(ti_obj, &ti_listener, NULL);
}
/* Content type: tag the window surface once it exists; the compositor
logs the type when the window maps. */
if (content_type_arg >= 0) { if (!content_type_mgr) {
fprintf(stderr, "wltest: content-type: manager missing\n");
return 1;
}
struct wp_content_type_v1 *ct =
wp_content_type_manager_v1_get_surface_content_type(
content_type_mgr, surface);
wp_content_type_v1_set_content_type(ct, (uint32_t)content_type_arg);
wl_display_flush(display);
}
/* Toplevel tag: label the window before the first commit, like a real
client tagging in its initial commit; the compositor logs [tag] on
claim and shows "title [tag]" in the overlay. */
if (tag_arg) {
if (!tag_mgr) {
fprintf(stderr, "wltest: tag-set: manager missing\n");
return 1;
}
xdg_toplevel_tag_manager_v1_set_toplevel_tag(tag_mgr, toplevel,
tag_arg);
if (tag_desc_arg)
xdg_toplevel_tag_manager_v1_set_toplevel_description(tag_mgr,
toplevel, tag_desc_arg);
wl_display_flush(display);
}
/* Color representation: declare the surface's alpha mode once the
window surface exists. The probe listener is attached at bind time
(see color_repr_bind_listener): the manager posts its
supported-features burst on bind, and done exits the client. */
if (color_probe || color_out) {
if (!color_repr_mgr) {
fprintf(stderr, "wltest: color-probe: manager missing\n");
return 1;
}
struct wp_color_representation_surface_v1 *cs =
wp_color_representation_manager_v1_get_surface(color_repr_mgr,
surface);
wp_color_representation_surface_v1_set_alpha_mode(cs,
WP_COLOR_REPRESENTATION_SURFACE_V1_ALPHA_MODE_PREMULTIPLIED_ELECTRICAL);
wl_display_flush(display);
}
/* Single-pixel mode scales the 1x1 buffer via a viewport destination;
without it the window would be a single pixel. */
if (single_pixel_mode) {
if (!viewporter) {
fprintf(stderr, "wltest: single-pixel: viewporter missing\n");
return 1;
}
viewport = wp_viewporter_get_viewport(viewporter, surface);
}
/* Modal main window: mark the toplevel a dialog before it maps. */
if (dialog_modal) {
if (!dialog_mgr) {
fprintf(stderr, "wltest: dialog-modal: xdg_wm_dialog_v1 missing\n");
return 1;
}
dialog_obj =
xdg_wm_dialog_v1_get_xdg_dialog(dialog_mgr, toplevel);
xdg_dialog_v1_set_modal(dialog_obj);
}
/* Tearing hint for the window; fractional scale is reported back
through the listener above (which exits on first receipt). */
if (tearing_arg >= 0) {
if (!tearing_mgr) {
fprintf(stderr, "wltest: tearing: manager missing\n");
return 1;
}
struct wp_tearing_control_v1 *tc =
wp_tearing_control_manager_v1_get_tearing_control(tearing_mgr,
surface);
wp_tearing_control_v1_set_presentation_hint(tc,
(uint32_t)tearing_arg);
wl_display_flush(display);
}
if (fractional_out) {
if (!fractional_mgr) {
fprintf(stderr, "wltest: fractional: manager missing\n");
return 1;
}
struct wp_fractional_scale_v1 *fs =
wp_fractional_scale_manager_v1_get_fractional_scale(
fractional_mgr, surface);
wp_fractional_scale_v1_add_listener(fs, &fractional_listener, NULL);
}
/* Client icon for the overlay: uploaded once the window exists. */
if (want_icon)
icon_upload();
if (constrain_mode != NO_CONSTRAINT)
request_constraint();
wl_surface_commit(surface);
wl_display_roundtrip(display);
if (text_input_mode)
ti_enable_field();
for (;;) {
if (ps_need_pull && primary_out)
demand_primary_once(); /* returns unless satisfied */
if (dc_need_pull && clip_out)
demand_dc_once(false); /* returns unless satisfied */
if (dc_prim_need_pull && dc_prim_out)
demand_dc_once(true); /* returns unless satisfied */
if (wl_display_dispatch(display) < 0)
break;
}
return 0;
}