Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
rwm: extract xkb bindings to an extension protocol
This makes the core river-window-management-v1 protocol independent of
possible future (breaking) changes to xkbcommon, or possible replacement
with some other keyboard library in the hypothetical far future.
build.zig | 2 +
protocol/river-window-management-v1.xml | 122 -----------------------
protocol/river-xkb-bindings-v1.xml | 171 ++++++++++++++++++++++++++++++++
river/Seat.zig | 14 ---
river/Server.zig | 4 +
river/XkbBindings.zig | 83 ++++++++++++++++
rivercompat/WindowManager.zig | 3 +
rivercompat/XkbBinding.zig | 4 +-
rivercompat/main.zig | 7 +-
9 files changed, 272 insertions(+), 138 deletions(-)
diff --git a/build.zig b/build.zig
index a32d3ca..ad37ef6 100644
--- a/build.zig
+++ b/build.zig
@@ -78,6 +78,7 @@ pub fn build(b: *Build) !void {
scanner.addSystemProtocol("unstable/xdg-decoration/xdg-decoration-unstable-v1.xml");
scanner.addCustomProtocol(b.path("protocol/river-window-management-v1.xml"));
+ scanner.addCustomProtocol(b.path("protocol/river-xkb-bindings-v1.xml"));
scanner.addCustomProtocol(b.path("protocol/wlr-output-power-management-unstable-v1.xml"));
// Some of these versions may be out of date with what wlroots implements.
@@ -103,6 +104,7 @@ pub fn build(b: *Build) !void {
scanner.generate("wp_single_pixel_buffer_manager_v1", 1);
scanner.generate("river_window_manager_v1", 1);
+ scanner.generate("river_xkb_bindings_v1", 1);
scanner.generate("zwlr_output_power_manager_v1", 1);
diff --git a/protocol/river-window-management-v1.xml b/protocol/river-window-management-v1.xml
index 112c3b3..1942c9f 100644
--- a/protocol/river-window-management-v1.xml
+++ b/protocol/river-window-management-v1.xml
@@ -1531,19 +1531,6 @@
<entry name="mod5" value="128"/>
</enum>
- <request name="get_xkb_binding">
- <description summary="define a new xkbcommon key binding">
- Define a key binding in terms of an xkbcommon keysym and other
- configurable properties.
-
- The new key binding is not enabled until initial configuration is
- completed and the enable request is made during a manage sequence.
- </description>
- <arg name="id" type="new_id" interface="river_xkb_binding_v1"/>
- <arg name="keysym" type="uint" summary="an xkbcommon keysym"/>
- <arg name="modifiers" type="uint" enum="modifiers"/>
- </request>
-
<request name="get_pointer_binding">
<description summary="define a new pointer binding">
Define a pointer binding in terms of a pointer button, modifiers, and
@@ -1561,115 +1548,6 @@
</request>
</interface>
- <interface name="river_xkb_binding_v1" version="1">
- <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.
-
- The new key binding is not enabled until the enable request is made during
- a manage sequence.
-
- Normally, all key events are sent to the surface with keyboard focus by
- the compositor. Key events that trigger a key binding are not sent to the
- surface with keyboard focus.
-
- If multiple key bindings would be triggered by a single physical key event
- on the compositor side, it is compositor policy which key binding(s) will
- receive press/release events or if all of the matched key bindings receive
- press/release events.
-
- Key bindings might be matched by the same physical key event due to shared
- keysym and modifiers. The layout override feature may also cause the same
- physical key event to trigger two key bindings with different keysyms and
- different layout overrides configured.
- </description>
-
- <request name="destroy" type="destructor">
- <description summary="destroy the xkb binding object">
- This request indicates that the client will no longer use the xkb key
- binding object and that it may be safely destroyed.
- </description>
- </request>
-
- <request name="set_layout_override">
- <description summary="override currently active xkb layout">
- Specify an xkb layout that should be used to translate key events for
- the purpose of triggering this key binding irrespective of the currently
- active xkb layout.
-
- The layout argument is a 0-indexed xkbcommon layout number for the
- keyboard that generated the key event.
-
- If this request is never made, the currently active xkb layout of the
- keyboard that generated the key event will be used.
-
- 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="layout" type="uint" summary="0-indexed xkbcommon layout"/>
- </request>
-
- <request name="enable">
- <description summary="enable the key binding">
- This request should be made after all initial configuration has been
- completed and the window manager wishes the key binding to be able to be
- triggered.
-
- 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>
- </request>
-
- <request name="disable">
- <description summary="disable the key binding">
- This request may be used to temporarily disable the key binding. It may
- be later re-enabled with the enable request.
-
- 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>
- </request>
-
- <event name="pressed">
- <description summary="the key triggering the binding has been pressed">
- This event indicates that the physical key triggering the binding has
- been pressed.
-
- 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>
- </event>
-
- <event name="released">
- <description summary="the key triggering the binding has been released">
- This event indicates that the physical key triggering the binding has
- been released.
-
- Releasing the modifiers for the binding without releasing the "main"
- physical key that produces the bound keysym does not trigger the release
- event. This event is sent when the "main" key is released, even if the
- modifiers have changed since the pressed event.
-
- 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>
- </event>
- </interface>
-
<interface name="river_pointer_binding_v1" version="1">
<description summary="configure a pointer binding, receive trigger events">
This object allows the window manager to configure a pointer binding and
diff --git a/protocol/river-xkb-bindings-v1.xml b/protocol/river-xkb-bindings-v1.xml
new file mode 100644
index 0000000..f04f106
--- /dev/null
+++ b/protocol/river-xkb-bindings-v1.xml
@@ -0,0 +1,171 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="river_xkb_bindings_v1">
+ <copyright>
+ Copyright 2025 The River Developers
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+ OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+ CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ </copyright>
+
+ <description summary="frame-perfect window management">
+ This protocol allows a single "window manager" client to determine the
+ window management policy of the compositor. State is globally
+ double-buffered allowing for frame perfect state changes involving multiple
+ windows.
+
+ The key words "must", "must not", "required", "shall", "shall not",
+ "should", "should not", "recommended", "may", and "optional" in this
+ document are to be interpreted as described in IETF RFC 2119.
+
+ Warning! The protocol described in this file is currently in the testing
+ phase. Backward compatible changes may be added together with the
+ corresponding interface version bump. Backward incompatible changes can only
+ be done by creating a new major version of the extension.
+ </description>
+
+ <interface name="river_xkb_bindings_v1" version="1">
+ <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.
+ </description>
+
+ <request name="destroy" type="destructor">
+ <description summary="destroy the river_xkb_bindings_v1 object">
+ This request indicates that the client will no longer use the
+ river_xkb_bindings_v1 object.
+ </description>
+ </request>
+
+ <request name="get_xkb_binding">
+ <description summary="define a new xkbcommon key binding">
+ Define a key binding for the given seat in terms of an xkbcommon keysym
+ and other configurable properties.
+
+ The new key binding is not enabled until initial configuration is
+ completed and the enable request is made during a manage sequence.
+ </description>
+ <arg name="seat" type="object" interface="river_seat_v1"/>
+ <arg name="id" type="new_id" interface="river_xkb_binding_v1"/>
+ <arg name="keysym" type="uint" summary="an xkbcommon keysym"/>
+ <arg name="modifiers" type="uint" enum="river_seat_v1.modifiers"/>
+ </request>
+ </interface>
+
+ <interface name="river_xkb_binding_v1" version="1">
+ <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.
+
+ The new key binding is not enabled until the enable request is made during
+ a manage sequence.
+
+ Normally, all key events are sent to the surface with keyboard focus by
+ the compositor. Key events that trigger a key binding are not sent to the
+ surface with keyboard focus.
+
+ If multiple key bindings would be triggered by a single physical key event
+ on the compositor side, it is compositor policy which key binding(s) will
+ receive press/release events or if all of the matched key bindings receive
+ press/release events.
+
+ Key bindings might be matched by the same physical key event due to shared
+ keysym and modifiers. The layout override feature may also cause the same
+ physical key event to trigger two key bindings with different keysyms and
+ different layout overrides configured.
+ </description>
+
+ <request name="destroy" type="destructor">
+ <description summary="destroy the xkb binding object">
+ This request indicates that the client will no longer use the xkb key
+ binding object and that it may be safely destroyed.
+ </description>
+ </request>
+
+ <request name="set_layout_override">
+ <description summary="override currently active xkb layout">
+ Specify an xkb layout that should be used to translate key events for
+ the purpose of triggering this key binding irrespective of the currently
+ active xkb layout.
+
+ The layout argument is a 0-indexed xkbcommon layout number for the
+ keyboard that generated the key event.
+
+ If this request is never made, the currently active xkb layout of the
+ keyboard that generated the key event will be used.
+
+ 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="layout" type="uint" summary="0-indexed xkbcommon layout"/>
+ </request>
+
+ <request name="enable">
+ <description summary="enable the key binding">
+ This request should be made after all initial configuration has been
+ completed and the window manager wishes the key binding to be able to be
+ triggered.
+
+ 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>
+ </request>
+
+ <request name="disable">
+ <description summary="disable the key binding">
+ This request may be used to temporarily disable the key binding. It may
+ be later re-enabled with the enable request.
+
+ 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>
+ </request>
+
+ <event name="pressed">
+ <description summary="the key triggering the binding has been pressed">
+ This event indicates that the physical key triggering the binding has
+ been pressed.
+
+ 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>
+ </event>
+
+ <event name="released">
+ <description summary="the key triggering the binding has been released">
+ This event indicates that the physical key triggering the binding has
+ been released.
+
+ Releasing the modifiers for the binding without releasing the "main"
+ physical key that produces the bound keysym does not trigger the release
+ event. This event is sent when the "main" key is released, even if the
+ modifiers have changed since the pressed event.
+
+ 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>
+ </event>
+ </interface>
+</protocol>
diff --git a/river/Seat.zig b/river/Seat.zig
index fcb4853..8ad11a3 100644
--- a/river/Seat.zig
+++ b/river/Seat.zig
@@ -463,20 +463,6 @@ fn handleRequest(
.pointer_confine_to_region => {},
.pointer_warp => {},
- .get_xkb_binding => |args| {
- XkbBinding.create(
- seat,
- seat_v1.getClient(),
- seat_v1.getVersion(),
- args.id,
- @enumFromInt(args.keysym),
- args.modifiers,
- ) catch {
- seat_v1.getClient().postNoMemory();
- log.err("out of memory", .{});
- return;
- };
- },
.get_pointer_binding => |args| {
PointerBinding.create(
seat,
diff --git a/river/Server.zig b/river/Server.zig
index 74260a9..4835695 100644
--- a/river/Server.zig
+++ b/river/Server.zig
@@ -38,6 +38,7 @@ const SceneNodeData = @import("SceneNodeData.zig");
const Seat = @import("Seat.zig");
const TabletTool = @import("TabletTool.zig");
const WindowManager = @import("WindowManager.zig");
+const XkbBindings = @import("XkbBindings.zig");
const XdgDecoration = @import("XdgDecoration.zig");
const XdgToplevel = @import("XdgToplevel.zig");
const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig");
@@ -89,6 +90,7 @@ config: Config,
idle_inhibit_manager: IdleInhibitManager,
lock_manager: LockManager,
wm: WindowManager,
+xkb_bindings: XkbBindings,
xwayland: if (build_options.xwayland) ?*wlr.Xwayland else void = if (build_options.xwayland) null,
new_xsurface: if (build_options.xwayland) wl.Listener(*wlr.XwaylandSurface) else void =
@@ -155,6 +157,7 @@ pub fn init(server: *Server, runtime_xwayland: bool) !void {
.idle_inhibit_manager = undefined,
.lock_manager = undefined,
.wm = undefined,
+ .xkb_bindings = undefined,
};
if (renderer.getTextureFormats(@intFromEnum(wlr.BufferCap.dmabuf)) != null) {
@@ -173,6 +176,7 @@ pub fn init(server: *Server, runtime_xwayland: bool) !void {
}
try server.wm.init();
+ try server.xkb_bindings.init();
try server.scene.init();
try server.om.init();
try server.input_manager.init();
diff --git a/river/XkbBindings.zig b/river/XkbBindings.zig
new file mode 100644
index 0000000..b6f1398
--- /dev/null
+++ b/river/XkbBindings.zig
@@ -0,0 +1,83 @@
+// This file is part of river, a dynamic tiling wayland compositor.
+//
+// Copyright 2025 The River Developers
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+const XkbBindings = @This();
+
+const std = @import("std");
+const assert = std.debug.assert;
+const wl = @import("wayland").server.wl;
+const river = @import("wayland").server.river;
+
+const server = &@import("main.zig").server;
+const util = @import("util.zig");
+
+const Seat = @import("Seat.zig");
+const XkbBinding = @import("XkbBinding.zig");
+
+const log = std.log.scoped(.wm);
+
+global: *wl.Global,
+
+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, 1, ?*anyopaque, null, bind),
+ };
+ server.wl_server.addDestroyListener(&bindings.server_destroy);
+}
+
+fn handleServerDestroy(listener: *wl.Listener(*wl.Server), _: *wl.Server) void {
+ const bindings: *XkbBindings = @fieldParentPtr("server_destroy", listener);
+
+ bindings.global.destroy();
+}
+
+fn bind(client: *wl.Client, _: ?*anyopaque, version: u32, id: u32) void {
+ const object = river.XkbBindingsV1.create(client, version, id) catch {
+ client.postNoMemory();
+ log.err("out of memory", .{});
+ return;
+ };
+
+ object.setHandler(?*anyopaque, handleRequest, null, null);
+}
+
+fn handleRequest(
+ object: *river.XkbBindingsV1,
+ request: river.XkbBindingsV1.Request,
+ _: ?*anyopaque,
+) void {
+ switch (request) {
+ .destroy => object.destroy(),
+ .get_xkb_binding => |args| {
+ const seat_data = args.seat.getUserData() orelse return;
+ const seat: *Seat = @ptrCast(@alignCast(seat_data));
+ XkbBinding.create(
+ seat,
+ object.getClient(),
+ object.getVersion(),
+ args.id,
+ @enumFromInt(args.keysym),
+ args.modifiers,
+ ) catch {
+ object.getClient().postNoMemory();
+ log.err("out of memory", .{});
+ return;
+ };
+ },
+ }
+}
diff --git a/rivercompat/WindowManager.zig b/rivercompat/WindowManager.zig
index 7a63313..5394703 100644
--- a/rivercompat/WindowManager.zig
+++ b/rivercompat/WindowManager.zig
@@ -31,6 +31,7 @@ const Seat = @import("Seat.zig");
const Window = @import("Window.zig");
wm_v1: *river.WindowManagerV1,
+xkb_bindings: *river.XkbBindingsV1,
compositor: *wl.Compositor,
viewporter: *wp.Viewporter,
single_pixel: *wp.SinglePixelBufferManagerV1,
@@ -50,12 +51,14 @@ fallback_stack_focus: wl.list.Head(Window, .link_focus),
pub fn init(
wm: *WindowManager,
wm_v1: *river.WindowManagerV1,
+ xkb_bindings: *river.XkbBindingsV1,
compositor: *wl.Compositor,
viewporter: *wp.Viewporter,
single_pixel: *wp.SinglePixelBufferManagerV1,
) void {
wm.* = .{
.wm_v1 = wm_v1,
+ .xkb_bindings = xkb_bindings,
.compositor = compositor,
.viewporter = viewporter,
.single_pixel = single_pixel,
diff --git a/rivercompat/XkbBinding.zig b/rivercompat/XkbBinding.zig
index 0b19e83..14663c3 100644
--- a/rivercompat/XkbBinding.zig
+++ b/rivercompat/XkbBinding.zig
@@ -22,6 +22,8 @@ const wayland = @import("wayland");
const wl = wayland.client.wl;
const river = wayland.client.river;
+const wm = &@import("root").wm;
+
const Seat = @import("Seat.zig");
const Window = @import("Window.zig");
@@ -37,7 +39,7 @@ pub fn create(
modifiers: river.SeatV1.Modifiers,
action: Seat.Action,
) void {
- const xkb_binding_v1 = seat.seat_v1.getXkbBinding(keysym, modifiers) catch @panic("OOM");
+ const xkb_binding_v1 = wm.xkb_bindings.getXkbBinding(seat.seat_v1, keysym, modifiers) catch @panic("OOM");
const binding = gpa.create(XkbBinding) catch @panic("OOM");
binding.* = .{
.seat = seat,
diff --git a/rivercompat/main.zig b/rivercompat/main.zig
index 1f72d35..8f4d81b 100644
--- a/rivercompat/main.zig
+++ b/rivercompat/main.zig
@@ -41,6 +41,7 @@ const usage =
const Globals = struct {
wm_v1: ?*river.WindowManagerV1 = null,
+ xkb_bindings: ?*river.XkbBindingsV1 = null,
compositor: ?*wl.Compositor = null,
viewporter: ?*wp.Viewporter = null,
single_pixel: ?*wp.SinglePixelBufferManagerV1 = null,
@@ -50,6 +51,8 @@ const Globals = struct {
.global => |global| {
if (mem.orderZ(u8, global.interface, river.WindowManagerV1.interface.name) == .eq) {
globals.wm_v1 = registry.bind(global.name, river.WindowManagerV1, 1) catch return;
+ } else if (mem.orderZ(u8, global.interface, river.XkbBindingsV1.interface.name) == .eq) {
+ globals.xkb_bindings = registry.bind(global.name, river.XkbBindingsV1, 1) catch return;
} else if (mem.orderZ(u8, global.interface, wl.Compositor.interface.name) == .eq) {
globals.compositor = registry.bind(global.name, wl.Compositor, 4) catch return;
} else if (mem.orderZ(u8, global.interface, wp.Viewporter.interface.name) == .eq) {
@@ -112,6 +115,8 @@ pub fn main() !void {
const wm_v1 = globals.wm_v1 orelse
fatal("wayland compositor does not support river-window-management-v1", .{});
+ const xkb_bindings = globals.xkb_bindings orelse
+ fatal("wayland compositor does not support river-xkb_bindings-v1", .{});
const compositor = globals.compositor orelse
fatal("wayland compositor does not support wl_compositor", .{});
const viewporter = globals.viewporter orelse
@@ -119,7 +124,7 @@ pub fn main() !void {
const single_pixel = globals.single_pixel orelse
fatal("wayland compositor does not support wp-single-pixel-buffer-v1", .{});
- wm.init(wm_v1, compositor, viewporter, single_pixel);
+ wm.init(wm_v1, xkb_bindings, compositor, viewporter, single_pixel);
while (true) {
if (display.dispatch() != .SUCCESS) fatal("failed to dispatch wayland events", .{});