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;