git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commit7d247413e46517bf457d2c7985834dacf14f2574
parent8542f8336a
authorIsaac Freund <[email protected]>
date2026-04-22 10:16
protocol: add xkb_binding_seat_v1.modifiers_{watch,update}

 build.zig                          |  2 +-
 protocol/river-xkb-bindings-v1.xml | 52 +++++++++++++++++++++++++++++++++++---
 river/KeyboardGroup.zig            | 16 ++++++++++++
 river/XkbBindings.zig              |  2 +-
 river/XkbBindingsSeat.zig          | 29 ++++++++++++++++++---
 5 files changed, 93 insertions(+), 8 deletions(-)

diff --git a/build.zig b/build.zig
index 71ca684..97392e1 100644
--- a/build.zig
+++ b/build.zig
@@ -121,7 +121,7 @@ pub fn build(b: *Build) !void {
     scanner.generate("wp_color_representation_manager_v1", 1);
 
     scanner.generate("river_window_manager_v1", 4);
-    scanner.generate("river_xkb_bindings_v1", 2);
+    scanner.generate("river_xkb_bindings_v1", 3);
     scanner.generate("river_layer_shell_v1", 1);
     scanner.generate("river_input_manager_v1", 1);
     scanner.generate("river_libinput_config_v1", 1);
diff --git a/protocol/river-xkb-bindings-v1.xml b/protocol/river-xkb-bindings-v1.xml
index de4f100..5d32967 100644
--- a/protocol/river-xkb-bindings-v1.xml
+++ b/protocol/river-xkb-bindings-v1.xml
@@ -33,7 +33,7 @@
     document are to be interpreted as described in IETF RFC 2119.
   </description>
 
-  <interface name="river_xkb_bindings_v1" version="2">
+  <interface name="river_xkb_bindings_v1" version="3">
     <description summary="xkbcommon bindings global interface">
       This global interface should only be advertised to the client if the
       river_window_manager_v1 global is also advertised.
@@ -76,7 +76,7 @@
     </request>
   </interface>
 
-  <interface name="river_xkb_binding_v1" version="2">
+  <interface name="river_xkb_binding_v1" version="3">
     <description summary="configure a xkb key binding, receive trigger events">
       This object allows the window manager to configure a xkbcommon key binding
       and receive events when the key binding is triggered.
@@ -200,7 +200,7 @@
     </event>
   </interface>
 
-  <interface name="river_xkb_bindings_seat_v1" version="2">
+  <interface name="river_xkb_bindings_seat_v1" version="3">
     <description summary="xkb bindings seat">
       This object manages xkb bindings state associated with a specific seat.
     </description>
@@ -264,5 +264,51 @@
         state has been sent by the server.
       </description>
     </event>
+
+    <request name="modifiers_watch" since="3">
+      <description summary="watch for change in active modifiers">
+        Request that the server send the modifiers_update event whenever a state
+        change occurs for at least one of the modifiers specified by the
+        modifiers argument.
+
+        The window manager should make this request with the modifiers argument
+        set to 0 when it no longer wishes to take action based on a change in
+        modifiers.
+
+        This request modifies window management state and may only be made as
+        part of a manage sequence, see the river_window_manager_v1 description.
+      </description>
+      <arg name="modifiers" type="uint" enum="river_seat_v1.modifiers"/>
+    </request>
+
+    <event name="modifiers_update" since="3">
+      <description summary="active modifiers for the seat changed">
+        The set of currently active modifiers for the seat changed. This event
+        is only sent when there is a change in state for modifiers marked as
+        watched using the modifiers_watch request.
+
+        The old and new arguments convey the set of modifiers active before and
+        after the change. All modifiers are included in the old and new
+        arguments, including modifiers that are not watched.
+
+        Since this event is only sent when there is a change in state for
+        watched modifiers, it follows that at least one watched modifier is
+        active in old but inactive in new or vice-versa.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+
+        The compositor should wait for the manage sequence to complete before
+        processing further input events. This allows the window manager client
+        to, for example, modify key bindings and keyboard focus without racing
+        against future input events. The window manager should of course respond
+        as soon as possible as the capacity of the compositor to buffer incoming
+        input events is finite.
+      </description>
+      <arg name="old" type="uint" enum="river_seat_v1.modifiers"
+        summary="previously active modifiers"/>
+      <arg name="new" type="uint" enum="river_seat_v1.modifiers"
+        summary="currently active modifiers"/>
+    </event>
   </interface>
 </protocol>
diff --git a/river/KeyboardGroup.zig b/river/KeyboardGroup.zig
index 00c7d34..177b5d6 100644
--- a/river/KeyboardGroup.zig
+++ b/river/KeyboardGroup.zig
@@ -63,6 +63,7 @@ config: Keyboard.Config,
 /// This is the keyboard that actually gets passed to wlr_seat functions for
 /// setting keyboard focus.
 state: wlr.Keyboard,
+modifiers_old: wlr.Keyboard.ModifierMask = .{},
 
 /// Maps from pressed libinput keycode (not xkb keycode) to information
 /// about where the press event has been sent.
@@ -360,6 +361,21 @@ pub fn processModifiers(group: *KeyboardGroup, modifiers: wlr.Keyboard.Modifiers
 
 fn handleModifiers(listener: *wl.Listener(*wlr.Keyboard), _: *wlr.Keyboard) void {
     const group: *KeyboardGroup = @fieldParentPtr("modifiers", listener);
+
+    {
+        const old: u32 = @bitCast(group.modifiers_old);
+        const new: u32 = @bitCast(group.state.getModifiers());
+        const watched: u32 = @bitCast(group.seat.xkb_bindings_seat.requested.mods_watched);
+        if (old & watched != new & watched) {
+            group.seat.xkb_bindings_seat.scheduled.mods_update = .{
+                .old = @bitCast(old),
+                .new = @bitCast(new),
+            };
+            server.wm.dirtyWindowing();
+        }
+        group.modifiers_old = @bitCast(new);
+    }
+
     if (group.getInputMethodGrab()) |keyboard_grab| {
         keyboard_grab.setKeyboard(&group.state);
         keyboard_grab.sendModifiers(&group.state.modifiers);
diff --git a/river/XkbBindings.zig b/river/XkbBindings.zig
index f6f18ef..05d2527 100644
--- a/river/XkbBindings.zig
+++ b/river/XkbBindings.zig
@@ -22,7 +22,7 @@ server_destroy: wl.Listener(*wl.Server) = .init(handleServerDestroy),
 
 pub fn init(bindings: *XkbBindings) !void {
     bindings.* = .{
-        .global = try wl.Global.create(server.wl_server, river.XkbBindingsV1, 2, ?*anyopaque, null, bind),
+        .global = try wl.Global.create(server.wl_server, river.XkbBindingsV1, 3, ?*anyopaque, null, bind),
     };
     server.wl_server.addDestroyListener(&bindings.server_destroy);
 }
diff --git a/river/XkbBindingsSeat.zig b/river/XkbBindingsSeat.zig
index 932d70d..14fef8d 100644
--- a/river/XkbBindingsSeat.zig
+++ b/river/XkbBindingsSeat.zig
@@ -21,14 +21,24 @@ object: ?*river.XkbBindingsSeatV1 = null,
 
 scheduled: struct {
     ate_unbound_key: bool = false,
+    mods_update: ?struct {
+        old: river.SeatV1.Modifiers,
+        new: river.SeatV1.Modifiers,
+    } = null,
 } = .{},
 requested: struct {
     next_key_change: enum {
         none,
         ensure_eaten,
         cancel_ensure_eaten,
-    } = .none,
-} = .{},
+    },
+    mods_watched: river.SeatV1.Modifiers,
+
+    const init: @This() = .{
+        .next_key_change = .none,
+        .mods_watched = .{},
+    };
+} = .init,
 
 ensure_next_key_eaten: bool = false,
 
@@ -49,7 +59,7 @@ pub fn createObject(
 pub fn makeInert(bindings_seat: *XkbBindingsSeat) void {
     if (bindings_seat.object) |object| {
         object.setHandler(?*anyopaque, handleRequestInert, null, null);
-        bindings_seat.object = null;
+        handleDestroy(object, bindings_seat);
     }
 }
 
@@ -63,6 +73,7 @@ fn handleRequestInert(
 
 fn handleDestroy(_: *river.XkbBindingsSeatV1, bindings_seat: *XkbBindingsSeat) void {
     bindings_seat.object = null;
+    bindings_seat.requested = .init;
 }
 
 fn handleRequest(
@@ -81,6 +92,10 @@ fn handleRequest(
             if (!server.wm.ensureWindowing()) return;
             bindings_seat.requested.next_key_change = .cancel_ensure_eaten;
         },
+        .modifiers_watch => |args| {
+            if (!server.wm.ensureWindowing()) return;
+            bindings_seat.requested.mods_watched = args.modifiers;
+        },
     }
 }
 
@@ -93,6 +108,14 @@ pub fn manageStart(bindings_seat: *XkbBindingsSeat) void {
         }
         bindings_seat.scheduled.ate_unbound_key = false;
     }
+    if (bindings_seat.scheduled.mods_update) |mods| {
+        if (bindings_seat.object) |object| {
+            if (object.getVersion() >= 3) {
+                object.sendModifiersUpdate(mods.old, mods.new);
+            }
+        }
+        bindings_seat.scheduled.mods_update = null;
+    }
 }
 
 pub fn manageFinish(bindings_seat: *XkbBindingsSeat) void {