Nested mode: re-create host seat keyboard/pointer on capabilities changes

labwc (wlroots) invalidates every object created from a wl_seat when it
tears down its input backend across a VT round-trip; we fetched
host.keyboard/host.pointer once at host_init and never re-created them,
so the nested client kept running silently with no keyboard events ever
arriving ([kbd] lines stopped, Alt bindings dead).

Bind a wl_seat listener at registry time and move the one-time seats
objects into a capabilities handler: drop the keyboard/pointer objects
when the capability disappears, re-create (plus re-add listeners) when it
returns. The compositor re-sends keymap/enter on the fresh wl_keyboard,
and host_kbd_enter re-focuses our nested seat, restoring input.

Also handle wl_seat global remove/re-add (some compositors recreate the
global rather than toggling capabilities): null the seat objects and
reset the clipboard bridge so a fresh seat re-runs bridge_init() --
which is now re-entrant. Added [seat]/[kbd] DEBUG lines for Pi triage.

Tests: 32/32.
This commit is contained in:
Andrew Hamilton 2026-08-16 22:02:55 +10:00
parent 6ccb3e8d12
commit 361a0b23b1

View file

@ -208,6 +208,8 @@ struct tabbed_host {
struct wl_seat *seat;
struct wl_keyboard *keyboard;
struct wl_pointer *pointer;
uint32_t seat_name; /* registry name of the bound wl_seat global */
bool registry_synced; /* initial registry scan completed */
bool configured;
bool fullscreen;
int width, height;
@ -2862,6 +2864,19 @@ static void bridge_init(void) {
tabbed_log(TABBED_LOG_INFO, "clipboard bridge: no host seat; disabled\n");
return;
}
/* Re-entrant: drop any objects bound to a previous host seat so a runtime
seat re-add (registry churn, e.g. VT round-trip) re-binds cleanly. */
if (bridge.dd)
wl_data_device_release(bridge.dd);
bridge.dd = NULL;
if (bridge.dc_device)
zwlr_data_control_device_v1_destroy(bridge.dc_device);
bridge.dc_device = NULL;
bridge.dd_offer = NULL;
bridge.dc_offer = NULL;
bridge.dd_echo_offer = NULL;
bridge.dc_echo_offer = NULL;
bridge.backend = BRIDGE_BACKEND_NONE;
if (bridge.ddm) {
bridge.dd = wl_data_device_manager_get_data_device(bridge.ddm, host.seat);
wl_data_device_add_listener(bridge.dd, &bridge_dd_listener, NULL);
@ -2918,6 +2933,18 @@ static void bridge_cleanup(void) {
/* ===================== Host-side listeners ===================== */
/* wl_seat capabilities handler: see host_seat_capabilities() below (defined
after the host keyboard/pointer listeners it installs). */
static void host_seat_capabilities(void *data, struct wl_seat *seat,
uint32_t capabilities);
static void host_seat_name(void *data, struct wl_seat *seat, const char *name) {
(void)data; (void)seat; (void)name;
}
static const struct wl_seat_listener host_seat_listener = {
.capabilities = host_seat_capabilities,
.name = host_seat_name,
};
static void host_registry_global(void *data, struct wl_registry *registry,
uint32_t name, const char *interface,
uint32_t version)
@ -2933,8 +2960,17 @@ static void host_registry_global(void *data, struct wl_registry *registry,
host.decoration = wl_registry_bind(registry, name,
&zxdg_decoration_manager_v1_interface, 1);
} else if (strcmp(interface, wl_seat_interface.name) == 0) {
/* Bind the seat and rely on the capabilities event (which the
compositor always sends after a bind) to (re)create the keyboard
and pointer objects -- including after a VT round-trip, where the
compositor invalidates seat objects and a re-bind or capability
re-gain must be observed to keep receiving input. */
host.seat = wl_registry_bind(registry, name,
&wl_seat_interface, version < 5 ? version : 5);
host.seat_name = name;
wl_seat_add_listener(host.seat, &host_seat_listener, NULL);
if (host.registry_synced)
bridge_init(); /* runtime seat re-add: re-establish the bridge */
} else if (strcmp(interface, wl_data_device_manager_interface.name) == 0) {
bridge.ddm = wl_registry_bind(registry, name,
&wl_data_device_manager_interface, version < 3 ? version : 3);
@ -2948,7 +2984,27 @@ static void host_registry_global(void *data, struct wl_registry *registry,
static void host_registry_global_remove(void *data, struct wl_registry *registry,
uint32_t name)
{
(void)data; (void)registry; (void)name;
(void)data; (void)registry;
/* Only the wl_seat global's removal matters to us: its children (keyboard,
pointer, clipboard data devices) are invalidated with it and must be
re-created when the compositor advertises a new seat. Other globals we
bind (compositor, xdg_wm_base, ...) are process-lifetime. */
if (host.seat_name && name == host.seat_name) {
tabbed_log(TABBED_LOG_DEBUG, "[seat] global %u removed\n", name);
host.seat_name = 0;
host.seat = NULL;
host.keyboard = NULL;
host.pointer = NULL;
/* Seat-owned bridge objects are invalidated too; null them so the
next bridge_init() on a fresh seat re-binds cleanly. */
bridge.dd = NULL;
bridge.dc_device = NULL;
bridge.dd_offer = NULL;
bridge.dc_offer = NULL;
bridge.dd_echo_offer = NULL;
bridge.dc_echo_offer = NULL;
bridge.backend = BRIDGE_BACKEND_NONE;
}
}
static const struct wl_registry_listener host_registry_listener = {
@ -3149,6 +3205,8 @@ static void host_kbd_enter(void *data, struct wl_keyboard *kbd, uint32_t serial,
bridge.host_set_pending = false;
bridge_host_set_selection(server.seat->selection_source);
}
tabbed_log(TABBED_LOG_DEBUG, "[kbd] enter surface=%p serial=%u active=%p\n",
(void *)surface, serial, (void *)server.active);
if (server.active)
wlr_seat_keyboard_notify_enter(server.seat,
server.active->base->surface,
@ -3160,6 +3218,8 @@ static void host_kbd_leave(void *data, struct wl_keyboard *kbd, uint32_t serial,
struct wl_surface *surface)
{
(void)data; (void)kbd; (void)serial; (void)surface;
tabbed_log(TABBED_LOG_DEBUG, "[kbd] leave surface=%p serial=%u\n",
(void *)surface, serial);
wlr_seat_keyboard_notify_clear_focus(server.seat);
}
@ -3967,6 +4027,43 @@ static const struct wl_pointer_listener host_ptr_listener = {
.axis_discrete = host_ptr_axis_discrete,
};
/* When the host compositor (labwc on a VT round-trip) removes a seat
capability, every object created from that seat -- our wl_keyboard /
wl_pointer -- is invalidated, yet the client keeps running silently with
no input ever arriving again. Re-create the objects as capabilities are
re-gained; the compositor re-sends keymap/modifiers/enter on the fresh
wl_keyboard, and host_kbd_enter() re-focuses our nested seat. */
static void host_seat_capabilities(void *data, struct wl_seat *seat,
uint32_t capabilities)
{
(void)data; (void)seat;
tabbed_log(TABBED_LOG_DEBUG, "[seat] capabilities=0x%x\n", capabilities);
const bool have_kbd = (capabilities & WL_SEAT_CAPABILITY_KEYBOARD) != 0;
const bool have_ptr = (capabilities & WL_SEAT_CAPABILITY_POINTER) != 0;
if (have_kbd && !host.keyboard) {
host.keyboard = wl_seat_get_keyboard(host.seat);
if (host.keyboard) {
tabbed_log(TABBED_LOG_DEBUG, "[seat] re-gained keyboard\n");
wl_keyboard_add_listener(host.keyboard, &host_kbd_listener, NULL);
}
} else if (!have_kbd && host.keyboard) {
wl_keyboard_release(host.keyboard);
host.keyboard = NULL;
tabbed_log(TABBED_LOG_DEBUG, "[seat] keyboard capability lost\n");
}
if (have_ptr && !host.pointer) {
host.pointer = wl_seat_get_pointer(host.seat);
if (host.pointer) {
tabbed_log(TABBED_LOG_DEBUG, "[seat] re-gained pointer\n");
wl_pointer_add_listener(host.pointer, &host_ptr_listener, NULL);
}
} else if (!have_ptr && host.pointer) {
wl_pointer_release(host.pointer);
host.pointer = NULL;
tabbed_log(TABBED_LOG_DEBUG, "[seat] pointer capability lost\n");
}
}
/* ===================== Host init / EGL ===================== */
static bool host_init(void) {
@ -3984,6 +4081,7 @@ static bool host_init(void) {
host.registry = wl_display_get_registry(host.display);
wl_registry_add_listener(host.registry, &host_registry_listener, NULL);
wl_display_roundtrip(host.display);
host.registry_synced = true;
if (!host.compositor || !host.wm_base) {
fprintf(stderr, "tabbed-wlroots: host lacks compositor/xdg_wm_base\n");
@ -4011,14 +4109,8 @@ static bool host_init(void) {
wl_surface_commit(host.surface);
wl_display_roundtrip(host.display);
if (host.seat) {
host.keyboard = wl_seat_get_keyboard(host.seat);
host.pointer = wl_seat_get_pointer(host.seat);
if (host.keyboard)
wl_keyboard_add_listener(host.keyboard, &host_kbd_listener, NULL);
if (host.pointer)
wl_pointer_add_listener(host.pointer, &host_ptr_listener, NULL);
}
/* Host keyboard/pointer are created by the seat capabilities handler
during the registry round-trip above; nothing more to fetch here. */
bridge_init();
/* Try EGL on the host wayland platform */