Implement river-input-management-v1 and river-libinput-config-v1

Right now, the support is still incomplete (no way to set config) but
we get the devices and set them up and handle current/support events
for the river_libinput_device_v1 devices.
This commit is contained in:
Ben Buhse 2026-02-09 12:55:47 -06:00
commit 72c1f33c28
No known key found for this signature in database
GPG key ID: 7916ACFCD38FD0B4
11 changed files with 1523 additions and 22 deletions

View file

@ -192,7 +192,7 @@ fn load(config: *Config, reader: *Io.Reader) !void {
continue;
}
const path_str = utils.stripQuotes(node.arg(&parser, 0) orelse unreachable);
const path_str = utils.stripQuotes(node.arg(&parser, 0).?);
config.wallpaper_image_path = expandTilde(path_str) catch {
logWarnInvalidNodeArg(name, path_str);
continue;

View file

@ -1,8 +1,6 @@
// SPDX-FileCopyrightText: 2026 Ben Buhse <me@benbuhse.email>
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
//
/// Context to pass Wayland info around.
const Context = @This();
@ -19,6 +17,7 @@ wl_outputs: *std.AutoHashMapUnmanaged(u32, *wl.Output),
zwlr_layer_shell_v1: *zwlr.LayerShellV1,
// Wayland globals that we have special structs for
im: *InputManager,
wm: *WindowManager,
xkb_bindings: *XkbBindings,
@ -48,6 +47,8 @@ pub const Options = struct {
wl_shm: *wl.Shm,
wl_outputs: *std.AutoHashMapUnmanaged(u32, *wl.Output),
river_input_manager_v1: *river.InputManagerV1,
river_libinput_config_v1: *river.LibinputConfigV1,
river_layer_shell_v1: *river.LayerShellV1, // TODO
river_window_manager_v1: *river.WindowManagerV1,
river_xkb_bindings_v1: *river.XkbBindingsV1,
@ -69,6 +70,7 @@ pub fn create(options: Options) !*Context {
.wl_outputs = options.wl_outputs,
.zwlr_layer_shell_v1 = options.zwlr_layer_shell_v1,
.wallpaper_image = loadWallpaperImage(options.config),
.im = try InputManager.create(context, options.river_input_manager_v1, options.river_libinput_config_v1),
.wm = try WindowManager.create(context, options.river_window_manager_v1),
.xkb_bindings = try XkbBindings.create(context, options.river_xkb_bindings_v1),
.config = options.config,
@ -156,8 +158,9 @@ const wl = wayland.client.wl;
const zwlr = wayland.client.zwlr;
const utils = @import("utils.zig");
const Config = @import("Config.zig");
const BufferPool = @import("BufferPool.zig");
const Config = @import("Config.zig");
const InputManager = @import("InputManager.zig");
const WallpaperImage = @import("WallpaperImage.zig");
const WindowManager = @import("WindowManager.zig");
const XkbBindings = @import("XkbBindings.zig");

76
src/InputDevice.zig Normal file
View file

@ -0,0 +1,76 @@
// SPDX-FileCopyrightText: 2026 Ben Buhse <me@benbuhse.email>
//
// SPDX-License-Identifier: GPL-3.0-or-later
const InputDevice = @This();
river_input_device_v1: *river.InputDeviceV1,
/// The LibinputDevice that corresponds to this same InputDevice.
/// This comes later (and from a separate river protocol) and
/// not all InputDevices are necessarily LibinputDevices, too,
/// so it's optional.
libinput_device: ?*LibinputDevice = null,
type: ?Type = null,
name: ?[]const u8 = null,
link: wl.list.Link,
pub fn create(river_input_device_v1: *river.InputDeviceV1) !*InputDevice {
const input_device = try utils.allocator.create(InputDevice);
errdefer input_device.destroy();
input_device.* = .{
.river_input_device_v1 = river_input_device_v1,
.link = undefined, // handled by the wl.List
};
input_device.river_input_device_v1.setListener(
*InputDevice,
riverInputDeviceV1Listener,
input_device,
);
return input_device;
}
pub fn destroy(input_device: *InputDevice) void {
input_device.link.remove();
utils.allocator.destroy(input_device);
}
fn riverInputDeviceV1Listener(river_input_device_v1: *river.InputDeviceV1, event: river.InputDeviceV1.Event, input_device: *InputDevice) void {
assert(input_device.river_input_device_v1 == river_input_device_v1);
switch (event) {
.removed => {
river_input_device_v1.destroy();
input_device.destroy();
},
.type => |ev| {
// This event is only sent once when the object is created
assert(input_device.type == null);
input_device.type = ev.type;
},
.name => |ev| {
// This event is only sent once when the object is created
assert(input_device.name == null);
input_device.name = mem.span(ev.name);
},
}
}
const std = @import("std");
const assert = std.debug.assert;
const mem = std.mem;
const wayland = @import("wayland");
const wl = wayland.client.wl;
const river = wayland.client.river;
const Type = river.InputDeviceV1.Type;
const utils = @import("utils.zig");
const Context = @import("Context.zig");
const LibinputDevice = @import("LibinputDevice.zig");
const log = std.log.scoped(.InputDevice);

87
src/InputManager.zig Normal file
View file

@ -0,0 +1,87 @@
// SPDX-FileCopyrightText: 2026 Ben Buhse <me@benbuhse.email>
//
// SPDX-License-Identifier: GPL-3.0-or-later
const InputManager = @This();
context: *Context,
river_input_manager_v1: *river.InputManagerV1,
river_libinput_config_v1: *river.LibinputConfigV1,
/// All input devices that we've been advertised
input_devices: wl.list.Head(InputDevice, .link),
/// All libinput devices that we've been advertised
/// Not necessarily the same length as input_devices
libinput_devices: wl.list.Head(LibinputDevice, .link),
pub fn create(context: *Context, river_input_manager_v1: *river.InputManagerV1, river_libinput_config_v1: *river.LibinputConfigV1) !*InputManager {
log.debug("Creating new InputManager", .{});
const im = try utils.allocator.create(InputManager);
errdefer im.destroy();
im.* = .{
.context = context,
.river_input_manager_v1 = river_input_manager_v1,
.river_libinput_config_v1 = river_libinput_config_v1,
.input_devices = undefined, // we will initialize these shortly
.libinput_devices = undefined,
};
im.input_devices.init();
im.libinput_devices.init();
im.river_input_manager_v1.setListener(*InputManager, inputManagerV1Listener, im);
im.river_libinput_config_v1.setListener(*InputManager, libinputConfigV1Listener, im);
return im;
}
pub fn destroy(im: *InputManager) void {
utils.allocator.destroy(im);
}
pub fn inputManagerV1Listener(river_input_manager_v1: *river.InputManagerV1, event: river.InputManagerV1.Event, im: *InputManager) void {
assert(im.river_input_manager_v1 == river_input_manager_v1);
switch (event) {
.input_device => |ev| {
const input_device = InputDevice.create(ev.id) catch @panic("Out of memory");
im.input_devices.append(input_device);
},
.finished => {
// TODO: Should call destroy on the river_input_manager_v1 and on this device,
// but might need to make the globals optional so that we know when we can destroy this
// object.
log.debug("unhandled event: finished", .{});
},
}
}
pub fn libinputConfigV1Listener(river_libinput_config_v1: *river.LibinputConfigV1, event: river.LibinputConfigV1.Event, im: *InputManager) void {
assert(im.river_libinput_config_v1 == river_libinput_config_v1);
switch (event) {
.libinput_device => |ev| {
const libinput_device = LibinputDevice.create(im.context, ev.id) catch @panic("Out of memory");
im.libinput_devices.append(libinput_device);
},
.finished => {
// TODO: Should call destroy on the river_libinput_config_v1 and on this device,
// but might need to make the globals optional so that we know when we can destroy this
// object.
log.debug("unhandled event: finished", .{});
},
}
}
const std = @import("std");
const assert = std.debug.assert;
const wayland = @import("wayland");
const wl = wayland.client.wl;
const river = wayland.client.river;
const utils = @import("utils.zig");
const Context = @import("Context.zig");
const InputDevice = @import("InputDevice.zig");
const LibinputDevice = @import("LibinputDevice.zig");
const log = std.log.scoped(.InputManager);

184
src/LibinputDevice.zig Normal file
View file

@ -0,0 +1,184 @@
// SPDX-FileCopyrightText: 2026 Ben Buhse <me@benbuhse.email>
//
// SPDX-License-Identifier: GPL-3.0-or-later
const LibinputDevice = @This();
context: *Context,
river_libinput_device_v1: *river.LibinputDeviceV1,
send_events_support: SendEventsModes = .{},
send_events_current: ?SendEventsModes = null,
/// The number of fingers supported for tap-to-click/drag.
/// If finger_count is 0, tap-to-click and drag are unsupported.
tap_support: u31 = 0,
tap_current: ?TapState = null,
tap_button_map_current: ?TapButtonMap = null,
drag_current: ?DragState = null,
drag_lock_current: ?DragLockState = null,
/// The number of fingers supported for three/four finger drag.
/// If finger_count is less than 3, three finger drag is unsupported.
three_finger_drag_support: u31 = 0,
three_finger_drag_current: ?ThreeFingerDragState = null,
/// A calibration matrix is supported if the supported argument is non-zero.
calibration_matrix_support: bool = false,
calibration_matrix_current: ?[]f32 = null,
accel_profiles_support: ?AccelProfiles = null,
accel_profile_current: ?AccelProfile = null,
accel_speed_current: ?f64 = null,
natural_scroll_support: bool = false,
natural_scroll_current: ?NaturalScrollState = null,
left_handed_support: bool = false,
left_handed_current: ?LeftHandedState = null,
click_method_support: ?ClickMethods = null,
click_method_current: ?ClickMethod = null,
clickfinger_button_map_current: ?ClickfingerButtonMap = null,
middle_emulation_support: bool = false,
middle_emulation_current: ?MiddleEmulationState = null,
scroll_method_support: ?ScrollMethods = null,
scroll_method_current: ?ScrollMethod = null,
/// Supported if scroll_methods.on_button_down is supported.
scroll_button_current: ?u32 = null,
/// Supported if scroll_methods.on_button_down is supported.
scroll_button_lock_current: ?ScrollButtonLockState = null,
dwt_support: bool = false,
dwt_current: ?DwtState = null,
dwtp_support: bool = false,
dwtp_current: ?DwtpState = null,
rotation_support: bool = false,
rotation_current: ?u32 = null,
link: wl.list.Link,
pub fn create(context: *Context, river_libinput_device_v1: *river.LibinputDeviceV1) !*LibinputDevice {
const libinput_device = try utils.allocator.create(LibinputDevice);
errdefer libinput_device.destroy();
libinput_device.* = .{
.context = context,
.river_libinput_device_v1 = river_libinput_device_v1,
.link = undefined, // handled by the wl.List
};
libinput_device.river_libinput_device_v1.setListener(
*LibinputDevice,
riverLibinputDeviceV1Listener,
libinput_device,
);
return libinput_device;
}
pub fn destroy(input_device: *LibinputDevice) void {
input_device.link.remove();
utils.allocator.destroy(input_device);
}
fn riverLibinputDeviceV1Listener(river_libinput_device_v1: *river.LibinputDeviceV1, event: river.LibinputDeviceV1.Event, libinput_device: *LibinputDevice) void {
assert(libinput_device.river_libinput_device_v1 == river_libinput_device_v1);
const im = libinput_device.context.im;
switch (event) {
.removed => {
river_libinput_device_v1.destroy();
libinput_device.destroy();
},
.input_device => |ev| {
const river_input_device_v1 = ev.device.?;
var it = im.input_devices.iterator(.forward);
while (it.next()) |input_device| {
if (input_device.river_input_device_v1 == river_input_device_v1) {
// This event is only sent once when the object is created
assert(input_device.libinput_device == null);
input_device.libinput_device = libinput_device;
log.info("input dev {} is associated to libinput {}", .{ river_libinput_device_v1.getId(), river_input_device_v1.getId() });
}
}
},
.send_events_support => |ev| libinput_device.send_events_support = ev.modes,
.send_events_current => |ev| libinput_device.send_events_current = ev.mode,
.tap_support => |ev| libinput_device.tap_support = @intCast(ev.finger_count),
.tap_current => |ev| libinput_device.tap_current = ev.state,
.tap_button_map_current => |ev| libinput_device.tap_button_map_current = ev.button_map,
.drag_current => |ev| libinput_device.drag_current = ev.state,
.drag_lock_current => |ev| libinput_device.drag_lock_current = ev.state,
.three_finger_drag_support => |ev| libinput_device.three_finger_drag_support = @intCast(ev.finger_count),
.three_finger_drag_current => |ev| libinput_device.three_finger_drag_current = ev.state,
.calibration_matrix_support => |ev| libinput_device.calibration_matrix_support = ev.supported != 0,
.calibration_matrix_current => |ev| libinput_device.calibration_matrix_current = ev.matrix.slice(f32),
.accel_profiles_support => |ev| libinput_device.accel_profiles_support = ev.profiles,
.accel_profile_current => |ev| libinput_device.accel_profile_current = ev.profile,
.accel_speed_current => |ev| libinput_device.accel_speed_current = ev.speed.slice(f64)[0],
.natural_scroll_support => |ev| libinput_device.natural_scroll_support = ev.supported != 0,
.natural_scroll_current => |ev| libinput_device.natural_scroll_current = ev.state,
.left_handed_support => |ev| libinput_device.left_handed_support = ev.supported != 0,
.left_handed_current => |ev| libinput_device.left_handed_current = ev.state,
.click_method_support => |ev| libinput_device.click_method_support = ev.methods,
.click_method_current => |ev| libinput_device.click_method_current = ev.method,
.clickfinger_button_map_current => |ev| libinput_device.clickfinger_button_map_current = ev.button_map,
.middle_emulation_support => |ev| libinput_device.middle_emulation_support = ev.supported != 0,
.middle_emulation_current => |ev| libinput_device.middle_emulation_current = ev.state,
.scroll_method_support => |ev| libinput_device.scroll_method_support = ev.methods,
.scroll_method_current => |ev| libinput_device.scroll_method_current = ev.method,
.scroll_button_current => |ev| libinput_device.scroll_button_current = ev.button,
.scroll_button_lock_current => |ev| libinput_device.scroll_button_lock_current = ev.state,
.dwt_support => |ev| libinput_device.dwt_support = ev.supported != 0,
.dwt_current => |ev| libinput_device.dwt_current = ev.state,
.dwtp_support => |ev| libinput_device.dwtp_support = ev.supported != 0,
.dwtp_current => |ev| libinput_device.dwtp_current = ev.state,
.rotation_support => |ev| libinput_device.rotation_support = ev.supported != 0,
.rotation_current => |ev| libinput_device.rotation_current = ev.angle,
else => |ev| {
// We don't keep track of any default states right now
log.debug("unhandled event: {s}", .{@tagName(ev)});
},
}
}
const std = @import("std");
const assert = std.debug.assert;
const wayland = @import("wayland");
const wl = wayland.client.wl;
const river = wayland.client.river;
const AccelProfile = river.LibinputDeviceV1.AccelProfile;
const AccelProfiles = river.LibinputDeviceV1.AccelProfiles;
const ClickfingerButtonMap = river.LibinputDeviceV1.ClickfingerButtonMap;
const ClickMethod = river.LibinputDeviceV1.ClickMethod;
const ClickMethods = river.LibinputDeviceV1.ClickMethods;
const DragLockState = river.LibinputDeviceV1.DragLockState;
const DragState = river.LibinputDeviceV1.DragState;
const DwtState = river.LibinputDeviceV1.DwtState;
const DwtpState = river.LibinputDeviceV1.DwtpState;
const LeftHandedState = river.LibinputDeviceV1.LeftHandedState;
const MiddleEmulationState = river.LibinputDeviceV1.MiddleEmulationState;
const NaturalScrollState = river.LibinputDeviceV1.NaturalScrollState;
const ScrollButtonLockState = river.LibinputDeviceV1.ScrollButtonLockState;
const ScrollMethod = river.LibinputDeviceV1.ScrollMethod;
const ScrollMethods = river.LibinputDeviceV1.ScrollMethods;
const SendEventsModes = river.LibinputDeviceV1.SendEventsModes;
const TapButtonMap = river.LibinputDeviceV1.TapButtonMap;
const TapState = river.LibinputDeviceV1.TapState;
const ThreeFingerDragState = river.LibinputDeviceV1.ThreeFingerDragState;
const utils = @import("utils.zig");
const Context = @import("Context.zig");
const log = std.log.scoped(.InputDevice);

View file

@ -90,31 +90,31 @@ pub fn destroy(output: *Output) void {
/// Get the next window in the list that shares at least one tag
/// with the output, wrapping to first if at end.
pub fn nextWindow(output: *Output, current: *Window) ?*Window {
var link = current.link.next orelse unreachable;
var link = current.link.next.?;
// Walk forward, wrapping at sentinel, until we find a visible window or return to current
while (true) {
// If this is the sentinel, wrap to the beginning
if (link == &output.windows.link) {
link = link.next orelse unreachable;
link = link.next.?;
}
const window: *Window = @fieldParentPtr("link", link);
if (window.tags & output.tags != 0 or window == current) return window;
link = link.next orelse unreachable;
link = link.next.?;
}
}
/// Get the previous window in the list that shares at least one tag
/// with the output, wrapping to the last if at beginning
pub fn prevWindow(output: *Output, current: *Window) ?*Window {
var link = current.link.prev orelse unreachable;
var link = current.link.prev.?;
while (true) {
// If this is the sentinel, wrap to the end
if (link == &output.windows.link) {
link = link.prev orelse unreachable;
link = link.prev.?;
}
const window: *Window = @fieldParentPtr("link", link);
if (window.tags & output.tags != 0 or window == current) return window;
link = link.prev orelse unreachable;
link = link.prev.?;
}
}
@ -171,7 +171,7 @@ fn riverOutputListener(river_output_v1: *river.OutputV1, event: river.OutputV1.E
},
.wl_output => |ev| {
// It's guaranteed for the wl_output global to advertised before this event happens
output.wl_output = output.context.wl_outputs.get(ev.name) orelse unreachable;
output.wl_output = output.context.wl_outputs.get(ev.name).?;
output.wl_output.?.setListener(*Output, wlOutputListener, output);
// The wl_output's initial events (mode, scale, name, done) were likely

View file

@ -4,6 +4,8 @@
/// Wayland globals that we need to bind listen in alphabetical order
const Globals = struct {
river_input_manager_v1: ?*river.InputManagerV1 = null,
river_libinput_config_v1: ?*river.LibinputConfigV1 = null,
river_layer_shell_v1: ?*river.LayerShellV1 = null,
river_window_manager_v1: ?*river.WindowManagerV1 = null,
river_xkb_bindings_v1: ?*river.XkbBindingsV1 = null,
@ -30,6 +32,8 @@ const usage: []const u8 =
\\ -version Print the version number and exit.
\\ -log-level <level> Set the log level to error, warning, info, or debug.
\\
\\ Config belongs under $XDG_CONFIG_DIR or $HOME/.config at beansprout/config.kdl
\\
;
pub fn main() !void {
@ -100,6 +104,8 @@ pub fn main() !void {
// We can theoretically start with zero wl_outputs; don't panic if it's empty.
const wl_outputs = &globals.wl_outputs;
const river_input_manager_v1 = globals.river_input_manager_v1 orelse utils.interfaceNotAdvertised(river.InputManagerV1);
const river_libinput_config_v1 = globals.river_libinput_config_v1 orelse utils.interfaceNotAdvertised(river.LibinputConfigV1);
const river_layer_shell_v1 = globals.river_layer_shell_v1 orelse utils.interfaceNotAdvertised(river.LayerShellV1);
const river_window_manager_v1 = globals.river_window_manager_v1 orelse utils.interfaceNotAdvertised(river.WindowManagerV1);
const river_xkb_bindings_v1 = globals.river_xkb_bindings_v1 orelse utils.interfaceNotAdvertised(river.XkbBindingsV1);
@ -114,6 +120,8 @@ pub fn main() !void {
.wl_outputs = wl_outputs,
.wl_registry = wl_registry,
.wl_shm = wl_shm,
.river_input_manager_v1 = river_input_manager_v1,
.river_libinput_config_v1 = river_libinput_config_v1,
.river_layer_shell_v1 = river_layer_shell_v1,
.river_window_manager_v1 = river_window_manager_v1,
.river_xkb_bindings_v1 = river_xkb_bindings_v1,
@ -155,6 +163,14 @@ fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, globals: *
globals.wl_shm = registry.bind(ev.name, wl.Shm, 1) catch |e| {
fatal("Failed to bind to wl_shm: {any}", .{@errorName(e)});
};
} else if (mem.orderZ(u8, ev.interface, river.InputManagerV1.interface.name) == .eq) {
globals.river_input_manager_v1 = registry.bind(ev.name, river.InputManagerV1, 1) catch |e| {
fatal("Failed to bind to river_input_manager_v1: {any}", .{@errorName(e)});
};
} else if (mem.orderZ(u8, ev.interface, river.LibinputConfigV1.interface.name) == .eq) {
globals.river_libinput_config_v1 = registry.bind(ev.name, river.LibinputConfigV1, 1) catch |e| {
fatal("Failed to bind to river_libinput_config_v1: {any}", .{@errorName(e)});
};
} else if (mem.orderZ(u8, ev.interface, river.LayerShellV1.interface.name) == .eq) {
globals.river_layer_shell_v1 = registry.bind(ev.name, river.LayerShellV1, 1) catch |e| {
fatal("Failed to bind to river_layer_shell_v1: {any}", .{@errorName(e)});
@ -174,7 +190,6 @@ fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, globals: *
};
}
},
// We don't need .global_remove
.global_remove => |ev| {
// The only remove we care about is for wl_outputs
if (!globals.wl_outputs.remove(ev.name)) {
@ -198,11 +213,11 @@ var runtime_log_level: std.log.Level = switch (builtin.mode) {
.ReleaseSafe, .ReleaseFast, .ReleaseSmall => .info,
};
pub const std_options: std.Options = .{
// Tell std.log to leave all log level filtering to us.
.log_level = .debug,
.logFn = logFn,
};
// pub const std_options: std.Options = .{
// // Tell std.log to leave all log level filtering to us.
// .log_level = .debug,
// .logFn = logFn,
// };
pub fn logFn(
comptime level: std.log.Level,