From 296f875993fefca6cf6c20bdccce357b569da11c Mon Sep 17 00:00:00 2001 From: Ben Buhse Date: Tue, 10 Feb 2026 20:06:17 -0600 Subject: [PATCH] Implement libinput device configuration We need to defer config application to the first manage_start event using a should_manage flag so that all *_support events have arrived before we try applying the configs This commit also has two other fixes - fixes a potential use-after-free by telling InputDevice when a LibinputDevice is .removed. - fix logFn (removed "if (scope != .default) return;") I used kwm to help figure out the manage pattern for the input config. Link to kwm: https://github.com/kewuaa/kwm --- README.md | 6 ++ src/Context.zig | 12 ++++ src/InputDevice.zig | 18 +++-- src/LibinputDevice.zig | 158 +++++++++++++++++++++++++++++++++++++++-- src/main.zig | 12 ++-- 5 files changed, 184 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index e296ec8..beaa325 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,20 @@ SPDX-License-Identifier: GPL-3.0-or-later These are in rough order of my priority, though no promises I do them in this order. +- [ ] Support `None` modifier for keybinds (needed for media/brightness keys) - [ ] Support per-host config using properties (maybe also per-output?) - [ ] Add input configuration, i.e. pointer acceleration and that type of thing - [ ] Support a basic bar - [ ] Support starting programs at WM launch - [ ] Support overriding config location - [ ] Add support for multimedia/brightness keys (this might not be neccesary) +- [ ] Support window rules (float/tags/SSD by app-id/title) +- [ ] Support switch handling (e.g. lid close) - [ ] Support multiple seats - [ ] Support clipping floating windows on edge of/between outputs +- [ ] Support keybind modes (e.g. passthrough) +- [ ] Support `focus-follows-cursor` granularity (`normal` vs `always`) +- [ ] Support solid `background-color` fallback (no wallpaper) - [x] Support changeable primary ratio - [x] Support changeable primary count - [x] Support multiple outputs diff --git a/src/Context.zig b/src/Context.zig index ad9cc26..7f4531a 100644 --- a/src/Context.zig +++ b/src/Context.zig @@ -112,6 +112,12 @@ pub fn manage(context: *Context) void { context.config = new_config; context.initialized = false; + // Mark all libinput devices as needing config re-application + var dev_it = context.im.libinput_devices.iterator(.forward); + while (dev_it.next()) |libinput_device| { + libinput_device.should_manage = true; + } + if (wallpaper_changed) { if (context.wallpaper_image) |img| img.destroy(); context.wallpaper_image = loadWallpaperImage(new_config); @@ -132,6 +138,12 @@ pub fn manage(context: *Context) void { } } } + + // Apply input configs for new or reconfigured devices + var dev_it = context.im.libinput_devices.iterator(.forward); + while (dev_it.next()) |libinput_device| { + libinput_device.manage(); + } } fn loadWallpaperImage(config: *Config) ?*WallpaperImage { diff --git a/src/InputDevice.zig b/src/InputDevice.zig index 68e3de9..aa38520 100644 --- a/src/InputDevice.zig +++ b/src/InputDevice.zig @@ -36,6 +36,12 @@ pub fn create(river_input_device_v1: *river.InputDeviceV1) !*InputDevice { } pub fn destroy(input_device: *InputDevice) void { + if (input_device.libinput_device) |libinput_device| { + libinput_device.input_device = null; + } + if (input_device.name) |name| { + utils.allocator.free(name); + } input_device.link.remove(); utils.allocator.destroy(input_device); } @@ -47,16 +53,8 @@ fn riverInputDeviceV1Listener(river_input_device_v1: *river.InputDeviceV1, event 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); - }, + .type => |ev| input_device.type = ev.type, + .name => |ev| input_device.name = utils.allocator.dupe(u8, mem.span(ev.name)) catch @panic("Out of memory"), } } diff --git a/src/LibinputDevice.zig b/src/LibinputDevice.zig index 708ff27..6c902c1 100644 --- a/src/LibinputDevice.zig +++ b/src/LibinputDevice.zig @@ -8,6 +8,15 @@ context: *Context, river_libinput_device_v1: *river.LibinputDeviceV1, +/// The river_input_device_v1 associated with this Libinput device. +input_device: ?*InputDevice = null, + +/// Set to true whenever we want to apply input configurations. +/// At first, we wait for the first manage_start because it's after all of the +/// _support events. After that, it's set to true whenever the config is +/// reloaded. +should_manage: bool = true, + send_events_support: SendEventsModes = .{}, send_events_current: ?SendEventsModes = null, @@ -87,14 +96,18 @@ pub fn create(context: *Context, river_libinput_device_v1: *river.LibinputDevice return libinput_device; } -pub fn destroy(input_device: *LibinputDevice) void { - input_device.link.remove(); - utils.allocator.destroy(input_device); +pub fn destroy(libinput_device: *LibinputDevice) void { + if (libinput_device.input_device) |input_device| { + input_device.libinput_device = null; + } + libinput_device.link.remove(); + utils.allocator.destroy(libinput_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; + log.debug("bwbuhse: {s} for {d}", .{ @tagName(event), river_libinput_device_v1.getId() }); switch (event) { .removed => { river_libinput_device_v1.destroy(); @@ -105,9 +118,8 @@ fn riverLibinputDeviceV1Listener(river_libinput_device_v1: *river.LibinputDevice 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; + libinput_device.input_device = input_device; log.info("input dev {} is associated to libinput {}", .{ river_libinput_device_v1.getId(), river_input_device_v1.getId() }); } } @@ -152,8 +164,143 @@ fn riverLibinputDeviceV1Listener(river_libinput_device_v1: *river.LibinputDevice } } +pub fn manage(libinput_device: *LibinputDevice) void { + if (libinput_device.should_manage) { + libinput_device.should_manage = false; + libinput_device.applyInputConfigs(); + } +} + +pub fn applyInputConfigs(libinput_device: *LibinputDevice) void { + const input_device = libinput_device.input_device orelse return; + const device_name = input_device.name orelse return; + const config = libinput_device.context.config; + const dev = libinput_device.river_libinput_device_v1; + + for (config.input_configs.items) |input_config| { + if (input_config.name) |config_name| { + if (!mem.eql(u8, config_name, device_name)) continue; + } + + log.debug("Applying input config to {s}", .{device_name}); + + if (@as(u32, @bitCast(libinput_device.send_events_support)) != 0) { + if (input_config.send_events) |val| { + const mode: SendEventsModes = @bitCast(@as(u32, @intCast(@intFromEnum(val)))); + const mode_bits: u32 = @bitCast(mode); + const support_bits: u32 = @bitCast(libinput_device.send_events_support); + if (mode_bits == 0 or mode_bits & support_bits == mode_bits) { + applyResult(dev.setSendEvents(mode)); + } + } + } + + if (libinput_device.tap_support > 0) { + if (input_config.tap) |val| applyResult(dev.setTap(val)); + if (input_config.tap_button_map) |val| applyResult(dev.setTapButtonMap(val)); + if (input_config.drag) |val| applyResult(dev.setDrag(val)); + if (input_config.drag_lock) |val| applyResult(dev.setDragLock(val)); + } + + if (libinput_device.three_finger_drag_support >= 3) { + if (input_config.three_finger_drag) |val| applyResult(dev.setThreeFingerDrag(val)); + } + + if (libinput_device.accel_profiles_support) |support| { + if (input_config.accel_profile) |val| { + if (isSupported(AccelProfile, AccelProfiles, val, support)) { + applyResult(dev.setAccelProfile(val)); + } + } + if (input_config.accel_speed) |speed| { + var speed_val: f64 = speed; + var speed_array: wl.Array = .{ + .size = @sizeOf(f64), + .alloc = @sizeOf(f64), + .data = @ptrCast(&speed_val), + }; + applyResult(dev.setAccelSpeed(&speed_array)); + } + } + + if (libinput_device.natural_scroll_support) { + if (input_config.natural_scroll) |val| applyResult(dev.setNaturalScroll(val)); + } + + if (libinput_device.left_handed_support) { + if (input_config.left_handed) |val| applyResult(dev.setLeftHanded(val)); + } + + if (libinput_device.click_method_support) |support| { + if (input_config.click_method) |val| { + if (isSupported(ClickMethod, ClickMethods, val, support)) { + applyResult(dev.setClickMethod(val)); + } + } + if (input_config.clickfinger_button_map) |val| applyResult(dev.setClickfingerButtonMap(val)); + } + + if (libinput_device.middle_emulation_support) { + if (input_config.middle_emulation) |val| applyResult(dev.setMiddleEmulation(val)); + } + + if (libinput_device.scroll_method_support) |support| { + if (input_config.scroll_method) |val| { + if (isSupported(ScrollMethod, ScrollMethods, val, support)) { + applyResult(dev.setScrollMethod(val)); + } + } + if (support.on_button_down) { + if (input_config.scroll_button) |val| applyResult(dev.setScrollButton(val)); + if (input_config.scroll_button_lock) |val| applyResult(dev.setScrollButtonLock(val)); + } + } + + if (libinput_device.dwt_support) { + if (input_config.dwt) |val| applyResult(dev.setDwt(val)); + } + + if (libinput_device.dwtp_support) { + if (input_config.dwtp) |val| applyResult(dev.setDwtp(val)); + } + + if (libinput_device.rotation_support) { + if (input_config.rotation) |val| applyResult(dev.setRotation(val)); + } + } +} + +fn isSupported(comptime E: type, comptime S: type, val: E, support: S) bool { + const int_val: u32 = @intCast(@intFromEnum(val)); + if (int_val == 0) return true; + const support_bits: u32 = @bitCast(support); + return int_val & support_bits == int_val; +} + +/// Handles the result of a set_* request by setting a listener to +/// log any unsupported or invalid config responses from the compositor. +fn applyResult(result: anyerror!*river.LibinputResultV1) void { + const Listener = struct { + fn resultListener(_: *river.LibinputResultV1, event: river.LibinputResultV1.Event, _: ?*anyopaque) void { + switch (event) { + .success => {}, + .unsupported => log.debug("Config option unsupported by device", .{}), + .invalid => log.warn("Invalid config value for device", .{}), + } + } + }; + + const r = result catch |err| { + log.err("Failed to send input config request: {}", .{err}); + return; + }; + // We don't need any userdata in this listener + r.setListener(?*anyopaque, Listener.resultListener, null); +} + const std = @import("std"); const assert = std.debug.assert; +const mem = std.mem; const wayland = @import("wayland"); const wl = wayland.client.wl; @@ -180,5 +327,6 @@ const ThreeFingerDragState = river.LibinputDeviceV1.ThreeFingerDragState; const utils = @import("utils.zig"); const Context = @import("Context.zig"); +const InputDevice = @import("InputDevice.zig"); const log = std.log.scoped(.InputDevice); diff --git a/src/main.zig b/src/main.zig index 26b6100..5f322b1 100644 --- a/src/main.zig +++ b/src/main.zig @@ -213,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, @@ -227,8 +227,6 @@ pub fn logFn( ) void { if (@intFromEnum(level) > @intFromEnum(runtime_log_level)) return; - if (scope != .default) return; - const scope_prefix = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): "; stderr.print(level.asText() ++ scope_prefix ++ format ++ "\n", args) catch return;