shortcuts-inhibit: honor zwp_keyboard_shortcuts_inhibit_v1

Let a focused fullscreen client (game/VM) grab the compositor's keyboard
shortcuts.  New inhibitors for the focused surface activate immediately;
a lazy sync at grab-chord time (de)activates across focus changes so all
focus paths are covered without extra listeners.  While honored, the
tab/window grab chords are swallowed in process_key_event (quit and VT
switch stay live so a stuck client can't trap the session).

Add a --inhibit wltest mode (window + inhibitor, reports active) and two
smoke tests asserting the global is advertised and the inhibitor becomes
active on the focused surface.
This commit is contained in:
Andrew Hamilton 2026-09-03 13:56:56 +10:00
parent c64a10e8d9
commit 45df9d4818
4 changed files with 196 additions and 1 deletions

View file

@ -3388,3 +3388,49 @@ def test_standalone_headless_tearing_and_fractional_globals(tmp_path):
finally:
s.close()
def test_standalone_headless_shortcuts_inhibit_globals(tmp_path):
"""The compositor advertises zwp_keyboard_shortcuts_inhibit_manager_v1
so a fullscreen game/VM can grab the compositor's shortcuts."""
out = tmp_path / "globals.txt"
s = _spawn_standalone({}, "%s --globals-out '%s' '#112233'" % (WL, out))
try:
deadline = time.monotonic() + 15.0
while time.monotonic() < deadline:
if out.exists():
break
time.sleep(0.1)
assert out.exists(), "wltest --globals-out never wrote its file"
names = {line.split()[0] for line in out.read_text().splitlines()}
assert "zwp_keyboard_shortcuts_inhibit_manager_v1" in names, \
f"missing inhibit manager; advertised: {sorted(names)}"
finally:
s.close()
def test_standalone_headless_shortcuts_inhibit_active(tmp_path):
"""A client that inhibits shortcuts on its focused window is honored:
the compositor replies with the active event (it only activates while
the inhibitor's surface has keyboard focus)."""
out = tmp_path / "inhibit.out"
s = _spawn_standalone(
{}, "%s --inhibit --inhibit-out '%s'" % (WL, out))
try:
s.wait_any_active()
deadline = time.monotonic() + 10.0
got_active = False
while time.monotonic() < deadline:
if out.exists():
text = out.read_text().strip()
if "active" in text:
got_active = True
break
time.sleep(0.1)
assert got_active, \
f"inhibitor never became active: " \
f"{(out.read_text().strip() if out.exists() else '(missing)')!r}"
assert s.proc.poll() is None, \
f"compositor exited with inhibitor; log tail:\n{s.read_log()[-400:]}"
finally:
s.close()

View file

@ -24,6 +24,7 @@
#include "idle-inhibit-unstable-v1-client.h"
#include "ext-session-lock-v1-client.h"
#include "wlr-screencopy-unstable-v1-client.h"
#include "keyboard-shortcuts-inhibit-unstable-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
@ -118,6 +119,7 @@ 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 zwlr_screencopy_manager_v1 *sc_mgr;
static struct zwp_keyboard_shortcuts_inhibit_manager_v1 *kb_inhibit_mgr;
static struct wl_output *wl_output;
struct foreign_state {
@ -728,6 +730,11 @@ static void registry_global(void *data, struct wl_registry *reg, uint32_t name,
sc_mgr = wl_registry_bind(reg, name,
&zwlr_screencopy_manager_v1_interface,
version < 3 ? version : 3);
} 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);
}
}
@ -1323,6 +1330,71 @@ static void screencopy_run(void) {
}
}
/* --- Keyboard shortcuts inhibit (zwp_keyboard_shortcuts_inhibit_v1) ---
--inhibit [--inhibit-out <file>]: act as a fullscreen game that grabs the
compositor's shortcuts. Creates a normal window (so it takes keyboard
focus), then inhibits shortcuts on its surface+seat. Writes "active" to
--inhibit-out when the compositor honors it (proving kick activates the
inhibitor for the focused surface), "inactive" if focus moves away. */
static bool inhibit_mode;
static const char *inhibit_out;
static struct zwp_keyboard_shortcuts_inhibitor_v1 *kb_inhibitor;
static void kb_inhibit_note(const char *s) {
if (inhibit_out) write_outfile(inhibit_out, s, strlen(s));
}
static void kb_inhibitor_active(void *data,
struct zwp_keyboard_shortcuts_inhibitor_v1 *inhib) {
(void)data; (void)inhib;
kb_inhibit_note("active");
}
static void kb_inhibitor_inactive(void *data,
struct zwp_keyboard_shortcuts_inhibitor_v1 *inhib) {
(void)data; (void)inhib;
kb_inhibit_note("inactive");
}
static const struct zwp_keyboard_shortcuts_inhibitor_v1_listener kb_inhibitor_listener = {
.active = kb_inhibitor_active,
.inactive = kb_inhibitor_inactive,
};
static void inhibit_run(void) {
display = wl_display_connect(NULL);
if (!display) {
fprintf(stderr, "wltest: inhibit: cannot connect\n");
exit(1);
}
struct wl_registry *reg = wl_display_get_registry(display);
wl_registry_add_listener(reg, &registry_listener, NULL);
wl_display_roundtrip(display);
if (!kb_inhibit_mgr || !seat) {
fprintf(stderr, "wltest: inhibit: manager or seat not advertised\n");
exit(1);
}
/* Need a mapped window to hold keyboard focus: draw a small buffer so
the compositor focuses it, then inhibit on its surface+seat. */
surface = wl_compositor_create_surface(compositor);
xdg_surface = xdg_wm_base_get_xdg_surface(wm_base, surface);
toplevel = xdg_surface_get_toplevel(xdg_surface);
xdg_toplevel_set_title(toplevel, "wltest-inhibit");
xdg_wm_base_add_listener(wm_base, &wm_base_listener, NULL);
xdg_surface_add_listener(xdg_surface, &xdg_surface_listener, NULL);
xdg_toplevel_add_listener(toplevel, &toplevel_listener, NULL);
wl_surface_commit(surface);
wl_display_roundtrip(display);
kb_inhibitor = zwp_keyboard_shortcuts_inhibit_manager_v1_inhibit_shortcuts(
kb_inhibit_mgr, surface, seat);
zwp_keyboard_shortcuts_inhibitor_v1_add_listener(kb_inhibitor,
&kb_inhibitor_listener, NULL);
for (;;) {
if (wl_display_dispatch(display) < 0)
break;
}
}
/* 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
@ -1618,10 +1690,26 @@ int main(int argc, char **argv) {
}
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 {
break;
}
}
/* 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) {