labwc sometimes fails to broadcast keyboard layout changes if an application is using a virtual keyboard
Problem description
I think this is probably a duplicate of https://github.com/labwc/labwc/issues/2723, but I'm not entirely sure. If it's not a duplicate, it's at least very likely related.
If:
- an application that uses the
virtual-keyboard-unstable-v1.xmlprotocol is running and has a virtual keyboard actively attached to the compositor, and - the compositor has not yet received a keystroke event originating from the machine's physical keyboard, and
- the user (somehow) modifies
~/.config/labwc/environmentand sets a different keyboard layout with theXKB_DEFAULT_LAYOUToption, and - the user then attempts to make that keyboard layout change take effect by running
labwc --reconfigure,
then labwc will not broadcast the keyboard layout change to all running applications. The user must press a physical key on the keyboard in order to cause the new keyboard layout to be broadcast to all applications.
This sounds like a ridiculous edge case no one will ever hit, especially since pressing a single key on the system's physical keyboard solves the issue, but there is one instance in which this can pose a serious problem, and that is when you have a Wayland client running that grabs all keyboard input devices on the system and proxies them to the compositor via the virtual-keyboard-unstable-v1.xml protocol. kloak is one such application. Because it both provides a virtual keyboard to labwc, and grabs exclusive access to all input devices, labwc doesn't broadcast keyboard layout changes because of the virtual keyboard, and will never start broadcasting them because it no longer is receiving keystrokes from the physical keyboard.
Steps to reproduce
There's probably an eaasier way to reproduce this, but the way I came up with was to use a custom Wayland client. I wrote this initially trying to figure out where the bug was in kloak, before discovering the issue was in labwc.
- Create a directory at
~/virt-kb-test. - Paste the following code into
~/virt-kb-test/kb_test.c:
#define _GNU_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdint.h>
#include <stdbool.h>
#include <unistd.h>
#include <sys/types.h>
#include <dirent.h>
#include <sys/ioctl.h>
#include <linux/input.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <wayland-client.h>
#include "virtual-keyboard.h"
struct disp_state {
struct wl_display *display;
struct wl_registry *registry;
struct wl_seat *seat;
struct wl_keyboard *kb;
uint32_t seat_caps;
bool seat_set;
struct zwp_virtual_keyboard_manager_v1 *virt_kb_manager;
struct zwp_virtual_keyboard_v1 *virt_kb;
char *old_kb_map_shm;
int32_t old_kb_map_shm_size;
};
static void registry_handle_global(void *data, struct wl_registry * registry,
uint32_t name, const char * interface, uint32_t version);
static void registry_handle_global_remove(void *data,
struct wl_registry * registry, uint32_t name);
static void seat_handle_name(void *data, struct wl_seat *seat,
const char *name);
static void seat_handle_capabilities(void *data, struct wl_seat *seat,
uint32_t capabilities);
static void kb_handle_keymap(void *data, struct wl_keyboard *kb,
uint32_t format, int32_t fd, uint32_t size);
static void kb_handle_enter(void *data, struct wl_keyboard *kb,
uint32_t serial, struct wl_surface *surface, struct wl_array *keys);
static void kb_handle_leave(void *data, struct wl_keyboard *kb,
uint32_t serial, struct wl_surface *surface);
static void kb_handle_key(void *data, struct wl_keyboard *kb, uint32_t serial,
uint32_t time, uint32_t key, uint32_t state);
static void kb_handle_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);
static void kb_handle_repeat_info(void *data, struct wl_keyboard *kb,
int32_t rate, int32_t delay);
static const struct wl_registry_listener registry_listener = {
.global = registry_handle_global,
.global_remove = registry_handle_global_remove,
};
static const struct wl_seat_listener seat_listener = {
.name = seat_handle_name,
.capabilities = seat_handle_capabilities,
};
static const struct wl_keyboard_listener kb_listener = {
.keymap = kb_handle_keymap,
.enter = kb_handle_enter,
.leave = kb_handle_leave,
.key = kb_handle_key,
.modifiers = kb_handle_modifiers,
.repeat_info = kb_handle_repeat_info,
};
struct disp_state state = { 0 };
static void registry_handle_global(void *data, struct wl_registry * registry,
uint32_t name, const char * interface, uint32_t version) {
struct disp_state *param_state = data;
if (strcmp(interface, wl_seat_interface.name) == 0) {
if (!param_state->seat_set) {
param_state->seat = wl_registry_bind(registry, name, &wl_seat_interface, 9);
wl_seat_add_listener(param_state->seat, &seat_listener, &state);
param_state->seat_set = true;
} else {
fprintf(stderr,
"WARNING: Multiple seats detected, all but first will be ignored.\n");
}
} else if (strcmp(interface, zwp_virtual_keyboard_manager_v1_interface.name) == 0) {
param_state->virt_kb_manager = wl_registry_bind(registry, name, &zwp_virtual_keyboard_manager_v1_interface, 1);
}
}
static void registry_handle_global_remove(void *data,
struct wl_registry * registry, uint32_t name) {
;
}
static void seat_handle_name(void *data, struct wl_seat *seat,
const char *name) {
;
}
static void seat_handle_capabilities(void *data, struct wl_seat *seat,
uint32_t capabilities) {
struct disp_state *param_state = data;
param_state->seat_caps = capabilities;
if (capabilities | WL_SEAT_CAPABILITY_KEYBOARD) {
param_state->kb = wl_seat_get_keyboard(seat);
wl_keyboard_add_listener(param_state->kb, &kb_listener, param_state);
} else {
fprintf(stderr,
"FATAL ERROR: No keyboard capability for seat, cannot continue.\n");
exit(1);
}
}
static void kb_handle_keymap(void *data, struct wl_keyboard *kb,
uint32_t format, int32_t fd, uint32_t size) {
fprintf(stderr, "Keymap changed!\n");
if (state.virt_kb == NULL && state.virt_kb_manager != NULL) {
state.virt_kb = zwp_virtual_keyboard_manager_v1_create_virtual_keyboard(
state.virt_kb_manager, state.seat);
}
char *kb_map_shm = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
if (kb_map_shm == MAP_FAILED) {
fprintf(stderr, "FATAL ERROR: Could nto mmap xkb layout!\n");
exit(1);
}
if (state.old_kb_map_shm) {
if (size == state.old_kb_map_shm_size && memcmp(state.old_kb_map_shm, kb_map_shm, size) == 0) {
munmap(kb_map_shm, size);
close(fd);
return;
} else {
munmap(state.old_kb_map_shm, (size_t)(state.old_kb_map_shm_size));
}
}
if (state.virt_kb != NULL) {
zwp_virtual_keyboard_v1_keymap(state.virt_kb, format, fd, size);
}
state.old_kb_map_shm = kb_map_shm;
state.old_kb_map_shm_size = (int32_t)(size);
close(fd);
}
static void kb_handle_enter(void *data, struct wl_keyboard *kb,
uint32_t serial, struct wl_surface *surface, struct wl_array *keys) {
wl_array_release(keys);
}
static void kb_handle_leave(void *data, struct wl_keyboard *kb,
uint32_t serial, struct wl_surface *surface) {
;
}
static void kb_handle_key(void *data, struct wl_keyboard *kb, uint32_t serial,
uint32_t time, uint32_t key, uint32_t state) {
;
}
static void kb_handle_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) {
;
}
static void kb_handle_repeat_info(void *data, struct wl_keyboard *kb,
int32_t rate, int32_t delay) {
;
}
int main(int argc, char **argv) {
state.virt_kb = NULL;
state.virt_kb_manager = NULL;
state.display = wl_display_connect(NULL);
if (!state.display) {
fprintf(stderr, "FATAL ERROR: Could not get Wayland display!\n");
exit(1);
}
state.registry = wl_display_get_registry(state.display);
if (!state.registry) {
fprintf(stderr, "FATAL ERROR: Could not get Wayland registry!\n");
exit(1);
}
wl_registry_add_listener(state.registry, ®istry_listener, &state);
wl_display_roundtrip(state.display);
while (wl_display_dispatch(state.display)) {
;
}
}
- Preferably review the above code before running it since it's always a bad idea to trust random code from strangers on the Internet. :P
- Obtain
virtual-keyboard-unstable-v1.xmlfrom wlroots, i.e. from https://gitlab.freedesktop.org/wlroots/wlroots/-/blob/5adf325333602d5b1e7ccdeb122633bbc8040ace/protocol/virtual-keyboard-unstable-v1.xml - Create virtual-keyboard.c:
wayland-scanner private-code virtual-keyboard-unstable-v1.xml virtual-keyboard.c - Create virtual-keyboard.h:
wayland-scanner client-header virtual-keyboard-unstable-v1.xml virtual-keyboard.c - Compile the client:
gcc kb_test.c virtual-keyboard.c -lwayland-client - Before executing the client, SSH into the machine.
- In the SSH console, run
export LABWC_PID="$(pgrep labwc)"so you can reconfigure the compositor from here later. - On the physical machine, ensure you are in the
virt-kb-testdirectory, then runsleep 1; ./a.out. Do not touch the physical system's keyboard after starting the program, if you do you will have to stop and restart the program to reproduce the issue. - Wait for a couple of "Keymap changed!" messages to be printed.
- In the SSH console, run
mkdir -p ~/.config/labwc; cd ~/.config/labwc; vim environment, and setXKB_DEFAULT_LAYOUTto something other than whatever it is set to currently. (I've been switching betweendeandusfor my testing.) - In the SSH console, save and close the file, then run
labwc --reconfigure. You should not see a new "Keymap changed!" line appear on the testing system. - In the SSH console, change
XKB_DEFAULT_LAYOUTin~/.config/labwc/environmentagain, then runlabwc --reconfigureagain. You should still not see a new "Keymap changed!" line appear on the testing system. - On the testing system, tap any key once. I usually just tap the left control key. You should immediately see a new "Keymap changed!" line appear.
- In the SSH console, change
XKB_DEFAULT_LAYOUTin~/.config/labwc/environmentagain, then runlabwc --reconfigureagain. This time, you should see a new "Keymap changed!" line appear on the testing system.
labwc build source
Release
labwc version
0.9.1
labwc environment
From a TTY or some display manager like lightdm
Distribution
Arch Linux
I don't have the capacity currently to look into the actual issue right now but one thing stood out to me:
when you have a Wayland client running that grabs all keyboard input devices on the system and proxies them to the compositor via the
virtual-keyboard-unstable-v1.xmlprotocol. kloak is one such application.
Wouldn't injecting the inputs via uinput rather than via the wlroots virtual keyboard and mouse protocols solve the issue? And also make the approach work regardless of the choice of the wayland compositor, X11 or even TTY?
@Consolatis uinput is unsuitable because it operates at the evdev layer, which works fine for keyboard but is very unsafe to manipulate directly when dealing with some pointing devices, especially touchpads. The original version of kloak used evdev and uinput directly, and Whonix ran into severe complications when attempting to fix its mouse and touchpad support. See https://who-t.blogspot.com/2018/07/why-its-not-good-idea-to-handle-evdev.html. (And yes, the very problems mentioned in this post are the ones we started running into.) See also https://forums.whonix.org/t/better-mouse-obfuscation/21445/7, this is the rationale for working with the compositor directly. You can't use libinput to get event codes to pass to uinput, but you can use it when working with the virtual keyboard and mouse devices.
Thanks for the detailed information, very interesting. Especially for someone who wrote a evdev -> network -> uinput framework as a toy project a while ago and thought about adding touchpad support to it.
I'll try to look at the actual issue in the next days.
Completely untested:
diff --git a/src/input/keyboard.c b/src/input/keyboard.c
index ae92e73f..593f5872 100644
--- a/src/input/keyboard.c
+++ b/src/input/keyboard.c
@@ -779,6 +779,7 @@ set_layout(struct server *server, struct wlr_keyboard *kb)
if (!wlr_keyboard_keymaps_match(kb->keymap, keymap)) {
wlr_keyboard_set_keymap(kb, keymap);
reset_window_keyboard_layout_groups(server);
+ wlr_seat_set_keyboard(server->seat.seat, kb);
}
xkb_keymap_unref(keymap);
} else {
I am also not sure if this may cause other issues. You can compile the labwc master branch by
# if on Debian
# sudo apt-get build-dep labwc
git clone https://github.com/labwc/labwc
cd labwc
# apply diff
meson setup build
meson compile -C build
# start labwc right from the build directory, no install necessary
Will give it a shot and report back, thanks!
On Tue, Sep 30, 2025 at 12:52 PM Consolatis @.***> wrote:
Consolatis left a comment (labwc/labwc#3113) https://github.com/labwc/labwc/issues/3113#issuecomment-3353232003
Completely untested:
diff --git a/src/input/keyboard.c b/src/input/keyboard.c index ae92e73f..593f5872 100644--- a/src/input/keyboard.c+++ b/src/input/keyboard.c@@ -779,6 +779,7 @@ set_layout(struct server *server, struct wlr_keyboard *kb) if (!wlr_keyboard_keymaps_match(kb->keymap, keymap)) { wlr_keyboard_set_keymap(kb, keymap); reset_window_keyboard_layout_groups(server);+ wlr_seat_set_keyboard(server->seat.seat, kb); } xkb_keymap_unref(keymap); } else {
I am also not sure if this may cause other issues. You can compile the labwc master branch by
sudo apt-get build-dep -y labwc git clone https://github.com/labwc/labwccd labwc# apply diff meson setup build meson compile -C build# start labwc right from the build directory, no install necessary
— Reply to this email directly, view it on GitHub https://github.com/labwc/labwc/issues/3113#issuecomment-3353232003, or unsubscribe https://github.com/notifications/unsubscribe-auth/AZAFFER6XARQHFEEBXETB233VK7MZAVCNFSM6AAAAACH3OWC6CVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZTGNJTGIZTEMBQGM . You are receiving this because you authored the thread.Message ID: @.***>
@Consolatis Inconclusive results... on the surface the change seems to fix the issue with the example client shared above. With kloak itself though, I can get the keyboard layout change mechanism stuck in a busy loop if I run labwc --reconfigure on the physical system while kloak is running. If I manage to release the Enter key before the server is done processing the keyboard layout change request, this happens:
--- I start kloak here ---
[3703959.236] {Default Queue} -> wl_display#1.get_registry(new id wl_registry#2)
[3703959.336] {Default Queue} -> wl_display#1.sync(new id wl_callback#3)
[3703959.908] {Display Queue} wl_display#1.delete_id(3)
[3703959.984] {Default Queue} wl_registry#2.global(1, "wl_shm", 2)
[3703960.426] {Default Queue} -> wl_registry#2.bind(1, "wl_shm", 2, new id [unknown]#4)
[3703960.482] {Default Queue} wl_registry#2.global(2, "wl_drm", 2)
[3703960.526] {Default Queue} wl_registry#2.global(3, "zwp_linux_dmabuf_v1", 4)
[3703960.558] {Default Queue} wl_registry#2.global(4, "wp_linux_drm_syncobj_manager_v1", 1)
[3703960.597] {Default Queue} wl_registry#2.global(5, "zcosmic_workspace_manager_v1", 1)
[3703960.625] {Default Queue} wl_registry#2.global(6, "ext_workspace_manager_v1", 1)
[3703960.661] {Default Queue} wl_registry#2.global(7, "zwlr_gamma_control_manager_v1", 1)
[3703960.687] {Default Queue} wl_registry#2.global(8, "zxdg_output_manager_v1", 3)
[3703960.729] {Default Queue} -> wl_registry#2.bind(8, "zxdg_output_manager_v1", 3, new id [unknown]#5)
[3703960.761] {Default Queue} wl_registry#2.global(9, "zwlr_output_manager_v1", 4)
[3703960.790] {Default Queue} wl_registry#2.global(10, "wl_compositor", 6)
[3703960.836] {Default Queue} -> wl_registry#2.bind(10, "wl_compositor", 5, new id [unknown]#6)
[3703960.881] {Default Queue} wl_registry#2.global(11, "wl_subcompositor", 1)
[3703960.902] {Default Queue} wl_registry#2.global(12, "wl_data_device_manager", 3)
[3703960.926] {Default Queue} wl_registry#2.global(13, "zwp_primary_selection_device_manager_v1", 1)
[3703960.949] {Default Queue} wl_registry#2.global(14, "zwp_input_method_manager_v2", 1)
[3703960.975] {Default Queue} wl_registry#2.global(15, "zwp_text_input_manager_v3", 1)
[3703960.999] {Default Queue} wl_registry#2.global(16, "wl_seat", 9)
[3703961.029] {Default Queue} -> wl_registry#2.bind(16, "wl_seat", 9, new id [unknown]#7)
[3703961.071] {Default Queue} wl_registry#2.global(17, "zwlr_virtual_pointer_manager_v1", 2)
[3703961.155] {Default Queue} -> wl_registry#2.bind(17, "zwlr_virtual_pointer_manager_v1", 2, new id [unknown]#8)
[3703961.202] {Default Queue} -> zwlr_virtual_pointer_manager_v1#8.create_virtual_pointer(nil, new id zwlr_virtual_pointer_v1#9)
[3703961.239] {Default Queue} wl_registry#2.global(18, "zwp_virtual_keyboard_manager_v1", 1)
[3703961.282] {Default Queue} -> wl_registry#2.bind(18, "zwp_virtual_keyboard_manager_v1", 1, new id [unknown]#10)
[3703961.320] {Default Queue} wl_registry#2.global(19, "zwp_pointer_gestures_v1", 3)
[3703961.347] {Default Queue} wl_registry#2.global(20, "wp_cursor_shape_manager_v1", 1)
[3703961.368] {Default Queue} wl_registry#2.global(21, "xdg_wm_base", 6)
[3703961.400] {Default Queue} wl_registry#2.global(22, "xdg_activation_v1", 1)
[3703961.422] {Default Queue} wl_registry#2.global(23, "xdg_toplevel_icon_manager_v1", 1)
[3703961.450] {Default Queue} wl_registry#2.global(24, "org_kde_kwin_server_decoration_manager", 1)
[3703961.474] {Default Queue} wl_registry#2.global(25, "zxdg_decoration_manager_v1", 1)
[3703961.501] {Default Queue} wl_registry#2.global(26, "wp_presentation", 2)
[3703961.523] {Default Queue} wl_registry#2.global(27, "zwlr_export_dmabuf_manager_v1", 1)
[3703961.550] {Default Queue} wl_registry#2.global(28, "zwlr_screencopy_manager_v1", 3)
[3703961.570] {Default Queue} wl_registry#2.global(29, "ext_image_copy_capture_manager_v1", 1)
[3703961.595] {Default Queue} wl_registry#2.global(30, "ext_output_image_capture_source_manager_v1", 1)
[3703961.620] {Default Queue} wl_registry#2.global(31, "zwlr_data_control_manager_v1", 2)
[3703961.656] {Default Queue} wl_registry#2.global(32, "ext_data_control_manager_v1", 1)
[3703961.685] {Default Queue} wl_registry#2.global(33, "wp_security_context_manager_v1", 1)
[3703961.719] {Default Queue} wl_registry#2.global(34, "wp_viewporter", 1)
[3703961.740] {Default Queue} wl_registry#2.global(35, "wp_single_pixel_buffer_manager_v1", 1)
[3703961.767] {Default Queue} wl_registry#2.global(36, "wp_fractional_scale_manager_v1", 1)
[3703961.788] {Default Queue} wl_registry#2.global(37, "ext_idle_notifier_v1", 2)
[3703961.819] {Default Queue} wl_registry#2.global(38, "zwp_idle_inhibit_manager_v1", 1)
[3703961.886] {Default Queue} wl_registry#2.global(39, "zwp_relative_pointer_manager_v1", 1)
[3703961.924] {Default Queue} wl_registry#2.global(40, "zwp_pointer_constraints_v1", 1)
[3703961.951] {Default Queue} wl_registry#2.global(41, "zwlr_foreign_toplevel_manager_v1", 3)
[3703961.984] {Default Queue} wl_registry#2.global(42, "ext_foreign_toplevel_list_v1", 1)
[3703962.004] {Default Queue} wl_registry#2.global(43, "wp_alpha_modifier_v1", 1)
[3703962.035] {Default Queue} wl_registry#2.global(44, "ext_session_lock_manager_v1", 1)
[3703962.056] {Default Queue} wl_registry#2.global(45, "wp_drm_lease_device_v1", 1)
[3703962.082] {Default Queue} wl_registry#2.global(46, "zwlr_output_power_manager_v1", 1)
[3703962.104] {Default Queue} wl_registry#2.global(47, "wp_tearing_control_manager_v1", 1)
[3703962.131] {Default Queue} wl_registry#2.global(48, "zwp_tablet_manager_v2", 1)
[3703962.153] {Default Queue} wl_registry#2.global(49, "zwlr_layer_shell_v1", 4)
[3703962.186] {Default Queue} -> wl_registry#2.bind(49, "zwlr_layer_shell_v1", 4, new id [unknown]#11)
[3703962.218] {Default Queue} wl_registry#2.global(50, "zxdg_exporter_v1", 1)
[3703962.244] {Default Queue} wl_registry#2.global(51, "zxdg_importer_v1", 1)
[3703962.272] {Default Queue} wl_registry#2.global(52, "zxdg_exporter_v2", 1)
[3703962.298] {Default Queue} wl_registry#2.global(53, "zxdg_importer_v2", 1)
[3703962.325] {Default Queue} wl_registry#2.global(55, "wl_output", 4)
[3703962.353] {Default Queue} -> wl_registry#2.bind(55, "wl_output", 4, new id [unknown]#12)
[3703962.385] {Default Queue} -> wl_compositor#6.create_surface(new id wl_surface#13)
[3703962.428] {Default Queue} -> zwlr_layer_shell_v1#11.get_layer_surface(new id zwlr_layer_surface_v1#14, wl_surface#13, wl_output#12, 3, "com.kicksecure.kloak")
[3703962.463] {Default Queue} -> zwlr_layer_surface_v1#14.set_anchor(1)
[3703962.492] {Default Queue} -> zwlr_layer_surface_v1#14.set_anchor(2)
[3703962.550] {Default Queue} -> zwlr_layer_surface_v1#14.set_anchor(4)
[3703962.584] {Default Queue} -> zwlr_layer_surface_v1#14.set_anchor(8)
[3703962.696] {Default Queue} -> zwlr_layer_surface_v1#14.set_exclusive_zone(-1)
[3703962.723] {Default Queue} -> wl_surface#13.commit()
[3703962.769] {Default Queue} -> zxdg_output_manager_v1#5.get_xdg_output(new id zxdg_output_v1#15, wl_output#12)
[3703962.798] {Default Queue} wl_callback#3.done(2316)
[3703962.835] {Default Queue} -> zwp_virtual_keyboard_manager_v1#10.create_virtual_keyboard(wl_seat#7, new id zwp_virtual_keyboard_v1#3)
[3703962.976] {Default Queue} -> wl_display#1.sync(new id wl_callback#16)
[3703964.765] {Display Queue} wl_display#1.delete_id(16)
[3703964.819] {Default Queue} discarded wl_shm#4.format(0)
[3703964.849] {Default Queue} discarded wl_shm#4.format(1)
[3703964.869] {Default Queue} discarded wl_shm#4.format(875709016)
[3703964.896] {Default Queue} discarded wl_shm#4.format(875708993)
[3703964.913] {Default Queue} discarded wl_shm#4.format(875710274)
[3703964.933] {Default Queue} discarded wl_shm#4.format(842094674)
[3703964.951] {Default Queue} discarded wl_shm#4.format(842088786)
[3703964.971] {Default Queue} discarded wl_shm#4.format(892426322)
[3703964.986] {Default Queue} discarded wl_shm#4.format(892420434)
[3703965.012] {Default Queue} discarded wl_shm#4.format(909199186)
[3703965.028] {Default Queue} discarded wl_shm#4.format(808665688)
[3703965.048] {Default Queue} discarded wl_shm#4.format(808665665)
[3703965.068] {Default Queue} discarded wl_shm#4.format(1211384408)
[3703965.095] {Default Queue} discarded wl_shm#4.format(1211384385)
[3703965.114] {Default Queue} discarded wl_shm#4.format(942948952)
[3703965.133] {Default Queue} discarded wl_shm#4.format(942948929)
[3703965.149] {Default Queue} wl_seat#7.name("seat0")
[3703965.172] {Default Queue} wl_seat#7.capabilities(3)
[3703965.192] {Default Queue} -> wl_seat#7.get_keyboard(new id wl_keyboard#17)
[3703965.223] {Default Queue} wl_output#12.geometry(0, 0, 340, 210, 0, "BOE", "0x0ACA", 0)
[3703965.247] {Default Queue} wl_output#12.mode(1, 2560, 1600, 90003)
[3703965.273] {Default Queue} wl_output#12.scale(1)
[3703965.291] {Default Queue} wl_output#12.name("eDP-1")
[3703965.361] {Default Queue} wl_output#12.description("BOE 0x0ACA (eDP-1)")
[3703965.378] {Default Queue} wl_output#12.done()
[3703965.402] {Default Queue} zwlr_layer_surface_v1#14.configure(2317, 2560, 1600)
[3703965.523] {Default Queue} -> wl_shm#4.create_pool(new id wl_shm_pool#18, fd 6, 49152000)
[3703965.563] {Default Queue} -> wl_compositor#6.create_region(new id wl_region#19)
[3703965.625] {Default Queue} -> wl_region#19.add(0, 0, 0, 0)
[3703965.675] {Default Queue} -> wl_surface#13.set_input_region(wl_region#19)
[3703965.701] {Default Queue} -> zwlr_layer_surface_v1#14.ack_configure(2317)
[3703965.725] {Default Queue} -> wl_region#19.destroy()
[3703965.750] {Default Queue} -> wl_shm_pool#18.create_buffer(new id wl_buffer#20, 0, 2560, 1600, 10240, 0)
[3703965.781] {Default Queue} -> wl_surface#13.attach(wl_buffer#20, 0, 0)
[3703965.816] {Default Queue} -> wl_surface#13.commit()
[3703965.840] {Default Queue} zxdg_output_v1#15.name("eDP-1")
[3703965.860] {Default Queue} zxdg_output_v1#15.description("BOE 0x0ACA (eDP-1)")
[3703965.881] {Default Queue} zxdg_output_v1#15.logical_position(0, 0)
[3703965.901] {Default Queue} zxdg_output_v1#15.logical_size(2560, 1600)
[3703965.934] {Default Queue} wl_output#12.done()
[3703966.231] {Default Queue} wl_callback#16.done(2318)
[3704208.044] {Display Queue} wl_display#1.delete_id(19)
[3704208.066] {Default Queue} wl_keyboard#17.keymap(1, fd 6, 67303)
[3704208.088] {Default Queue} -> zwp_virtual_keyboard_v1#3.keymap(1, fd 18, 67303)
[3704211.355] {Default Queue} wl_keyboard#17.repeat_info(25, 600)
[3704211.377] {Default Queue} zwlr_layer_surface_v1#14.configure(2319, 2560, 1600)
[3704211.496] {Default Queue} -> wl_shm#4.create_pool(new id wl_shm_pool#19, fd 19, 49152000)
[3704211.512] {Default Queue} -> wl_compositor#6.create_region(new id wl_region#16)
[3704211.526] {Default Queue} -> wl_region#16.add(0, 0, 0, 0)
[3704211.538] {Default Queue} -> wl_surface#13.set_input_region(wl_region#16)
[3704211.556] {Default Queue} -> zwlr_layer_surface_v1#14.ack_configure(2319)
[3704211.574] {Default Queue} -> wl_region#16.destroy()
[3704211.589] {Default Queue} -> wl_shm_pool#19.create_buffer(new id wl_buffer#21, 16384000, 2560, 1600, 10240, 0)
[3704211.662] {Default Queue} -> wl_surface#13.damage_buffer(0, 0, 16, 16)
[3704211.681] {Default Queue} -> wl_surface#13.attach(wl_buffer#21, 0, 0)
[3704211.697] {Default Queue} -> wl_surface#13.commit()
[3704211.713] {Default Queue} discarded wl_surface#13.enter(wl_output#12)
[3704211.723] {Default Queue} zwlr_layer_surface_v1#14.configure(2320, 2560, 1600)
[3704211.766] {Default Queue} -> wl_shm#4.create_pool(new id wl_shm_pool#22, fd 21, 49152000)
[3704211.778] {Default Queue} -> wl_compositor#6.create_region(new id wl_region#23)
[3704211.794] {Default Queue} -> wl_region#23.add(0, 0, 0, 0)
[3704211.805] {Default Queue} -> wl_surface#13.set_input_region(wl_region#23)
[3704211.819] {Default Queue} -> zwlr_layer_surface_v1#14.ack_configure(2320)
[3704211.833] {Default Queue} -> wl_region#23.destroy()
[3704211.847] {Default Queue} -> wl_shm_pool#22.create_buffer(new id wl_buffer#24, 32768000, 2560, 1600, 10240, 0)
[3704211.866] {Default Queue} -> wl_surface#13.damage_buffer(0, 0, 16, 16)
[3704211.928] {Default Queue} -> wl_surface#13.damage_buffer(0, 0, 16, 16)
[3704211.940] {Default Queue} -> wl_surface#13.attach(wl_buffer#24, 0, 0)
[3704211.954] {Default Queue} -> wl_surface#13.commit()
[3704211.966] {Default Queue} wl_buffer#20.release()
[3704212.171] {Default Queue} -> wl_buffer#20.destroy()
[3704215.566] {Display Queue} wl_display#1.delete_id(16)
[3704215.592] {Display Queue} wl_display#1.delete_id(23)
[3704215.606] {Display Queue} wl_display#1.delete_id(20)
[3704215.617] {Default Queue} wl_keyboard#17.keymap(1, fd 18, 67303)
[3704215.719] {Default Queue} wl_buffer#21.release()
[3704215.791] {Default Queue} -> wl_buffer#21.destroy()
[3704215.810] {Default Queue} wl_buffer#24.release()
[3704215.826] {Default Queue} -> wl_buffer#24.destroy()
--- I start typing into the physical machine to change the keyboard layout here ---
... lots of irrelevant keystroke events ...
--- I've typed in `labwc --reconfigure` into my terminal, I hit Enter immediately after this line ---
[3784365.069] {Default Queue} -> zwp_virtual_keyboard_v1#3.modifiers(0, 0, 0, 0)
[3784365.188] {Default Queue} -> zwp_virtual_keyboard_v1#3.key(80168, 28, 1)
[3784437.113] {Display Queue} wl_display#1.delete_id(21)
[3784437.225] {Display Queue} wl_display#1.delete_id(24)
[3784437.230] {Default Queue} wl_keyboard#17.keymap(1, fd 18, 65554)
[3784437.276] {Default Queue} -> zwp_virtual_keyboard_v1#3.keymap(1, fd 19, 65554)
[3784439.326] {Default Queue} wl_keyboard#17.repeat_info(25, 600)
[3784470.681] {Default Queue} -> zwp_virtual_keyboard_v1#3.modifiers(0, 0, 0, 0)
[3784470.735] {Default Queue} -> zwp_virtual_keyboard_v1#3.key(80274, 28, 0)
[3784471.184] {Default Queue} wl_keyboard#17.keymap(1, fd 18, 65554)
[3784471.328] {Default Queue} wl_keyboard#17.repeat_info(25, 600)
--- Looks like the keymap change was processed before I released Enter, so things work. Trying again... ---
... more irrelevant keystroke events ...
--- `labwc --reconfigure` is now in my terminal, about to hit Enter... ---
[3839790.937] {Default Queue} -> zwp_virtual_keyboard_v1#3.modifiers(0, 0, 0, 0) <-- Pressing enter here
[3839791.024] {Default Queue} -> zwp_virtual_keyboard_v1#3.key(135594, 28, 1)
[3839828.514] {Default Queue} -> zwp_virtual_keyboard_v1#3.modifiers(0, 0, 0, 0) <--- Releasing enter here
[3839828.539] {Default Queue} -> zwp_virtual_keyboard_v1#3.key(135632, 28, 0)
[3839860.199] {Default Queue} wl_keyboard#17.keymap(1, fd 18, 67303) <-- Keymap event from compositor, as expected
[3839860.328] {Default Queue} -> zwp_virtual_keyboard_v1#3.keymap(1, fd 19, 67303) <-- Switching the virtual keyboard keymap to match
[3839862.742] {Default Queue} wl_keyboard#17.repeat_info(25, 600)
[3839865.936] {Default Queue} wl_keyboard#17.keymap(1, fd 18, 65554) <-- !!! WHY DOES THE COMPOSITOR TRY TO REVERT BACK TO THE PREVIOUS LAYOUT
[3839866.017] {Default Queue} -> zwp_virtual_keyboard_v1#3.keymap(1, fd 21, 65554) <-- kloak now attempts to switch keyboard layout again, to match the previous one
[3839867.966] {Default Queue} wl_keyboard#17.repeat_info(25, 600)
[3839867.974] {Default Queue} wl_keyboard#17.keymap(1, fd 19, 67303) <-- And now labwc attempts to switch to the new layout again
[3839867.989] {Default Queue} -> zwp_virtual_keyboard_v1#3.keymap(1, fd 18, 67303) <-- kloak follows it
[3839872.540] {Default Queue} wl_keyboard#17.keymap(1, fd 18, 65554)
[3839872.577] {Default Queue} -> zwp_virtual_keyboard_v1#3.keymap(1, fd 21, 65554)
[3839874.397] {Default Queue} wl_keyboard#17.keymap(1, fd 19, 67303)
[3839874.417] {Default Queue} -> zwp_virtual_keyboard_v1#3.keymap(1, fd 18, 67303)
[3839878.993] {Default Queue} wl_keyboard#17.keymap(1, fd 18, 65554)
[3839879.051] {Default Queue} -> zwp_virtual_keyboard_v1#3.keymap(1, fd 21, 65554)
[3839881.077] {Default Queue} wl_keyboard#17.keymap(1, fd 19, 67303)
[3839881.095] {Default Queue} -> zwp_virtual_keyboard_v1#3.keymap(1, fd 18, 67303)
... above cycle keeps repeating infinitely ...
If I were to take a guess, I'd say probably what's happening is that labwc doesn't register the keyup event until after it's reloaded the keyboard layout, but does recognize that the keyup event was sent while the prior keyboard layout was in effect, and that somehow this triggers labwc to try to switch back to the older mode after kloak has already tried to change the virtual keyboard's layout to the new mode. That would then result in the cycle above, as kloak would be constantly trying to change its keyboard layout to that of labwc, while labwc is constantly trying to change its keyboard layout to that of kloak, and both of them are always one step behind the other.
Whether this is a bug in labwc, a shortcoming of this particular solution, or a bug in kloak, I don't know. kloak is designed to detect when a new keyboard layout is identical to the already loaded one and ignore those events, the above shows kloak and labwc actually switching very rapidly between two different keyboard layouts. One way to avoid the issue is to use sleep 1; labwc --reconfigure, another way is to press and hold Enter when running labwc --reconfigure so that the keyup event occurs after the keyboard layout change. Both of those are obviously bad solutions, but I figured I'd throw that in there. I tried to no avail to create a minimal Wayland client that could reproduce the issue without all of the "fanciness" of kloak, I'll probably keep trying, but I figured I may as well report back what happened.
In case it's useful, the source code for kloak (at least my fork of it that I use for developing it) is at https://github.com/ArrayBolt3/kloak/tree/arraybolt3/trixie.
Thanks for the log. There is something weird going on, e.g. why do we send the keymap twice on startup (before I start typing into the physical machine):
[3704208.066] {Default Queue} wl_keyboard#17.keymap(1, fd 6, 67303)
[3704215.617] {Default Queue} wl_keyboard#17.keymap(1, fd 18, 67303)
First of all the keymap should not change on load. And secondly we do check for the same keymap (e.g. size and memcmp() like you do in your test client) and prevent setting it again. So something fishy is going on here. This might be related to the keyboard group (a collection of physical keyboards sharing a keymap and modifier state).
Another issue is that we don't apply keymaps of virtual keyboards to the group so the feedback loop gets even more weird. I'll try to actually test and debug things in the next days.