kick/tests/wltest.c
Andrew Hamilton 38a17401d1 fix: composite overlay thumbnails from live textures for subsurface windows
Firefox paints into subsurfaces while its main surface stays transparent, so the single-buffer snapshot saw only black. Windows with subsurface content now composite at overlay-draw time from each live surface texture (which outlives current.buffer), while plain windows keep the legacy snapshot path. Adds wltest --subsurface repro mode and a regression test.
2026-09-27 18:22:07 +10:00

4834 lines
No EOL
189 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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 };
/* --pulse MS: repaint alternating between the main color and black every
MS milliseconds, driven by frame callbacks (so a hidden client only
animates while the compositor keeps sending them). Gives the overlay
preview tests a deterministic live source. */
static int pulse_ms;
static uint8_t PULSE_BASE[4];
static bool pulse_phase;
static struct wl_callback *pulse_cb;
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;
/* --subsurface #RRGGBB: paint the window through a subsurface instead of the
main surface. The main surface commits a fully transparent buffer while a
single subsurface, covering the whole window, carries the solid color --
the pattern real clients (e.g. Firefox) use when their content lives off
the main surface. The scene shows the color; a thumbnail that only reads
the main surface sees black. */
static bool subsurface_mode;
static uint8_t SUB_PIXEL[4] = { 0x00, 0xff, 0x00, 0xff }; /* RGBA */
static struct wl_subcompositor *subcomp;
static struct wl_surface *sub_surface;
static struct wl_subsurface *subsurface;
static struct wl_buffer *sub_buffer;
static void create_buffer(void);
static struct wl_shm_pool *make_pool(size_t size, int *fd_out);
static struct wl_buffer *make_solid_buffer(uint8_t r, uint8_t g, uint8_t b,
uint8_t a, int32_t format);
static void subsurface_attach(void);
static void extra_open(void);
static void request_constraint(void);
static void pulse_arm(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;
if (pointer_log) {
fprintf(pointer_log, "axis %u %d\n", axis,
(int)wl_fixed_to_double(value));
fflush(pointer_log);
}
}
/* Axis framing the compositor may send around axis events. libwayland
aborts on a NULL handler, so stub them even though tests only assert
on the axis lines themselves. */
static void pointer_axis_source(void *data, struct wl_pointer *p,
uint32_t axis_source) {
(void)data; (void)p; (void)axis_source;
}
static void pointer_axis_stop(void *data, struct wl_pointer *p,
uint32_t time, uint32_t axis) {
(void)data; (void)p; (void)time; (void)axis;
}
static void pointer_axis_discrete(void *data, struct wl_pointer *p,
uint32_t axis, int32_t discrete) {
(void)data; (void)p; (void)axis; (void)discrete;
}
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,
.axis_source = pointer_axis_source,
.axis_stop = pointer_axis_stop,
.axis_discrete = pointer_axis_discrete,
};
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,
};
/* Fill a W×H SHM buffer with one RGBA pixel (B/G/R/A byte order). */
static struct wl_buffer *make_solid_buffer(uint8_t r, uint8_t g, uint8_t b,
uint8_t a, int32_t format) {
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: subsurface: 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: subsurface: 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] = b;
row[x*4+1] = g;
row[x*4+2] = r;
row[x*4+3] = a;
}
}
munmap(data, size);
close(fd);
struct wl_buffer *buf = wl_shm_pool_create_buffer(pool, 0, W, H,
(int32_t)stride, format);
wl_shm_pool_destroy(pool);
return buf;
}
/* --subsurface attach: transparent main buffer plus the color subsurface.
The subsurface stays synchronous (the default), so the parent commit
applies both atomically and the compositor sees one coherent frame. */
static void subsurface_attach(void) {
if (!subcomp) {
fprintf(stderr, "wltest: subsurface: wl_subcompositor not advertised\n");
exit(1);
}
if (!subsurface) {
sub_surface = wl_compositor_create_surface(compositor);
subsurface = wl_subcompositor_get_subsurface(subcomp, sub_surface,
surface);
wl_subsurface_set_position(subsurface, 0, 0);
wl_subsurface_place_above(subsurface, surface);
}
if (sub_buffer)
wl_buffer_destroy(sub_buffer);
sub_buffer = make_solid_buffer(SUB_PIXEL[0], SUB_PIXEL[1], SUB_PIXEL[2],
SUB_PIXEL[3], WL_SHM_FORMAT_ARGB8888);
wl_surface_attach(sub_surface, sub_buffer, 0, 0);
wl_surface_damage_buffer(sub_surface, 0, 0, W, H);
wl_surface_commit(sub_surface);
/* Main surface: fully transparent (alpha 0 everywhere). */
if (buffer)
wl_buffer_destroy(buffer);
buffer = make_solid_buffer(0, 0, 0, 0, WL_SHM_FORMAT_ARGB8888);
wl_surface_attach(surface, buffer, 0, 0);
wl_surface_damage_buffer(surface, 0, 0, W, H);
}
static void xdg_surface_configure(void *data, struct xdg_surface *xs, uint32_t serial) {
(void)data;
xdg_surface_ack_configure(xs, serial);
if (subsurface_mode) {
subsurface_attach();
} else {
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);
}
if (pulse_ms && !pulse_cb) {
/* Start the pulse loop once the window exists; the base color is
whatever the command line set. Armed before the commit below
so the frame request takes effect immediately. */
memcpy(PULSE_BASE, PIXEL, sizeof(PULSE_BASE));
pulse_phase = false;
pulse_arm();
}
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, wl_subcompositor_interface.name) == 0) {
subcomp = wl_registry_bind(reg, name, &wl_subcompositor_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);
}
static uint64_t pulse_now_ms(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000;
}
static void pulse_frame_done(void *data, struct wl_callback *cb,
uint32_t time);
static const struct wl_callback_listener pulse_frame_listener = {
.done = pulse_frame_done,
};
static void pulse_arm(void) {
if (pulse_cb)
wl_callback_destroy(pulse_cb);
pulse_cb = wl_surface_frame(surface);
wl_callback_add_listener(pulse_cb, &pulse_frame_listener, NULL);
}
static void pulse_frame_done(void *data, struct wl_callback *cb,
uint32_t time) {
(void)data;
(void)time;
if (cb == pulse_cb)
pulse_cb = NULL;
else
wl_callback_destroy(cb);
if (!pulse_ms)
return;
bool phase = (pulse_now_ms() / (uint64_t)pulse_ms) & 1;
/* Alternate on wall clock so phases stay aligned no matter how often
(or rarely) the compositor sends frames. Always attach+commit:
the new frame request below only takes effect on a commit. */
if (phase != pulse_phase) {
pulse_phase = phase;
if (phase) {
PIXEL[0] = 0x00;
PIXEL[1] = 0x00;
PIXEL[2] = 0x00;
} else {
PIXEL[0] = PULSE_BASE[0];
PIXEL[1] = PULSE_BASE[1];
PIXEL[2] = PULSE_BASE[2];
}
create_buffer();
wl_surface_attach(surface, buffer, 0, 0);
wl_surface_damage_buffer(surface, 0, 0, W, H);
} else {
wl_surface_attach(surface, buffer, 0, 0);
}
pulse_arm();
wl_surface_commit(surface);
wl_display_flush(display);
}
/* ---- 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;
/* --virtual-axis <axis> <value> <discrete>: send one scroll event through
a virtual pointer (axis 0 = vertical). Values are client wire units:
wlroots scales discrete by WLR_POINTER_AXIS_DISCRETE_STEP itself, so
discrete=1 arrives at the compositor exactly like one libinput wheel
notch. Exits after flushing. */
static bool virtual_axis_set;
static uint32_t virtual_axis;
static double virtual_axis_value;
static int32_t virtual_axis_discrete;
static void virtual_axis_run(void) {
display = wl_display_connect(NULL);
if (!display) {
fprintf(stderr, "wltest: virtual-axis: 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-axis: 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);
zwlr_virtual_pointer_v1_axis(vptr, 1000, virtual_axis,
wl_fixed_from_double(virtual_axis_value));
zwlr_virtual_pointer_v1_axis_discrete(vptr, 1000, virtual_axis,
wl_fixed_from_double(virtual_axis_value), virtual_axis_discrete);
zwlr_virtual_pointer_v1_frame(vptr);
wl_display_roundtrip(display);
}
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"
" --subsurface paints the color through a subsurface over\n"
" a transparent main surface (subsurface-client proof)\n"
" --pulse MS repaints alternating main color/black every\n"
" MS ms on frame callbacks (a live source for previews)\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], "--subsurface") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --subsurface needs a value\n");
return 1;
}
const char *x = argv[i + 1];
if (*x == '#') x++;
if (strlen(x) != 6 || sscanf(x, "%2hhx%2hhx%2hhx",
&SUB_PIXEL[0], &SUB_PIXEL[1], &SUB_PIXEL[2]) != 3) {
fprintf(stderr, "wltest: bad subsurface color '%s'\n", argv[i + 1]);
return 1;
}
SUB_PIXEL[3] = 0xff;
subsurface_mode = 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], "--pulse") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --pulse needs ms\n");
return 1;
}
pulse_ms = atoi(argv[i + 1]);
if (pulse_ms <= 0) {
fprintf(stderr, "wltest: bad --pulse\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-axis") == 0) {
if (i + 3 >= argc) {
fprintf(stderr, "wltest: --virtual-axis needs axis value discrete\n");
return 1;
}
virtual_axis = (uint32_t)strtoul(argv[i + 1], NULL, 0);
virtual_axis_value = atof(argv[i + 2]);
virtual_axis_discrete = (int32_t)strtol(argv[i + 3], NULL, 0);
virtual_axis_set = true;
i += 4;
} 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;
}
/* Virtual pointer scroll: --virtual-axis sends one axis event. */
if (virtual_axis_set) {
virtual_axis_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/axis 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;
}