lib/sys/src/evdev.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const builtin = @import("builtin");
3
4 pub const supported = builtin.os.tag == .linux;
5 pub const max_query_bytes: usize = std.math.maxInt(u14);
6
7 pub const Error = error{
8 UnsupportedPlatform,
9 AccessDenied,
10 OpenFailed,
11 QueryFailed,
12 ReadFailed,
13 };
14
15 pub const event_kind = struct {
16 pub const syn: u16 = 0x00;
17 pub const key: u16 = 0x01;
18 pub const abs: u16 = 0x03;
19 };
20
21 pub const button = struct {
22 pub const south: u16 = 0x130;
23 pub const east: u16 = 0x131;
24 pub const north: u16 = 0x133;
25 pub const west: u16 = 0x134;
26 pub const left_bumper: u16 = 0x136;
27 pub const right_bumper: u16 = 0x137;
28 pub const select: u16 = 0x13A;
29 pub const start: u16 = 0x13B;
30 pub const mode: u16 = 0x13C;
31 pub const left_thumb: u16 = 0x13D;
32 pub const right_thumb: u16 = 0x13E;
33 pub const dpad_up: u16 = 0x220;
34 pub const dpad_down: u16 = 0x221;
35 pub const dpad_left: u16 = 0x222;
36 pub const dpad_right: u16 = 0x223;
37 pub const gamepad_marker: u16 = south;
38 };
39
40 pub const axis = struct {
41 pub const x: u16 = 0x00;
42 pub const y: u16 = 0x01;
43 pub const z: u16 = 0x02;
44 pub const rx: u16 = 0x03;
45 pub const ry: u16 = 0x04;
46 pub const rz: u16 = 0x05;
47 pub const hat0x: u16 = 0x10;
48 pub const hat0y: u16 = 0x11;
49 };
50
51 pub const key_bit_count = 0x300;
52 pub const abs_bit_count = 0x40;
53 pub const KeyBits = [key_bit_count / 8]u8;
54 pub const AbsBits = [abs_bit_count / 8]u8;
55
56 pub const InputEvent = extern struct {
57 seconds: u64,
58 microseconds: u64,
59 kind: u16,
60 code: u16,
61 value: i32,
62 };
63
64 pub const AbsInfo = extern struct {
65 value: i32,
66 minimum: i32,
67 maximum: i32,
68 fuzz: i32,
69 flat: i32,
70 resolution: i32,
71 };
72
73 pub fn hasBit(bits: []const u8, index: u16) bool {
74 const byte = index / 8;
75 if (byte >= bits.len) return false;
76 return (bits[byte] >> @intCast(index % 8)) & 1 != 0;
77 }
78
79 pub fn isGamepad(key_bits: *const KeyBits) bool {
80 return hasBit(key_bits, button.gamepad_marker);
81 }
82
83 pub fn normalizeAxis(info: AbsInfo, value: i32) f32 {
84 if (info.maximum <= info.minimum) return 0;
85 const span: f32 = @floatFromInt(info.maximum - info.minimum);
86 const offset: f32 = @floatFromInt(value - info.minimum);
87 const unit = (offset / span) * 2.0 - 1.0;
88 return std.math.clamp(unit, -1.0, 1.0);
89 }
90
91 pub fn normalizeTrigger(info: AbsInfo, value: i32) f32 {
92 if (info.maximum <= info.minimum) return 0;
93 const span: f32 = @floatFromInt(info.maximum - info.minimum);
94 const offset: f32 = @floatFromInt(value - info.minimum);
95 return std.math.clamp(offset / span, 0.0, 1.0);
96 }
97
98 pub fn eventPath(buffer: []u8, index: u32) ![]const u8 {
99 return std.fmt.bufPrint(buffer, "/dev/input/event{d}", .{index});
100 }
101
102 const linux = std.os.linux;
103
104 fn ioctlRead(fd: i32, nr: u8, out: []u8) Error!void {
105 if (comptime !supported) return error.UnsupportedPlatform;
106 if (out.len > max_query_bytes) return error.QueryFailed;
107 const request: u32 = (2 << 30) | (@as(u32, @intCast(out.len)) << 16) | (@as(u32, 'E') << 8) | nr;
108 const rc = linux.ioctl(fd, request, @intFromPtr(out.ptr));
109 if (linux.errno(rc) != .SUCCESS) return error.QueryFailed;
110 }
111
112 pub const Device = struct {
113 fd: i32,
114
115 pub fn open(path: []const u8) Error!Device {
116 if (comptime !supported) return error.UnsupportedPlatform;
117 var path_buffer: [128]u8 = undefined;
118 if (path.len >= path_buffer.len) return error.OpenFailed;
119 @memcpy(path_buffer[0..path.len], path);
120 path_buffer[path.len] = 0;
121 const rc = linux.open(
122 path_buffer[0..path.len :0],
123 .{ .ACCMODE = .RDONLY, .NONBLOCK = true, .CLOEXEC = true },
124 0,
125 );
126 return switch (linux.errno(rc)) {
127 .SUCCESS => .{ .fd = @intCast(rc) },
128 .ACCES, .PERM => error.AccessDenied,
129 else => error.OpenFailed,
130 };
131 }
132
133 pub fn close(self: Device) void {
134 if (comptime !supported) return;
135 _ = linux.close(self.fd);
136 }
137
138 pub fn name(self: Device, buffer: []u8) Error![]const u8 {
139 try ioctlRead(self.fd, 0x06, buffer);
140 const len = std.mem.indexOfScalar(u8, buffer, 0) orelse buffer.len;
141 return buffer[0..len];
142 }
143
144 pub fn keyBits(self: Device, bits: *KeyBits) Error!void {
145 @memset(bits, 0);
146 try ioctlRead(self.fd, 0x20 + @as(u8, @intCast(event_kind.key)), bits);
147 }
148
149 pub fn absBits(self: Device, bits: *AbsBits) Error!void {
150 @memset(bits, 0);
151 try ioctlRead(self.fd, 0x20 + @as(u8, @intCast(event_kind.abs)), bits);
152 }
153
154 pub fn absInfo(self: Device, code: u16) Error!AbsInfo {
155 var info: AbsInfo = undefined;
156 try ioctlRead(self.fd, 0x40 + @as(u8, @intCast(code)), std.mem.asBytes(&info));
157 return info;
158 }
159
160 pub fn readEvents(self: Device, events: []InputEvent) Error!usize {
161 if (comptime !supported) return error.UnsupportedPlatform;
162 if (events.len == 0) return 0;
163 const rc = linux.read(self.fd, @ptrCast(events.ptr), events.len * @sizeOf(InputEvent));
164 return switch (linux.errno(rc)) {
165 .SUCCESS => @as(usize, @intCast(rc)) / @sizeOf(InputEvent),
166 .AGAIN => 0,
167 else => error.ReadFailed,
168 };
169 }
170 };
171
172 test "bit probing reads packed capability masks" {
173 var bits: KeyBits = @splat(0);
174 bits[button.gamepad_marker / 8] = 1 << (button.gamepad_marker % 8);
175 try std.testing.expect(hasBit(&bits, button.gamepad_marker));
176 try std.testing.expect(isGamepad(&bits));
177 try std.testing.expect(!hasBit(&bits, button.start));
178
179 const empty: KeyBits = @splat(0);
180 try std.testing.expect(!isGamepad(&empty));
181 try std.testing.expect(!hasBit(empty[0..1], 800));
182 }
183
184 test "axis normalization spans the device range" {
185 const info = AbsInfo{ .value = 0, .minimum = -32768, .maximum = 32767, .fuzz = 0, .flat = 0, .resolution = 0 };
186 try std.testing.expectApproxEqAbs(@as(f32, -1.0), normalizeAxis(info, -32768), 0.001);
187 try std.testing.expectApproxEqAbs(@as(f32, 1.0), normalizeAxis(info, 32767), 0.001);
188 try std.testing.expectApproxEqAbs(@as(f32, 0.0), normalizeAxis(info, 0), 0.01);
189
190 const trigger = AbsInfo{ .value = 0, .minimum = 0, .maximum = 255, .fuzz = 0, .flat = 0, .resolution = 0 };
191 try std.testing.expectApproxEqAbs(@as(f32, 0.0), normalizeTrigger(trigger, 0), 0.001);
192 try std.testing.expectApproxEqAbs(@as(f32, 1.0), normalizeTrigger(trigger, 255), 0.001);
193 try std.testing.expectApproxEqAbs(@as(f32, 0.5), normalizeTrigger(trigger, 128), 0.01);
194
195 const degenerate = AbsInfo{ .value = 0, .minimum = 5, .maximum = 5, .fuzz = 0, .flat = 0, .resolution = 0 };
196 try std.testing.expectEqual(@as(f32, 0), normalizeAxis(degenerate, 5));
197 }
198
199 test "input event layout matches the kernel wire size" {
200 try std.testing.expectEqual(@as(usize, 24), @sizeOf(InputEvent));
201 var buffer: [32]u8 = undefined;
202 try std.testing.expectEqualStrings("/dev/input/event7", try eventPath(&buffer, 7));
203 }
204
205 test "query buffers reject lengths outside the ioctl encoding" {
206 if (comptime !supported) return error.SkipZigTest;
207 var oversized: [max_query_bytes + 1]u8 = undefined;
208 try std.testing.expectError(error.QueryFailed, ioctlRead(-1, 0, &oversized));
209 }