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/build.zig b/build.zig index 6751489..8d4ec39 100644 --- a/build.zig +++ b/build.zig @@ -22,18 +22,22 @@ pub fn build(b: *std.Build) void { const xkbcommon = b.dependency("xkbcommon", .{}).module("xkbcommon"); const zigimg = b.dependency("zigimg", .{}).module("zigimg"); + scanner.addCustomProtocol(b.path("protocol/river-input-management-v1.xml")); + scanner.addCustomProtocol(b.path("protocol/river-libinput-config-v1.xml")); + scanner.addCustomProtocol(b.path("protocol/river-layer-shell-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/river-layer-shell-v1.xml")); scanner.addSystemProtocol("stable/xdg-shell/xdg-shell.xml"); // dep of wlr-layer-shell-unstable-v1 scanner.addCustomProtocol(b.path("protocol/wlr-layer-shell-unstable-v1.xml")); scanner.generate("wl_compositor", 4); scanner.generate("wl_shm", 1); scanner.generate("wl_output", 4); + scanner.generate("river_input_manager_v1", 1); + scanner.generate("river_libinput_config_v1", 1); + scanner.generate("river_layer_shell_v1", 1); scanner.generate("river_window_manager_v1", 3); scanner.generate("river_xkb_bindings_v1", 2); - scanner.generate("river_layer_shell_v1", 1); scanner.generate("zwlr_layer_shell_v1", 3); const options = b.addOptions(); diff --git a/build.zig.zon b/build.zig.zon index d06c22e..376a45f 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,7 +1,7 @@ .{ .name = .beansprout, - .version = "0.0.0", + .version = "0.0.1", .fingerprint = 0x145dac71c283d187, // Changing this has security and trust implications. @@ -9,16 +9,16 @@ .dependencies = .{ .wayland = .{ - .url = "https://codeberg.org/ifreund/zig-wayland/archive/v0.4.0.tar.gz", - .hash = "wayland-0.4.0-lQa1khbMAQAsLS2eBR7M5lofyEGPIbu2iFDmoz8lPC27", + .url = "https://codeberg.org/ifreund/zig-wayland/archive/e57368ecbda85d564362779b253b744260a4b053.tar.gz", + .hash = "wayland-0.5.0-dev-lQa1kv_ZAQCZfnVZMocokZ78QJbH6NaM5RUC9ODQPhx5", }, .xkbcommon = .{ .url = "https://codeberg.org/ifreund/zig-xkbcommon/archive/6786ca619bb442c3f523b5bb894e6a1e48d7e897.tar.gz", .hash = "xkbcommon-0.4.0-dev-VDqIe0y2AgCNeWLthDZ3MUcUYzhyKXjK85ISm_zxk9Nk", }, .kdl = .{ - .url = "https://codeberg.org/desttinghim/zig-kdl/archive/edc943426ba1fc47606568a9fc7f402b2b1992e0.tar.gz", - .hash = "kdl-0.0.0-8rilEPw_AQDhyfjEIg9pzpBHUyz6bOQ6qCfZImzYn42A", + .url = "https://codeberg.org/desttinghim/zig-kdl/archive/9a92d2cc6bb25031778d321c6c1d87e9e4052eab.tar.gz", + .hash = "kdl-0.0.0-8rilEMFEAQCYVNhFIcJZWp8HLrjYaEIZGov6CSH05Dsv", }, .known_folders = .{ .url = "https://github.com/ziglibs/known-folders/archive/83d39161eac2ed6f37ad3cb4d9dd518696ce90bb.tar.gz", diff --git a/examples/config.kdl b/examples/config.kdl index ff7e31d..5451cf1 100644 --- a/examples/config.kdl +++ b/examples/config.kdl @@ -69,4 +69,15 @@ pointer_binds { // tiled windows will automatically float if resized resize_window Mod4 BTN_RIGHT } +// Default input config for all devices +input { + accel_profile "flat" +} +// Framework 13 Touchpad +input "PIXA3854:00 093A:0274 Touchpad" { + accel_profile "adaptive" + click_method "clickfinger" + natural_scroll "enabled" + tap "disabled" +} diff --git a/protocol/river-input-management-v1.xml b/protocol/river-input-management-v1.xml new file mode 100644 index 0000000..9ee9608 --- /dev/null +++ b/protocol/river-input-management-v1.xml @@ -0,0 +1,240 @@ + + + + SPDX-FileCopyrightText: © 2025 Isaac Freund + SPDX-License-Identifier: MIT + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + IN THE SOFTWARE. + + + + This protocol supports creating/destroying seats, assigning input devices to + seats, and configuring input devices (e.g. setting keyboard repeat rate). + + 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. + + + + + Input manager global interface. + + + + + + + + + This request indicates that the client no longer wishes to receive + events on this object. + + The Wayland protocol is asynchronous, which means the server may send + further events until the stop request is processed. The client must wait + for a river_input_manager_v1.finished event before destroying this + object. + + + + + + This event indicates that the server will send no further events on this + object. The client should destroy the object. See + river_input_manager_v1.destroy for more information. + + + + + + This request should be called after the finished event has been received + to complete destruction of the object. + + It is a protocol error to make this request before the finished event + has been received. + + If a client wishes to destroy this object it should send a + river_input_manager_v1.stop request and wait for a + river_input_manager_v1.finished event. Once the finished event is + received it is safe to destroy this object and any other objects created + through this interface. + + + + + + Create a new seat with the given name. Has no effect if a seat with the + given name already exists. + + The default seat with name "default" always exists and does not need to + be explicitly created. + + + + + + + Destroy the seat with the given name. Has no effect if a seat with the + given name does not exist. + + The default seat with name "default" cannot be destroyed and attempting + to destroy it will have no effect. + + Any input devices assigned to the destroyed seat at the time of + destruction are assigned to the default seat. + + + + + + + A new input device has been created. + + + + + + + + An input device represents a physical keyboard, mouse, touchscreen, or + drawing tablet tool. It is assigned to exactly one seat at a time. + By default, all input devices are assigned to the default seat. + + + + + + + + + + + This request indicates that the client will no longer use the input + device object and that it may be safely destroyed. + + + + + + This event indicates that the input device has been removed. + + The server will send no further events on this object and ignore any + request (other than river_input_device_v1.destroy) made after this event is + sent. The client should destroy this object with the + river_input_device_v1.destroy request to free up resources. + + + + + + + + + + + + + The type of the input device. This event is sent once when the + river_input_device_v1 object is created. The device type cannot + change during the lifetime of the object. + + + + + + + The name of the input device. This event is sent once when the + river_input_device_v1 object is created. The device name cannot + change during the lifetime of the object. + + + + + + + Assign the input device to a seat. All input devices not explicitly + assigned to a seat are considered assigned to the default seat. + + Has no effect if a seat with the given name does not exist. + + + + + + + Set repeat rate and delay for a keyboard input device. Has no effect if + the device is not a keyboard. + + Negative values for either rate or delay are illegal. A rate of zero + will disable any repeating (regardless of the value of delay). + + + + + + + + Set the scroll factor for a pointer input device. Has no effect if the + device is not a pointer. + + For example, a factor of 0.5 will make scrolling twice as slow while a + factor of 3.0 will make scrolling 3 times as fast. + + Negative values for either rate or delay are illegal. A rate of zero + will disable any repeating (regardless of the value of delay). + + + + + + + Map the input device to the given output. Has no effect if the device is + not a pointer, touch, or tablet device. + + If mapped to both an output and a rectangle, the rectangle has priority. + + Passing null clears an existing mapping. + + + + + + + Map the input device to the given rectangle in the global compositor + coordinate space. Has no effect if the device is not a pointer, touch, + or tablet device. + + If mapped to both an output and a rectangle, the rectangle has priority. + + Width and height must be greater than or equal to 0. + + Passing 0 for width or height clears an existing mapping. + + + + + + + + diff --git a/protocol/river-libinput-config-v1.xml b/protocol/river-libinput-config-v1.xml new file mode 100644 index 0000000..18e4518 --- /dev/null +++ b/protocol/river-libinput-config-v1.xml @@ -0,0 +1,892 @@ + + + + SPDX-FileCopyrightText: © 2025 Isaac Freund + SPDX-License-Identifier: MIT + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + IN THE SOFTWARE. + + + + This protocol exposes libinput device configuration APIs. The libinput + documentation should be referred to for detailed information on libinput's + behavior. + + This protocol is designed so that (hopefully) any backwards compatible + change to libinput's API can be matched with a backwards compatible change + to this protocol. + + Note: the libinput API uses floating point types (float and double in C) + which are not (yet?) natively supported by the Wayland protocol. However, + the Wayland protocol does support sending arbitrary bytes through the array + argument type. This protocol uses e.g. type="array" summary="double" to + indicate a native-endian IEEE-754 64-bit double value. + + 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. + + + + + Global interface for configuring libinput devices. This global should + only be advertised if river_input_manager_v1 is advertised as well. + + + + + + + + + + This request indicates that the client no longer wishes to receive + events on this object. + + The Wayland protocol is asynchronous, which means the server may send + further events until the stop request is processed. The client must wait + for a river_libinput_config_v1.finished event before destroying this + object. + + + + + + This event indicates that the server will send no further events on this + object. The client should destroy the object. See + river_libinput_config_v1.destroy for more information. + + + + + + This request should be called after the finished event has been received + to complete destruction of the object. + + It is a protocol error to make this request before the finished event + has been received. + + If a client wishes to destroy this object it should send a + river_libinput_config_v1.stop request and wait for a + river_libinput_config_v1.finished event. Once the finished event is + received it is safe to destroy this object and any other objects created + through this interface. + + + + + + A new libinput device has been created. Not every river_input_device_v1 + is necessarily a libinput device as well. + + + + + + + Create a acceleration config which can be applied + with river_libinput_device_v1.apply_accel_config. + + + + + + + + + In general, *_support events will be sent exactly once directly after the + river_libinput_device_v1 is created. *_default events will be sent after + *_support events if the config option is supported, and *_current events + willl be sent after the *_default events and again whenever the config + option is changed. + + + + + + + + + This request indicates that the client will no longer use the input + device object and that it may be safely destroyed. + + + + + + This event indicates that the libinput device has been removed. + + The server will send no further events on this object and ignore any + request (other than river_libinput_device_v1.destroy) made after this + event is sent. The client should destroy this object with the + river_libinput_device_v1.destroy request to free up resources. + + + + + + The river_input_device_v1 corresponding to this libinput device. + This event will always be the first event sent on the + river_libinput_device_v1 object, and it will be sent exactly once. + + + + + + + + + + + + + Supported send events modes. + + + + + + + Default send events mode. + + + + + + + Current send events mode. + + + + + + + Set the send events mode for the device. + + + + + + + + + + + + + The number of fingers supported for tap-to-click/drag. + If finger_count is 0, tap-to-click and drag are unsupported. + + + + + + + Default tap-to-click state. + + + + + + + Current tap-to-click state. + + + + + + + Configure tap-to-click on this device, with a default mapping of + 1, 2, 3 finger tap mapping to left, right, middle click, respectively. + + + + + + + + + + + + + Default tap-to-click button map. + + + + + + + Current tap-to-click button map. + + + + + + + Set the finger number to button number mapping for tap-to-click. The + default mapping on most devices is to have a 1, 2 and 3 finger tap to + map to the left, right and middle button, respectively. + + + + + + + + + + + + + Default tap-and-drag state. + + + + + + + Current tap-and-drag state. + + + + + + + Configure tap-and-drag functionality on the device. + + + + + + + + + + + + + + Default drag lock state. + + + + + + + Current drag lock state. + + + + + + + Configure drag-lock during tapping on this device. When enabled, a + finger may be lifted and put back on the touchpad and the drag process + continues. A timeout for lifting the finger is optional. When disabled, + lifting the finger during a tap-and-drag will immediately stop the drag. + See the libinput documentation for more details. + + + + + + + + The number of fingers supported for three/four finger drag. + If finger_count is less than 3, three finger drag is unsupported. + + + + + + + + + + + + + Default three finger drag state. + + + + + + + Current three finger drag state. + + + + + + + Configure three finger drag functionality for the device. + + + + + + + + A calibration matrix is supported if the supported argument is non-zero. + + + + + + + Default calibration matrix. + + + + + + + Current calibration matrix. + + + + + + + Set calibration matrix. + + + + + + + + + + + + + + + + + + + + + + Supported acceleration profiles. + + + + + + + Default acceleration profile. + + + + + + + Current acceleration profile. + + + + + + + Set the acceleration profile. + + + + + + + + Default acceleration speed. + + + + + + + Current acceleration speed. + + + + + + + Set the acceleration speed within a range of [-1, 1], where 0 is + the default acceleration for this device, -1 is the slowest acceleration + and 1 is the maximum acceleration available on this device. + + + + + + + + Apply a pointer accleration config. + + + + + + + + Natural scroll is supported if the supported argument is non-zero. + + + + + + + + + + + + Default natural scroll. + + + + + + + Current natural scroll. + + + + + + + Set natural scroll state. + + + + + + + + Left-handed mode is supported if the supported argument is non-zero. + + + + + + + + + + + + Default left-handed mode. + + + + + + + Current left-handed mode. + + + + + + + Set left-handed mode state. + + + + + + + + + + + + + + + + + + + + The click methods suppported by the device. + + + + + + + Default click method. + + + + + + + Current click method. + + + + + + + Set click method. + + + + + + + + + + + + + Default clickfinger button map. + Supported if click_methods.clickfinger is supported. + + + + + + + Current clickfinger button map. + Supported if click_methods.clickfinger is supported. + + + + + + + Set clickfinger button map. + Supported if click_methods.clickfinger is supported. + + + + + + + + Middle mouse button emulation is supported if the supported argument is + non-zero. + + + + + + + + + + + + Default middle mouse button emulation. + + + + + + + Current middle mouse button emulation. + + + + + + + Set middle mouse button emulation state. + + + + + + + + + + + + + + + + + + + + + + The scroll methods suppported by the device. + + + + + + + Default scroll method. + + + + + + + Current scroll method. + + + + + + + Set scroll method. + + + + + + + + Default scroll button. + Supported if scroll_methods.on_button_down is supported. + + + + + + + Current scroll button. + Supported if scroll_methods.on_button_down is supported. + + + + + + + Set scroll button. + Supported if scroll_methods.on_button_down is supported. + + + + + + + + + + + + + Default scroll button lock state. + Supported if scroll_methods.on_button_down is supported. + + + + + + + Current scroll button lock state. + Supported if scroll_methods.on_button_down is supported. + + + + + + + Set scroll button lock state. + Supported if scroll_methods.on_button_down is supported. + + + + + + + + Disable-while-typing is supported if the supported argument is + non-zero. + + + + + + + + + + + + Default disable-while-typing state. + + + + + + + Current disable-while-typing state. + + + + + + + Set disable-while-typing state. + + + + + + + + Disable-while-trackpointing is supported if the supported argument is + non-zero. + + + + + + + + + + + + Default disable-while-trackpointing state. + + + + + + + Current disable-while-trackpointing state. + + + + + + + Set disable-while-trackpointing state. + + + + + + + + Rotation is supported if the supported argument is non-zero. + + + + + + + Default rotation angle. + + + + + + + Current rotation angle. + + + + + + + Set rotation angle in degrees clockwise off the logical neutral + position. Angle must be in the range [0-360). + + + + + + + + + The result returned by libinput on setting configuration for a device. + + + + + + + + + This request indicates that the client will no longer use the accel + config object and that it may be safely destroyed. + + + + + + + + + + + + Defines the acceleration function for a given movement type + in an acceleration configuration with custom accel profile. + + + + + + + + + + + The result returned by libinput on setting configuration for a device. + + + + + The configuration was successfully applied to the device. + + + + + + The configuration is unsupported by the device and was ignored. + + + + + + The configuration is invalid and was ignored. + + + + diff --git a/src/Config.zig b/src/Config.zig index c39f9ef..c2495f5 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -28,6 +28,7 @@ wallpaper_image_path: ?[]const u8 = null, tag_binds: std.ArrayList(Keybind) = .{}, keybinds: std.ArrayList(Keybind) = .{}, pointer_binds: std.ArrayList(PointerBind) = .{}, +input_configs: std.ArrayList(InputConfig) = .{}, pub const Keybind = struct { modifiers: river.SeatV1.Modifiers, @@ -46,6 +47,32 @@ pub const PointerAction = enum { resize_window, }; +pub const InputConfig = struct { + /// Device name to match + /// If this is null, applies to all devices + name: ?[]const u8 = null, + + send_events: ?SendEventsModes.Enum = null, + tap: ?TapState = null, + tap_button_map: ?TapButtonMap = null, + drag: ?DragState = null, + drag_lock: ?DragLockState = null, + three_finger_drag: ?ThreeFingerDragState = null, + accel_profile: ?AccelProfile = null, + accel_speed: ?f64 = null, + natural_scroll: ?NaturalScrollState = null, + left_handed: ?LeftHandedState = null, + click_method: ?ClickMethod = null, + clickfinger_button_map: ?ClickfingerButtonMap = null, + middle_emulation: ?MiddleEmulationState = null, + scroll_method: ?ScrollMethod = null, + scroll_button: ?u32 = null, + scroll_button_lock: ?ScrollButtonLockState = null, + dwt: ?DwtState = null, + dwtp: ?DwtpState = null, + rotation: ?u32 = null, +}; + pub const AttachMode = enum { top, bottom, @@ -56,9 +83,11 @@ const NodeName = enum { focus_follows_pointer, pointer_warp_on_focus_change, wallpaper_image_path, + // Sections with child blocks borders, keybinds, pointer_binds, + input, }; const BorderNodeName = enum { @@ -72,6 +101,28 @@ const PointerBindNodeName = enum { resize_window, }; +const InputConfigNodeName = enum { + send_events, + tap, + tap_button_map, + drag, + drag_lock, + three_finger_drag, + accel_profile, + accel_speed, + natural_scroll, + left_handed, + click_method, + clickfinger_button_map, + middle_emulation, + scroll_method, + scroll_button, + scroll_button_lock, + dwt, + dwtp, + rotation, +}; + // We can just directly use the tag type from Command as our node name const KeybindNodeName = @typeInfo(XkbBindings.Command).@"union".tag_type.?; @@ -106,6 +157,10 @@ pub fn create() !*Config { config.keybinds.clearAndFree(utils.allocator); config.tag_binds.clearAndFree(utils.allocator); config.pointer_binds.clearAndFree(utils.allocator); + for (config.input_configs.items) |ic| { + if (ic.name) |name| utils.allocator.free(name); + } + config.input_configs.clearAndFree(utils.allocator); if (config.wallpaper_image_path) |path| { utils.allocator.free(path); } @@ -129,6 +184,10 @@ pub fn destroy(config: *Config) void { config.keybinds.deinit(utils.allocator); config.tag_binds.deinit(utils.allocator); config.pointer_binds.deinit(utils.allocator); + for (config.input_configs.items) |ic| { + if (ic.name) |name| utils.allocator.free(name); + } + config.input_configs.deinit(utils.allocator); if (config.wallpaper_image_path) |path| { utils.allocator.free(path); } @@ -141,6 +200,8 @@ fn load(config: *Config, reader: *Io.Reader) !void { defer parser.deinit(utils.allocator); var next_child_block: ?NodeName = null; + var pending_input_name: ?[]const u8 = null; + defer if (pending_input_name) |n| utils.allocator.free(n); // Parse the KDL config while (try parser.next()) |event| { @@ -150,6 +211,8 @@ fn load(config: *Config, reader: *Io.Reader) !void { if (next_child_block) |child_block| { logWarnMissingChildBlock(child_block); next_child_block = null; + if (pending_input_name) |n| utils.allocator.free(n); + pending_input_name = null; } // If it's a node, we check if it's a valid NodeName const node_name = std.meta.stringToEnum(NodeName, node.name); @@ -192,7 +255,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; @@ -208,6 +271,13 @@ fn load(config: *Config, reader: *Io.Reader) !void { .pointer_binds => { next_child_block = .pointer_binds; }, + .input => { + pending_input_name = if (node.argcount() > 0) + try utils.allocator.dupe(u8, utils.stripQuotes(node.arg(&parser, 0).?)) + else + null; + next_child_block = .input; + }, } } else { logWarnInvalidNode(node.name); @@ -219,6 +289,10 @@ fn load(config: *Config, reader: *Io.Reader) !void { .borders => try config.loadBordersChildBlock(&parser), .keybinds => try config.loadKeybindsChildBlock(&parser), .pointer_binds => try config.loadPointerBindsChildBlock(&parser), + .input => { + try config.loadInputChildBlock(&parser, pending_input_name); + pending_input_name = null; // ownership transferred + }, else => { // Nothing else should ever be marked as a next_child_block unreachable; @@ -494,6 +568,89 @@ fn loadPointerBindsChildBlock(config: *Config, parser: *kdl.Parser) !void { } } +fn loadInputChildBlock(config: *Config, parser: *kdl.Parser, name: ?[]const u8) !void { + var input_config: InputConfig = .{ .name = name }; + errdefer if (input_config.name) |n| utils.allocator.free(n); + + while (try parser.next()) |event| { + switch (event) { + .node => |node| { + const node_name = std.meta.stringToEnum(InputConfigNodeName, node.name); + if (node_name) |tag| { + const val_str = utils.stripQuotes(node.arg(parser, 0) orelse { + logWarnMissingNodeArg(tag, "value"); + continue; + }); + switch (tag) { + .accel_speed => { + const speed = fmt.parseFloat(f64, val_str) catch { + logWarnInvalidNodeArg(tag, val_str); + continue; + }; + input_config.accel_speed = speed; + log.debug("input.accel_speed: {s}", .{val_str}); + }, + .scroll_button => { + const button = parseButton(val_str) orelse { + logWarnInvalidNodeArg(tag, val_str); + continue; + }; + input_config.scroll_button = button; + log.debug("input.scroll_button: {s}", .{val_str}); + }, + .rotation => { + const angle = fmt.parseInt(u32, val_str, 0) catch { + logWarnInvalidNodeArg(tag, val_str); + continue; + }; + input_config.rotation = angle; + log.debug("input.rotation: {s}", .{val_str}); + }, + inline .send_events, + .tap, + .tap_button_map, + .drag, + .drag_lock, + .three_finger_drag, + .accel_profile, + .natural_scroll, + .left_handed, + .click_method, + .clickfinger_button_map, + .middle_emulation, + .scroll_method, + .scroll_button_lock, + .dwt, + .dwtp, + => |cmd| { + // These all have arguments, but we can use compile time constructs to reduce + // code re-use here. + // Because all the fields are optional, we have to use @typeInfo and get the optional's child type. + const field_name = @tagName(cmd); + const FieldType = @typeInfo(@TypeOf(@field(input_config, field_name))).optional.child; + if (std.meta.stringToEnum(FieldType, val_str)) |val| { + @field(input_config, field_name) = val; + log.debug("input.{s}: {s}", .{ field_name, val_str }); + } else { + logWarnInvalidNodeArg(cmd, val_str); + } + }, + } + } else { + logWarnInvalidNode(node.name); + } + }, + .child_block_begin => { + try config.skipChildBlock(parser); + }, + .child_block_end => { + try config.input_configs.append(utils.allocator, input_config); + return; + }, + } + } +} + fn parseButton(s: []const u8) ?u32 { // Support both numeric and named buttons var lower_buf: [16]u8 = undefined; @@ -560,6 +717,7 @@ fn logWarnInvalidNodeArg(node_name: anytype, node_value: []const u8) void { BorderNodeName => log.warn("Invalid \"border.{s}\" ({s}). Using default value", .{ @tagName(node_name), node_value }), KeybindNodeName => log.warn("Invalid \"keybind.{s}\" ({s}). Ignoring", .{ @tagName(node_name), node_value }), PointerBindNodeName => log.warn("Invalid \"pointer_binds.{s}\" ({s}). Ignoring", .{ @tagName(node_name), node_value }), + InputConfigNodeName => log.warn("Invalid \"input.{s}\" ({s}). Ignoring", .{ @tagName(node_name), node_value }), else => @compileError("This function does not (yet) support type \"" ++ @typeName(node_name_type) ++ "\""), } } @@ -570,6 +728,7 @@ fn logWarnMissingNodeArg(node_name: anytype, comptime arg: []const u8) void { NodeName => log.warn("\"{s}\" missing " ++ arg ++ " argument. Ignoring", .{@tagName(node_name)}), KeybindNodeName => log.warn("\"keybind.{s}\" missing " ++ arg ++ " argument. Ignoring", .{@tagName(node_name)}), PointerBindNodeName => log.warn("\"pointer_binds.{s}\" missing " ++ arg ++ " argument. Ignoring", .{@tagName(node_name)}), + InputConfigNodeName => log.warn("\"input.{s}\" missing " ++ arg ++ " argument. Ignoring", .{@tagName(node_name)}), else => @compileError("This function does not (yet) support type \"" ++ @typeName(node_name_type) ++ "\""), } } @@ -611,6 +770,22 @@ const Io = std.Io; const wayland = @import("wayland"); const river = wayland.client.river; +const AccelProfile = river.LibinputDeviceV1.AccelProfile; +const ClickfingerButtonMap = river.LibinputDeviceV1.ClickfingerButtonMap; +const ClickMethod = river.LibinputDeviceV1.ClickMethod; +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 SendEventsModes = river.LibinputDeviceV1.SendEventsModes; +const TapButtonMap = river.LibinputDeviceV1.TapButtonMap; +const TapState = river.LibinputDeviceV1.TapState; +const ThreeFingerDragState = river.LibinputDeviceV1.ThreeFingerDragState; const kdl = @import("kdl"); const known_folders = @import("known_folders"); diff --git a/src/Context.zig b/src/Context.zig index c9f9fdd..7f4531a 100644 --- a/src/Context.zig +++ b/src/Context.zig @@ -1,8 +1,6 @@ // SPDX-FileCopyrightText: 2026 Ben Buhse // // 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, @@ -110,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); @@ -130,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 { @@ -156,8 +170,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"); diff --git a/src/InputDevice.zig b/src/InputDevice.zig new file mode 100644 index 0000000..aa38520 --- /dev/null +++ b/src/InputDevice.zig @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2026 Ben Buhse +// +// 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 { + 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); +} + +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| input_device.type = ev.type, + .name => |ev| input_device.name = utils.allocator.dupe(u8, mem.span(ev.name)) catch @panic("Out of memory"), + } +} + +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); diff --git a/src/InputManager.zig b/src/InputManager.zig new file mode 100644 index 0000000..e1f2041 --- /dev/null +++ b/src/InputManager.zig @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: 2026 Ben Buhse +// +// 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); diff --git a/src/LibinputDevice.zig b/src/LibinputDevice.zig new file mode 100644 index 0000000..3048fdc --- /dev/null +++ b/src/LibinputDevice.zig @@ -0,0 +1,336 @@ +// SPDX-FileCopyrightText: 2026 Ben Buhse +// +// SPDX-License-Identifier: GPL-3.0-or-later + +const LibinputDevice = @This(); + +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, + +/// 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(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(); + 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) { + 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() }); + } + } + }, + .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)}); + }, + } +} + +pub fn manage(libinput_device: *LibinputDevice) void { + if (libinput_device.should_manage) { + if (libinput_device.input_device) |input_device| { + if (input_device.name) |_| { + 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; +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 InputDevice = @import("InputDevice.zig"); + +const log = std.log.scoped(.InputDevice); diff --git a/src/Output.zig b/src/Output.zig index 501a530..e4602b5 100644 --- a/src/Output.zig +++ b/src/Output.zig @@ -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 diff --git a/src/main.zig b/src/main.zig index 0ea7b46..5f322b1 100644 --- a/src/main.zig +++ b/src/main.zig @@ -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 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)) { @@ -212,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;