git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commit57c3200cac320b4606fe07bbf23d77f564bfe10b
parent3278f657e8
authorIsaac Freund <[email protected]>
date2024-12-29 12:54
river: implement river_pointer_binding_v1

 protocol/river-window-management-v1.xml |   2 +-
 river/Config.zig                        |  17 ----
 river/Cursor.zig                        | 143 +++++++++++++++++--------------
 river/PointerBinding.zig                | 146 ++++++++++++++++++++++++++++++++
 river/Seat.zig                          |  73 +++++++++++++++-
 rivercompat/PointerBinding.zig          |  58 +++++++++++++
 rivercompat/Seat.zig                    |   6 +-
 rivercompat/c.zig                       |   3 +
 8 files changed, 364 insertions(+), 84 deletions(-)

diff --git a/protocol/river-window-management-v1.xml b/protocol/river-window-management-v1.xml
index 453880f..b503882 100644
--- a/protocol/river-window-management-v1.xml
+++ b/protocol/river-window-management-v1.xml
@@ -1199,7 +1199,7 @@
         The button argument is a Linux input event code defined in the
         linux/input-event-codes.h header file (e.g. BTN_RIGHT).
 
-        The new key binding is not enabled until initial configuration is
+        The new pointer binding is not enabled until initial configuration is
         completed, the enable request is made, and the change is committed with
         a river_window_manager_v1.commit request.
       </description>
diff --git a/river/Config.zig b/river/Config.zig
index a36b73e..438cc9d 100644
--- a/river/Config.zig
+++ b/river/Config.zig
@@ -19,7 +19,6 @@ const Config = @This();
 const std = @import("std");
 const fmt = std.fmt;
 const mem = std.mem;
-const globber = @import("globber");
 const wlr = @import("wlroots");
 const xkb = @import("xkbcommon");
 
@@ -27,19 +26,7 @@ const server = &@import("main.zig").server;
 const util = @import("util.zig");
 
 const Server = @import("Server.zig");
-const Output = @import("Output.zig");
 const Switch = @import("Switch.zig");
-const Window = @import("Window.zig");
-
-pub const Position = struct {
-    x: u31,
-    y: u31,
-};
-
-pub const Dimensions = struct {
-    width: u31,
-    height: u31,
-};
 
 /// Color of background in RGBA with premultiplied alpha (alpha should only affect nested sessions)
 background_color: [4]f32 = [_]f32{ 0.0, 0.16862745, 0.21176471, 1.0 }, // Solarized base03
@@ -50,10 +37,6 @@ border_width: u31 = 2,
 /// Color of border in RGBA with premultiplied alpha
 border_color: [4]f32 = [_]f32{ 0.34509804, 0.43137255, 0.45882353, 1.0 }, // Solarized base01
 
-pointer_mappings: std.ArrayListUnmanaged(struct {
-    event_code: u32,
-    modifiers: wlr.Keyboard.ModifierMask,
-}) = .{},
 switch_mappings: std.ArrayListUnmanaged(struct {
     switch_type: Switch.Type,
     switch_state: Switch.State,
diff --git a/river/Cursor.zig b/river/Cursor.zig
index 6405ab4..057f4dd 100644
--- a/river/Cursor.zig
+++ b/river/Cursor.zig
@@ -35,6 +35,7 @@ const DragIcon = @import("DragIcon.zig");
 const InputDevice = @import("InputDevice.zig");
 const LockSurface = @import("LockSurface.zig");
 const Output = @import("Output.zig");
+const PointerBinding = @import("PointerBinding.zig");
 const PointerConstraint = @import("PointerConstraint.zig");
 const Scene = @import("Scene.zig");
 const Seat = @import("Seat.zig");
@@ -46,7 +47,10 @@ const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig");
 const log = std.log.scoped(.input);
 
 const Mode = union(enum) {
-    passthrough: void,
+    passthrough,
+    /// This mode is entered when a binding is triggered and exited when there
+    /// are no longer any buttons pressed.
+    ignore,
     down: struct {
         // TODO: To handle the surface with pointer focus being moved during
         // down mode we need to store the starting location of the surface as
@@ -127,8 +131,8 @@ xcursor_manager: *wlr.XcursorManager,
 /// surface to be used as the cursor shape instead.
 xcursor_name: ?[*:0]const u8 = null,
 
-/// Number of distinct buttons currently pressed
-pressed_count: u32 = 0,
+/// The set of currently pressed pointer buttons and the corresponding pointer mapping if any.
+pressed: std.AutoHashMapUnmanaged(u32, ?*PointerBinding) = .{},
 
 /// The pointer constraint for the surface that currently has keyboard focus, if any.
 /// This constraint is not necessarily active, activation only occurs once the cursor
@@ -233,6 +237,8 @@ pub fn init(cursor: *Cursor, seat: *Seat) !void {
 pub fn deinit(cursor: *Cursor) void {
     cursor.xcursor_manager.destroy();
     cursor.wlr_cursor.destroy();
+    cursor.pressed.deinit(util.gpa);
+    cursor.touch_points.deinit(util.gpa);
 }
 
 /// Set the cursor theme for the given seat, as well as the xwayland theme if
@@ -332,13 +338,14 @@ pub fn processMotionRelative(cursor: *Cursor, event: *const wlr.Pointer.event.Mo
     }
 
     switch (cursor.mode) {
-        .passthrough, .down => {
+        .passthrough, .ignore, .down => {
             cursor.wlr_cursor.move(event.device, dx, dy);
 
             switch (cursor.mode) {
                 .passthrough => {
                     cursor.passthrough(event.time_msec);
                 },
+                .ignore => {},
                 .down => |data| {
                     cursor.seat.wlr_seat.pointerNotifyMotion(
                         event.time_msec,
@@ -438,57 +445,84 @@ pub fn processMotionAbsolute(cursor: *Cursor, event: *const wlr.Pointer.event.Mo
 }
 
 pub fn processButton(cursor: *Cursor, event: *const wlr.Pointer.event.Button) void {
-    if (event.state == .released) {
-        assert(cursor.pressed_count > 0);
-        cursor.pressed_count -= 1;
-        if (cursor.pressed_count == 0 and cursor.mode != .passthrough) {
-            log.debug("leaving {s} mode", .{@tagName(cursor.mode)});
+    if (event.state == .pressed) {
+        const result = cursor.pressed.getOrPut(util.gpa, event.button) catch {
+            log.err("out of memory", .{});
+            return;
+        };
+        if (result.found_existing) {
+            log.err("ignoring duplicate pointer button {d} press", .{event.button});
+            return;
+        }
+
+        if (cursor.seat.matchPointerBinding(event.button)) |binding| {
+            result.value_ptr.* = binding;
+            binding.pressed();
+            log.debug("entering cursor mode ignore", .{});
+            cursor.mode = .ignore;
+            cursor.clearFocus();
+            return;
+        }
+
+        result.value_ptr.* = null;
+
+        switch (cursor.mode) {
+            .passthrough => {
+                if (server.scene.at(cursor.wlr_cursor.x, cursor.wlr_cursor.y)) |at| {
+                    cursor.interact(at);
+
+                    if (at.surface != null) {
+                        _ = cursor.seat.wlr_seat.pointerNotifyButton(event.time_msec, event.button, event.state);
+                        log.debug("entering cursor mode down", .{});
+                        cursor.mode = .{
+                            .down = .{
+                                .lx = cursor.wlr_cursor.x,
+                                .ly = cursor.wlr_cursor.y,
+                                .sx = at.sx,
+                                .sy = at.sy,
+                            },
+                        };
+                        return;
+                    }
+                }
+
+                log.debug("entering cursor mode ignore", .{});
+                cursor.mode = .ignore;
+                cursor.clearFocus();
+                return;
+            },
+            // Pointer focus does not change while in down mode.
+            .down => {
+                _ = cursor.seat.wlr_seat.pointerNotifyButton(event.time_msec, event.button, event.state);
+            },
+            // No client has pointer focus while in ignore/move/resize mode.
+            .ignore, .move, .resize => {},
+        }
+    } else {
+        assert(event.state == .released);
+        const result = cursor.pressed.fetchRemove(event.button);
+        if (result) |kv| {
+            if (kv.value) |binding| {
+                binding.released();
+            }
 
             switch (cursor.mode) {
                 .passthrough => unreachable,
                 .down => {
-                    // If we were in down mode, we need pass along the release event
                     _ = cursor.seat.wlr_seat.pointerNotifyButton(event.time_msec, event.button, event.state);
                 },
-                .move, .resize => {},
+                // No client has pointer focus while in ignore/move/resize mode.
+                .ignore, .move, .resize => {},
+            }
+            if (cursor.pressed.count() == 0) {
+                log.debug("exiting cursor mode {s}", .{@tagName(cursor.mode)});
+                cursor.mode = .passthrough;
+                cursor.passthrough(event.time_msec);
             }
-
-            cursor.mode = .passthrough;
-            cursor.passthrough(event.time_msec);
         } else {
-            _ = cursor.seat.wlr_seat.pointerNotifyButton(event.time_msec, event.button, event.state);
-        }
-        return;
-    }
-
-    assert(event.state == .pressed);
-    cursor.pressed_count += 1;
-
-    if (cursor.pressed_count > 1) {
-        _ = cursor.seat.wlr_seat.pointerNotifyButton(event.time_msec, event.button, event.state);
-        return;
-    }
-
-    if (server.scene.at(cursor.wlr_cursor.x, cursor.wlr_cursor.y)) |result| {
-        if (result.data == .window and cursor.handlePointerMapping(event, result.data.window)) {
-            // If a mapping is triggered don't send events to clients.
+            log.err("ignoring duplicate pointer button {d} release", .{event.button});
             return;
         }
-
-        cursor.interact(result);
-
-        _ = cursor.seat.wlr_seat.pointerNotifyButton(event.time_msec, event.button, event.state);
-
-        if (result.surface != null) {
-            cursor.mode = .{
-                .down = .{
-                    .lx = cursor.wlr_cursor.x,
-                    .ly = cursor.wlr_cursor.y,
-                    .sx = result.sx,
-                    .sy = result.sy,
-                },
-            };
-        }
     }
 }
 
@@ -673,21 +707,6 @@ fn handleTabletToolButton(
     tool.button(tablet, event);
 }
 
-/// Handle the mapping for the passed button if any. Returns true if there
-/// was a mapping and the button was handled.
-fn handlePointerMapping(cursor: *Cursor, event: *const wlr.Pointer.event.Button, _: *Window) bool {
-    const wlr_keyboard = cursor.seat.wlr_seat.getKeyboard() orelse return false;
-    // XXX this is not ok, we need to store current modifiers per-Keyboard ourselves
-    const modifiers = wlr_keyboard.getModifiers();
-
-    return for (server.config.pointer_mappings.items) |mapping| {
-        if (event.button == mapping.event_code and std.meta.eql(modifiers, mapping.modifiers)) {
-            // trigger action
-            break true;
-        }
-    } else false;
-}
-
 pub fn startMove(cursor: *Cursor, window: *Window) void {
     if (cursor.constraint) |constraint| {
         if (constraint.state == .active) constraint.deactivate();
@@ -797,7 +816,7 @@ pub fn updateState(cursor: *Cursor) void {
             cursor.passthrough(msec);
         },
         // TODO: Leave down mode if the target surface is no longer visible.
-        .down => {},
+        .ignore, .down => {},
         .move, .resize => {
             // Moving and resizing of windows is handled through the transaction system. Therefore,
             // we must inspect the inflight_mode instead if a move or a resize is in progress.
@@ -814,7 +833,7 @@ pub fn updateState(cursor: *Cursor) void {
             // Therefore, the user already expects the cursor to be free from the window and
             // we should not warp it back to the fixed offset of the move/resize.
             switch (cursor.inflight_mode) {
-                .passthrough, .down => {},
+                .passthrough, .ignore, .down => {},
                 inline .move, .resize => |data, mode| {
 
                     // These conditions are checked in WindowManager.dirtyPending()
diff --git a/river/PointerBinding.zig b/river/PointerBinding.zig
new file mode 100644
index 0000000..b7ee0fe
--- /dev/null
+++ b/river/PointerBinding.zig
@@ -0,0 +1,146 @@
+// This file is part of river, a dynamic tiling wayland compositor.
+//
+// Copyright 2020-2024 The River Developers
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+const PointerBinding = @This();
+
+const std = @import("std");
+const assert = std.debug.assert;
+const wlr = @import("wlroots");
+const wayland = @import("wayland");
+const wl = wayland.server.wl;
+const river = wayland.server.river;
+
+const c = @import("c.zig");
+const server = &@import("main.zig").server;
+const util = @import("util.zig");
+
+const Seat = @import("Seat.zig");
+
+const log = std.log.scoped(.input);
+
+const WmState = struct {
+    enabled: bool = false,
+};
+
+seat: *Seat,
+object: *river.PointerBindingV1,
+
+button: u32,
+modifiers: river.SeatV1.Modifiers,
+
+pending: struct {
+    state_change: enum {
+        none,
+        pressed,
+        released,
+    } = .none,
+} = .{},
+uncommitted: WmState = .{},
+committed: WmState = .{},
+
+/// This bit of state is used to ensure that multiple simultaneous
+/// presses across multiple keyboards do not cause multiple press
+/// events to be sent to the window manager.
+sent_pressed: bool = false,
+
+/// Seat.pointer_bindings
+link: wl.list.Link,
+
+pub fn create(
+    seat: *Seat,
+    client: *wl.Client,
+    version: u32,
+    id: u32,
+    button: u32,
+    modifiers: river.SeatV1.Modifiers,
+) !void {
+    const binding = try util.gpa.create(PointerBinding);
+    errdefer util.gpa.destroy(binding);
+
+    const pointer_binding_v1 = try river.PointerBindingV1.create(client, version, id);
+    errdefer comptime unreachable;
+
+    log.debug("new river_pointer_binding_v1: button: {d}({?s}) modifiers: {d}", .{
+        button,
+        c.libevdev_event_code_get_name(c.EV_KEY, button),
+        @as(u32, @bitCast(modifiers)),
+    });
+
+    binding.* = .{
+        .seat = seat,
+        .object = pointer_binding_v1,
+        .button = button,
+        .modifiers = modifiers,
+        .link = undefined,
+    };
+    pointer_binding_v1.setHandler(*PointerBinding, handleRequest, handleDestroy, binding);
+
+    seat.pointer_bindings.append(binding);
+}
+
+fn handleDestroy(_: *river.PointerBindingV1, binding: *PointerBinding) void {
+    if (binding.seat.cursor.pressed.getPtr(binding.button)) |value_ptr| {
+        // It is possible for the window manager to create duplicate pointer bindings.
+        if (value_ptr.* == binding) {
+            value_ptr.* = null;
+        }
+    }
+
+    binding.link.remove();
+    util.gpa.destroy(binding);
+}
+
+fn handleRequest(
+    pointer_binding_v1: *river.PointerBindingV1,
+    request: river.PointerBindingV1.Request,
+    binding: *PointerBinding,
+) void {
+    assert(binding.object == pointer_binding_v1);
+    switch (request) {
+        .destroy => pointer_binding_v1.destroy(),
+        .enable => binding.uncommitted.enabled = true,
+        .disable => binding.uncommitted.enabled = false,
+    }
+}
+
+pub fn pressed(binding: *PointerBinding) void {
+    assert(!binding.sent_pressed);
+    // Input event processing should not continue after a press/release event
+    // until that event is sent to the window manager in an update and acked.
+    assert(binding.pending.state_change == .none);
+    binding.pending.state_change = .pressed;
+    server.wm.dirtyPending();
+}
+
+pub fn released(binding: *PointerBinding) void {
+    assert(binding.sent_pressed);
+    // Input event processing should not continue after a press/release event
+    // until that event is sent to the window manager in an update and acked.
+    assert(binding.pending.state_change == .none);
+    binding.pending.state_change = .released;
+    server.wm.dirtyPending();
+}
+
+pub fn match(
+    binding: *const PointerBinding,
+    button: u32,
+    modifiers: wlr.Keyboard.ModifierMask,
+) bool {
+    if (!binding.committed.enabled) return false;
+
+    return button == binding.button and
+        @as(u32, @bitCast(modifiers)) == @as(u32, @bitCast(binding.modifiers));
+}
diff --git a/river/Seat.zig b/river/Seat.zig
index 82c9c8f..643cd6f 100644
--- a/river/Seat.zig
+++ b/river/Seat.zig
@@ -37,6 +37,7 @@ const Keyboard = @import("Keyboard.zig");
 const KeyboardGroup = @import("KeyboardGroup.zig");
 const LockSurface = @import("LockSurface.zig");
 const Output = @import("Output.zig");
+const PointerBinding = @import("PointerBinding.zig");
 const PointerConstraint = @import("PointerConstraint.zig");
 const Switch = @import("Switch.zig");
 const Tablet = @import("Tablet.zig");
@@ -137,6 +138,7 @@ uncommitted: WmState = .{},
 committed: WmState = .{},
 
 xkb_bindings: wl.list.Head(XkbBinding, .link),
+pointer_bindings: wl.list.Head(PointerBinding, .link),
 
 /// Multiple physical mice are handled by the same Cursor
 cursor: Cursor,
@@ -174,6 +176,7 @@ pub fn create(name: [*:0]const u8) !void {
         .link_pending = undefined,
         .link_sent = undefined,
         .xkb_bindings = undefined,
+        .pointer_bindings = undefined,
         .cursor = undefined,
         .relay = undefined,
     };
@@ -185,6 +188,7 @@ pub fn create(name: [*:0]const u8) !void {
     server.wm.dirtyPending();
 
     seat.xkb_bindings.init();
+    seat.pointer_bindings.init();
 
     try seat.cursor.init(seat);
     seat.relay.init();
@@ -362,6 +366,25 @@ pub fn sendDirty(seat: *Seat) void {
                 binding.pending.state_change = .none;
             }
         }
+        {
+            var it = seat.pointer_bindings.iterator(.forward);
+            while (it.next()) |binding| {
+                switch (binding.pending.state_change) {
+                    .none => {},
+                    .pressed => {
+                        assert(!binding.sent_pressed);
+                        binding.sent_pressed = true;
+                        binding.object.sendPressed();
+                    },
+                    .released => {
+                        assert(binding.sent_pressed);
+                        binding.sent_pressed = false;
+                        binding.object.sendReleased();
+                    },
+                }
+                binding.pending.state_change = .none;
+            }
+        }
     }
 }
 
@@ -413,7 +436,20 @@ fn handleRequest(
                 return;
             };
         },
-        .get_pointer_binding => {},
+        .get_pointer_binding => |args| {
+            PointerBinding.create(
+                seat,
+                seat_v1.getClient(),
+                seat_v1.getVersion(),
+                args.id,
+                args.button,
+                args.modifiers,
+            ) catch {
+                seat_v1.getClient().postNoMemory();
+                log.err("out of memory", .{});
+                return;
+            };
+        },
     }
 }
 
@@ -424,6 +460,12 @@ pub fn commitWmState(seat: *Seat) void {
             binding.committed = binding.uncommitted;
         }
     }
+    {
+        var it = seat.pointer_bindings.iterator(.forward);
+        while (it.next()) |binding| {
+            binding.committed = binding.uncommitted;
+        }
+    }
 
     seat.committed = seat.uncommitted;
 }
@@ -546,7 +588,7 @@ pub fn matchXkbBinding(
                 if (found == null) {
                     found = binding;
                 } else {
-                    log.debug("already found a matching binding, ignoring additional match", .{});
+                    log.debug("already found a matching xkb_binding, ignoring additional match", .{});
                 }
             }
         }
@@ -563,7 +605,32 @@ pub fn matchXkbBinding(
                 if (found == null) {
                     found = binding;
                 } else {
-                    log.debug("already found a matching binding, ignoring additional match", .{});
+                    log.debug("already found a matching xkb_binding, ignoring additional match", .{});
+                }
+            }
+        }
+    }
+
+    return found;
+}
+
+pub fn matchPointerBinding(
+    seat: *Seat,
+    button: u32,
+) ?*PointerBinding {
+    const wlr_keyboard = seat.wlr_seat.getKeyboard() orelse return null;
+    // XXX this is not ok, we need to store current modifiers per-Keyboard ourselves
+    const modifiers = wlr_keyboard.getModifiers();
+
+    var found: ?*PointerBinding = null;
+    {
+        var it = seat.pointer_bindings.iterator(.forward);
+        while (it.next()) |binding| {
+            if (binding.match(button, modifiers)) {
+                if (found == null) {
+                    found = binding;
+                } else {
+                    log.debug("already found a matching pointer binding, ignoring additional match", .{});
                 }
             }
         }
diff --git a/rivercompat/PointerBinding.zig b/rivercompat/PointerBinding.zig
new file mode 100644
index 0000000..5cdf0fb
--- /dev/null
+++ b/rivercompat/PointerBinding.zig
@@ -0,0 +1,58 @@
+// This file is part of river, a dynamic tiling wayland compositor.
+//
+// Copyright 2024 The River Developers
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+const PointerBinding = @This();
+
+const std = @import("std");
+const assert = std.debug.assert;
+const wayland = @import("wayland");
+const wl = wayland.client.wl;
+const river = wayland.client.river;
+
+const Seat = @import("Seat.zig");
+const Window = @import("Window.zig");
+
+const gpa = std.heap.c_allocator;
+
+seat: *Seat,
+pointer_binding_v1: *river.PointerBindingV1,
+
+pub fn create(
+    seat: *Seat,
+    button: u32,
+    modifiers: river.SeatV1.Modifiers,
+) void {
+    const pointer_binding_v1 = seat.seat_v1.getPointerBinding(button, modifiers) catch @panic("OOM");
+    const binding = gpa.create(PointerBinding) catch @panic("OOM");
+    binding.* = .{
+        .seat = seat,
+        .pointer_binding_v1 = pointer_binding_v1,
+    };
+    pointer_binding_v1.setListener(*PointerBinding, handleEvent, binding);
+    pointer_binding_v1.enable();
+}
+
+fn handleEvent(pointer_binding_v1: *river.PointerBindingV1, event: river.PointerBindingV1.Event, binding: *PointerBinding) void {
+    assert(binding.pointer_binding_v1 == pointer_binding_v1);
+    switch (event) {
+        .pressed => {
+            if (binding.seat.focused) |window| {
+                window.window_v1.close();
+            }
+        },
+        .released => {},
+    }
+}
diff --git a/rivercompat/Seat.zig b/rivercompat/Seat.zig
index 6bf1b25..7b04b7e 100644
--- a/rivercompat/Seat.zig
+++ b/rivercompat/Seat.zig
@@ -23,9 +23,12 @@ const xkb = @import("xkbcommon");
 const wl = wayland.client.wl;
 const river = wayland.client.river;
 
+const c = @import("c.zig");
+
 const Window = @import("Window.zig");
 const WindowManager = @import("WindowManager.zig");
 const XkbBinding = @import("XkbBinding.zig");
+const PointerBinding = @import("PointerBinding.zig");
 
 const gpa = std.heap.c_allocator;
 
@@ -41,7 +44,8 @@ pub fn create(wm: *WindowManager, seat_v1: *river.SeatV1) void {
     };
     seat_v1.setListener(*Seat, handleEvent, seat);
 
-    XkbBinding.create(seat, xkb.Keysym.n, .{ .mod1 = true });
+    XkbBinding.create(seat, xkb.Keysym.n, .{ .mod4 = true });
+    PointerBinding.create(seat, c.BTN_RIGHT, .{ .mod4 = true });
 }
 
 pub fn focus(seat: *Seat, target: ?*Window) void {
diff --git a/rivercompat/c.zig b/rivercompat/c.zig
new file mode 100644
index 0000000..38eb7c2
--- /dev/null
+++ b/rivercompat/c.zig
@@ -0,0 +1,3 @@
+pub usingnamespace @cImport({
+    @cInclude("linux/input-event-codes.h");
+});