#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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 : 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-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 : 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 installs a source on the seat; --primary-demand pulls the first "text/plain" offer's content and exits; --primary-demand-expect keeps pulling until the payload matches (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 publishes a clipboard source on the seat; --clipboard-demand-expect 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 ] [+ --foreign-close ]: 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 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, ®istry_listener, NULL); wl_display_roundtrip(display); if (!ft_mgr || !ext_list) { fprintf(stderr, "wltest: foreign: manager/list globals missing\n"); exit(1); } zwlr_foreign_toplevel_manager_v1_add_listener(ft_mgr, &ftm_listener, NULL); ext_foreign_toplevel_list_v1_add_listener(ext_list, &extl_listener, NULL); if (foreign_out) { foreign_fp = fopen(foreign_out, "w"); if (!foreign_fp) { fprintf(stderr, "wltest: foreign: cannot write %s\n", foreign_out); exit(1); } } for (;;) { if (wl_display_dispatch(display) < 0) break; } if (foreign_fp) fclose(foreign_fp); } /* A primary-selection set_selection serial must be one the seat actually gave THIS client (wlr_seat_client_validate_event_serial), so the setter cannot hammer a byzantine constant. Defer the request until the first keyboard/pointer event serial arrives, then use it. */ static void maybe_set_primary(uint32_t serial) { if (!primary_text || ps_set_done || !ps_dev) return; ps_set_done = true; struct zwp_primary_selection_source_v1 *s = zwp_primary_selection_device_manager_v1_create_source(ps_mgr); zwp_primary_selection_source_v1_offer(s, "text/plain;charset=utf-8"); zwp_primary_selection_source_v1_offer(s, "text/plain"); zwp_primary_selection_source_v1_add_listener(s, &ps_source_listener, NULL); zwp_primary_selection_device_v1_set_selection(ps_dev, s, serial); wl_display_flush(display); } static int W = 1280, H = 800; static uint8_t PIXEL[4] = { 0xff, 0x00, 0x00, 0xff }; /* RGBA */ /* Optional second toplevel from the SAME client (--extra ): 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 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 [--idle-out ] [+ --idle-inhibit]: act as a plain window client that also binds ext-idle-notify, asks for an idle notification after 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 : destroy the inhibitor 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, ®istry_listener, NULL); wl_display_roundtrip(display); if (!idle_mgr) { fprintf(stderr, "wltest: idle: ext_idle_notifier_v1 not advertised\n"); exit(1); } /* Need a surface for the idle-inhibit inhibitor and a seat for the notification. Draw a small buffer so the window is present. */ surface = wl_compositor_create_surface(compositor); xdg_surface = xdg_wm_base_get_xdg_surface(wm_base, surface); toplevel = xdg_surface_get_toplevel(xdg_surface); xdg_toplevel_set_title(toplevel, "wltest-idle"); xdg_wm_base_add_listener(wm_base, &wm_base_listener, NULL); xdg_surface_add_listener(xdg_surface, &xdg_surface_listener, NULL); xdg_toplevel_add_listener(toplevel, &toplevel_listener, NULL); /* Commit an empty surface and let the configure handler attach the real buffer + commit (xdg_surface_configure does that). */ wl_surface_commit(surface); wl_display_roundtrip(display); if (seat) { idle_notif = ext_idle_notifier_v1_get_idle_notification( idle_mgr, (uint32_t)idle_timeout_ms, seat); ext_idle_notification_v1_add_listener(idle_notif, &idle_notif_listener, NULL); } if (idle_inhibit && inhib_mgr) idle_inhib = zwp_idle_inhibit_manager_v1_create_inhibitor(inhib_mgr, surface); 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 ]: 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, ®istry_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 ]: 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-x/<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 "/" 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, ®istry_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 ]: 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, ®istry_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 : 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 [--output-mgmt-size WxH]: list the head (name + current size) into ; 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, ®istry_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 : request that presentation hint on the toplevel (the compositor drives its commit's async-flip flag from the shown surface). --fractional-out : 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 ]: 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 : 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 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, ®istry_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, ®istry_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 : 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, ®istry_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 : 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 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, ®istry_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 [--virtual-click]: move the pointer relatively by (dx,dy); --virtual-abs 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 : 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, ®istry_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, ®istry_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 ]: 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, ®istry_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 ]: 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 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 : 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, ®istry_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 : 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 ("commit:") and the client exits once one arrives. --input-method --im-commit --im-out : act as the IME side. On activate it sends commit_string() + 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, ®istry_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 ] [--title ] [--extra #RRGGBB] " "[--cursor-shape name] [--globals-out ] " "[--constrain locked|confined] [--confine-rect X Y W H] " "[--primary-set ] [--primary-demand ] " "[--primary-demand-expect ] " "[--clipboard-set ] " "[--clipboard-demand-expect ] " "[--dc-primary-set ] " "[--dc-primary-demand-expect ] [--dc-clear] " "[--touch-log ] [--gesture-log ] " "[--layer ] [--layer-anchor ] " "[--exclusive-zone ] [--layer-size ] " "[--foreign [--foreign-out ] [--foreign-close ]] " "[--idle [--idle-out ] [--idle-inhibit]] " "[--text-input [--text-input-out ]] " "[--input-method [--im-commit ] [--im-out ] " "[--im-grab] [--im-popup] [--im-exit-on-deactivate]] " "[--content-type ] [--single-pixel] " "[--tag-set [--tag-desc ]] " "[--color-probe [--color-out ]] " "[--power ] [--gamma [--gamma-out ]] [--bell] " "[--dialog-modal] [--extra-modal] [--extra-delay ] " "[--tearing ] [--fractional-out ] " "[--icon <#RRGGBB> [SIZE]] " "[--output-mgmt [--output-mgmt-out ] " "[--output-mgmt-size x]] " "#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 and exits\n" " --constrain requests a pointer constraint (locked/confined)\n" " --confine-rect sets the confined pointer's region\n" " --primary-set installs as the primary selection\n" " --primary-demand pulls the selection into and exits\n" " --primary-demand-expect pulls until the payload equals " ", then writes it to and exits\n" " --clipboard-set publishes 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 ,\n" " then writes it to 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 , closes that window;\n" " --foreign-activate/--foreign-maximize/\n" " --foreign-fullscreen send that taskbar\n" " request for the matching window\n" " --idle asks ext-idle-notify for an idle event after 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 drops it after \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 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, ®istry_listener, NULL); wl_display_roundtrip(display); wl_display_roundtrip(display); wl_display_disconnect(display); return 0; } if (i >= argc || argv[i][0] != '#') { print_usage(argv[0]); return 1; } const char *hex = argv[i] + 1; if (strlen(hex) != 6 || sscanf(hex, "%2hhx%2hhx%2hhx", &PIXEL[0], &PIXEL[1], &PIXEL[2]) != 3) { fprintf(stderr, "wltest: bad color '%s'\n", argv[i]); return 1; } if (i + 2 < argc) { W = atoi(argv[i + 1]); H = atoi(argv[i + 2]); if (W <= 0 || H <= 0) { fprintf(stderr, "wltest: bad size\n"); return 1; } } display = wl_display_connect(NULL); if (!display) { fprintf(stderr, "wltest: cannot connect (WAYLAND_DISPLAY=%s)\n", getenv("WAYLAND_DISPLAY") ? getenv("WAYLAND_DISPLAY") : "unset"); return 1; } struct wl_registry *registry = wl_display_get_registry(display); wl_registry_add_listener(registry, ®istry_listener, NULL); wl_display_roundtrip(display); if (!compositor || !shm) { fprintf(stderr, "wltest: compositor/shm missing\n"); return 1; } /* Layer mode: create a wlr_layer_surface instead of an xdg toplevel. */ if (layer_mode) { if (!layer_mgr) { fprintf(stderr, "wltest: wlr_layer_shell_v1 not supported\n"); return 1; } /* PIXEL[] and W/H were already parsed from the trailing args. */ surface = wl_compositor_create_surface(compositor); layer_surface = zwlr_layer_shell_v1_get_layer_surface( layer_mgr, surface, NULL, layer_id, "wltest"); zwlr_layer_surface_v1_set_anchor(layer_surface, layer_anchor); zwlr_layer_surface_v1_set_exclusive_zone(layer_surface, layer_exclusive); if (layer_w > 0 || layer_h > 0) zwlr_layer_surface_v1_set_size(layer_surface, (uint32_t)layer_w, (uint32_t)layer_h); zwlr_layer_surface_v1_set_keyboard_interactivity(layer_surface, ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE); zwlr_layer_surface_v1_add_listener(layer_surface, &layer_listener, NULL); wl_surface_commit(surface); wl_display_roundtrip(display); for (;;) { if (wl_display_dispatch(display) < 0) break; } return 0; } if (!wm_base) { fprintf(stderr, "wltest: xdg_wm_base missing\n"); return 1; } surface = wl_compositor_create_surface(compositor); xdg_surface = xdg_wm_base_get_xdg_surface(wm_base, surface); toplevel = xdg_surface_get_toplevel(xdg_surface); xdg_toplevel_set_title(toplevel, 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; }