wayland: Phase 78 — keyboard event dispatch and WM shortcut handler

Add wWaylandKeyPress(keycode, modifiers) to event.c/h: scans
wKeyBindings and dispatches a subset of WM shortcuts (miniaturize,
hide, maximize, raise/lower, close, focus-next/prev, workspace switch)
using raw XKB keycode + modifier values instead of an XEvent.

In wl_backend.c, wire handle_keyboard_key and handle_keyboard_modifiers
listeners on the headless keyboard device so that:
- key events are checked against WM shortcuts first
- non-shortcut keys are forwarded to the focused client via
  wlr_seat_keyboard_notify_key
- modifier changes are always notified via wlr_seat_keyboard_notify_modifiers

The headless backend doesn't generate real key events so the listener
is a no-op in the smoke test, but the infrastructure is ready for a
real DRM+libinput backend.

Smoke test: check for "keyboard key+modifiers listeners wired" log.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Levi Neely 2026-03-28 20:36:32 +01:00
parent b1e4a99c5d
commit b576025f9b
4 changed files with 211 additions and 0 deletions

View File

@ -95,6 +95,7 @@
#include "../../screen.h"
#include "../../window.h"
#include "../../xmodifier.h" /* Phase 77: wSetModifierMasks */
#include "../../event.h" /* Phase 78: wWaylandKeyPress */
#include "../backend.h"
#include "wl_backend.h"
@ -162,6 +163,8 @@ struct wl_compositor_state {
unsigned long next_synthetic_id; /* >= 0x10000000 */
/* Phase 43: headless keyboard device for seat keyboard focus */
struct wlr_input_device *keyboard_dev;
struct wl_listener keyboard_key_listener; /* Phase 78 */
struct wl_listener keyboard_modifiers_listener; /* Phase 78 */
/* Phase 53: headless pointer device so seat advertises pointer capability */
struct wlr_input_device *pointer_dev;
/* Phase 54: virtual pointer position (screen coordinates) */
@ -401,6 +404,55 @@ handle_xdg_new_surface(struct wl_listener *listener, void *data)
wlr_xdg_surface_schedule_configure(xdg_surface);
}
/* ------------------------------------------------------------------ */
/* Phase 78: keyboard event handlers */
/* ------------------------------------------------------------------ */
/*
* handle_keyboard_key — fires for every key press/release on the headless
* keyboard. On key-press, dispatches WM shortcuts via wWaylandKeyPress();
* then notifies the focused Wayland client via wlr_seat_keyboard_notify_key.
* (The headless backend never actually fires these in practice, but the
* listener is wired so it works immediately when a real backend is used.)
*/
static void
handle_keyboard_key(struct wl_listener *listener, void *data)
{
struct wlr_event_keyboard_key *event = data;
(void)listener;
if (!wl_state.seat || !wl_state.keyboard_dev || !wl_state.keyboard_dev->keyboard)
return;
struct wlr_keyboard *kb = wl_state.keyboard_dev->keyboard;
/* Update modifier state */
wlr_seat_keyboard_notify_modifiers(wl_state.seat, &kb->modifiers);
if (event->state == WL_KEYBOARD_KEY_STATE_PRESSED) {
uint32_t keycode = event->keycode + 8; /* evdev → XKB keycode */
uint32_t modifiers = wlr_keyboard_get_modifiers(kb);
/* Try WM shortcut dispatch first */
if (wWaylandKeyPress((unsigned int)keycode, (unsigned int)modifiers))
return; /* consumed — do not forward to client */
}
/* Forward non-shortcut key events to focused client */
wlr_seat_keyboard_notify_key(wl_state.seat,
event->time_msec, event->keycode, event->state);
}
static void
handle_keyboard_modifiers(struct wl_listener *listener, void *data)
{
(void)listener; (void)data;
if (!wl_state.seat || !wl_state.keyboard_dev || !wl_state.keyboard_dev->keyboard)
return;
struct wlr_keyboard *kb = wl_state.keyboard_dev->keyboard;
wlr_seat_keyboard_notify_modifiers(wl_state.seat, &kb->modifiers);
}
/* ------------------------------------------------------------------ */
/* Display lifecycle (Phase 32 / Phase 34) */
/* ------------------------------------------------------------------ */
@ -523,6 +575,18 @@ wl_display_open(const char *name)
}
}
wlr_seat_set_keyboard(wl_state.seat, wl_state.keyboard_dev);
/* Phase 78: wire keyboard key + modifier listeners so WM shortcuts
* are dispatched and non-shortcut keys reach the focused client. */
if (wl_state.keyboard_dev->keyboard) {
wl_state.keyboard_key_listener.notify = handle_keyboard_key;
wl_signal_add(&wl_state.keyboard_dev->keyboard->events.key,
&wl_state.keyboard_key_listener);
wl_state.keyboard_modifiers_listener.notify = handle_keyboard_modifiers;
wl_signal_add(&wl_state.keyboard_dev->keyboard->events.modifiers,
&wl_state.keyboard_modifiers_listener);
wwarning("wl_backend: keyboard key+modifiers listeners wired");
}
}
/* Phase 53: create a headless pointer so the seat advertises pointer

View File

@ -1299,6 +1299,133 @@ static int CheckFullScreenWindowFocused(WScreen * scr)
return 0;
}
/*
* Phase 78: Wayland keyboard event dispatch.
* Called from wl_backend's keyboard key listener with the XKB keycode and
* modifier mask. Scans wKeyBindings and dispatches the matching WM action.
* Returns 1 if consumed (WM shortcut), 0 if the event should be forwarded.
*/
int wWaylandKeyPress(unsigned int keycode, unsigned int modifiers)
{
WScreen *scr = wScreenWithNumber(0);
if (!scr)
return 0;
WWindow *wwin = scr->focused_window;
int modifiers_filtered = (int)(modifiers & (unsigned int)w_global.shortcut.modifiers_mask);
int command = -1;
short i;
for (i = 0; i < WKBD_LAST; i++) {
if (wKeyBindings[i].keycode == 0)
continue;
if ((unsigned int)wKeyBindings[i].keycode == keycode &&
wKeyBindings[i].modifier == modifiers_filtered) {
command = i;
break;
}
}
if (command < 0)
return 0; /* not a WM shortcut */
/* Reuse the ISMAPPED/ISFOCUSED macros defined inside handleKeyPress below. */
#ifndef ISMAPPED
#define ISMAPPED(w) ((w) && !(w)->flags.miniaturized && ((w)->flags.mapped || (w)->flags.shaded))
#define ISFOCUSED(w) ((w) && (w)->flags.focused)
#endif
/* Dispatch WM shortcuts — identical to handleKeyPress switch but without
* XEvent arguments. Only workspace/window commands that don't need an
* XEvent are included; pointer-position shortcuts use the virtual pointer. */
switch (command) {
case WKBD_MINIATURIZE:
if (ISMAPPED(wwin) && ISFOCUSED(wwin)) {
CloseWindowMenu(scr);
wIconifyWindow(wwin);
}
break;
case WKBD_HIDE:
if (ISMAPPED(wwin) && ISFOCUSED(wwin)) {
WApplication *wapp = wApplicationOf(wwin->main_window);
CloseWindowMenu(scr);
if (wapp && !WFLAGP(wapp->main_window_desc, no_appicon))
wHideApplication(wapp);
}
break;
case WKBD_MAXIMIZE:
if (ISMAPPED(wwin) && ISFOCUSED(wwin)) {
CloseWindowMenu(scr);
wMaximizeWindow(wwin, MAX_VERTICAL | MAX_HORIZONTAL,
wGetHeadForWindow(wwin));
}
break;
case WKBD_VMAXIMIZE:
if (ISMAPPED(wwin) && ISFOCUSED(wwin)) {
CloseWindowMenu(scr);
wMaximizeWindow(wwin, MAX_VERTICAL,
wGetHeadForWindow(wwin));
}
break;
case WKBD_HMAXIMIZE:
if (ISMAPPED(wwin) && ISFOCUSED(wwin)) {
CloseWindowMenu(scr);
wMaximizeWindow(wwin, MAX_HORIZONTAL,
wGetHeadForWindow(wwin));
}
break;
case WKBD_RAISE:
if (ISMAPPED(wwin) && ISFOCUSED(wwin) && wwin->frame) {
CloseWindowMenu(scr);
wRaiseFrame(wwin->frame->core);
}
break;
case WKBD_LOWER:
if (ISMAPPED(wwin) && ISFOCUSED(wwin) && wwin->frame) {
CloseWindowMenu(scr);
wLowerFrame(wwin->frame->core);
}
break;
case WKBD_CLOSE:
if (ISMAPPED(wwin) && ISFOCUSED(wwin)) {
CloseWindowMenu(scr);
if (wwin->protocols.DELETE_WINDOW && !WFLAGP(wwin, kill_close))
wClientSendProtocol(wwin, w_global.atom.wm.delete_window,
w_global.timestamp.last_event);
}
break;
case WKBD_FOCUSNEXT:
wWindowFocusNext(wwin, True);
break;
case WKBD_FOCUSPREV:
wWindowFocusPrev(wwin, True);
break;
case WKBD_WORKSPACE1: case WKBD_WORKSPACE2: case WKBD_WORKSPACE3:
case WKBD_WORKSPACE4: case WKBD_WORKSPACE5: case WKBD_WORKSPACE6:
case WKBD_WORKSPACE7: case WKBD_WORKSPACE8: case WKBD_WORKSPACE9:
case WKBD_WORKSPACE10:
{
int widx2 = command - WKBD_WORKSPACE1;
int ws = (scr->current_workspace / 10) * 10 + widx2;
if (wPreferences.ws_advance || ws < scr->workspace_count)
wWorkspaceChange(scr, ws);
}
break;
case WKBD_NEXTWORKSPACE:
wWorkspaceRelativeChange(scr, 1);
break;
case WKBD_PREVWORKSPACE:
wWorkspaceRelativeChange(scr, -1);
break;
default:
/* Complex shortcuts that need XEvent (pointer coords, menus) are
* not dispatched here; return 0 so the caller forwards the key. */
return 0;
}
return 1; /* consumed */
}
static void handleKeyPress(XEvent * event)
{
WScreen *scr = wScreenForRootWindow(event->xkey.root);

View File

@ -40,4 +40,12 @@ Bool IsDoubleClick(WScreen *scr, WBackendEvent *event);
/* called from the signal handler */
void NotifyDeadProcess(pid_t pid, unsigned char status);
/* Phase 78: Wayland keyboard event dispatch — called from wl_backend when a
* key-press event arrives. keycode is the XKB keycode (evdev+8, compatible
* with wKeyBindings); modifiers is the XKB modifier mask (compatible with the
* X11 ShiftMask/ControlMask/Mod1Mask bit layout). Returns 1 if the event was
* consumed as a WM shortcut, 0 if it should be forwarded to the focused client.
*/
int wWaylandKeyPress(unsigned int keycode, unsigned int modifiers);
#endif /* WMEVENT_H */

View File

@ -97,6 +97,13 @@
# XKeysymToKeycode(dpy,...) replaced by vtable in defaults.c/rootmenu.c/
# usermenu.c so keyboard shortcuts are correctly configured in Wayland mode.
# Verified by "XKB keymap set on headless keyboard" log.
# 36. (Phase 78) wWaylandKeyPress() added to event.c: scans wKeyBindings
# and dispatches WM shortcuts (miniaturize, hide, maximize, raise/lower,
# close, focus, workspace switch) without needing an XEvent.
# Keyboard key+modifiers listeners wired in wl_backend so key events
# from the headless keyboard are dispatched through the WM shortcut
# handler before being forwarded to focused clients.
# Verified by "keyboard key+modifiers listeners wired" log.
#
# This test grows as wl_backend vtable slots are implemented.
#
@ -217,6 +224,11 @@ grep -q 'XKB keymap set on headless keyboard' /tmp/wl-smoke-stderr \
|| die "compositor did not log XKB keymap setup (wl_keysym_to_keycode will return 0)"
echo " xkb check: XKB keymap set on headless keyboard"
# -- 4e. Phase 78: keyboard listeners must be wired ----------------------
grep -q 'keyboard key+modifiers listeners wired' /tmp/wl-smoke-stderr \
|| die "compositor did not log keyboard listeners wired"
echo " kbd listener check: keyboard key+modifiers listeners wired"
# -- 5. Core Wayland globals must be advertised ---------------------------
#
# Phase 35: wl_compositor, wl_seat, and xdg_wm_base must appear in the