drmkit_input/seat.rs
1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! The devices on a seat, through libinput.
5
6use std::os::fd::{AsFd, BorrowedFd};
7use std::path::Path;
8
9use input::event::keyboard::{KeyState, KeyboardEventTrait as _};
10use input::event::pointer::{Axis, ButtonState, PointerEventTrait, PointerScrollEvent};
11use input::event::switch::{Switch as LibinputSwitch, SwitchEventTrait as _, SwitchState};
12use input::event::touch::{TouchEventPosition as _, TouchEventSlot as _, TouchEventTrait as _};
13use input::event::{
14 DeviceEvent, EventTrait, KeyboardEvent as LibinputKeyboard, PointerEvent as LibinputPointer,
15 SwitchEvent as LibinputSwitchEvent, TouchEvent as LibinputTouch,
16};
17use input::{AsRaw as _, Device, DeviceCapability, Led, Libinput, LibinputInterface};
18
19use crate::InputError;
20use crate::event::{
21 InputEvent, KeyEvent, PointerAbsolute, PointerAxis, PointerButton, PointerEvent, PointerMotion,
22 ScrollSource, Switch, SwitchEvent, TouchEvent, TouchPhase,
23};
24use crate::leds::Leds;
25use crate::libinput_log::{self, LogPriority};
26
27/// What to open.
28#[derive(Debug, Clone)]
29#[allow(
30 clippy::exhaustive_structs,
31 reason = "built by callers with `..Default::default()`, which a new field does not break"
32)]
33pub struct SeatOptions {
34 /// The seat's udev name. `seat0` unless the machine has more than one.
35 ///
36 /// A name udev has never heard of is not an error: libinput asks for the
37 /// devices tagged with it, gets none, and carries on. So a typo here and
38 /// a machine with no input devices look the same from the outside --
39 /// [`keyboard_count`](Seat::keyboard_count) after the first dispatch is
40 /// how to tell.
41 pub seat: String,
42 /// How much libinput should say about itself.
43 ///
44 /// Whatever passes this is forwarded into [`drmkit_log`] tagged
45 /// `[libinput]`, where the process-wide sink and level apply on top --
46 /// so a message has to pass both. The default matches libinput's own, so
47 /// leaving it alone costs nothing.
48 pub log_priority: LogPriority,
49}
50
51impl Default for SeatOptions {
52 fn default() -> Self {
53 Self {
54 seat: "seat0".to_owned(),
55 log_priority: LogPriority::default(),
56 }
57 }
58}
59
60/// Every input device on one seat.
61///
62/// Poll [`AsFd`], call [`dispatch`](Self::dispatch) when it is readable, and
63/// act on what comes back. Nothing is delivered through a callback: an event
64/// loop that has to reason about what a handler might re-enter is harder than
65/// one that gets a list.
66#[derive(Debug)]
67pub struct Seat {
68 libinput: Libinput,
69 /// The keyboard-capable devices, held so their lights can be driven.
70 keyboards: Vec<Device>,
71 /// The last state pushed, re-applied to anything that turns up later.
72 ///
73 /// A keyboard plugged in mid-session, or every device coming back after a
74 /// VT switch, would otherwise come up dark while xkb says Caps Lock is on.
75 leds: Option<Leds>,
76}
77
78impl Seat {
79 /// Open the seat, opening its devices directly.
80 ///
81 /// Needs read access to `/dev/input/event*`, which outside a login
82 /// session means root or the `input` group. For devices that survive a VT
83 /// switch, use [`Seat::open_with`] and route through a session instead.
84 ///
85 /// Everything typed on this machine comes through here, including into
86 /// other applications: an evdev reader is a keylogger that happens to be
87 /// asked politely. That is inherent to the interface -- it is why the
88 /// permission is restricted -- and it is worth being deliberate about
89 /// where the events go.
90 ///
91 /// # Errors
92 ///
93 /// [`InputError::Seat`] if udev has no such seat, or if there is no
94 /// permission to enumerate it.
95 pub fn open(options: &SeatOptions) -> Result<Self, InputError> {
96 Self::open_with(options, Direct)
97 }
98
99 /// Open the seat, opening its devices through `opener`.
100 ///
101 /// The hook libinput calls for every privileged open and its matching
102 /// close. Implement [`LibinputInterface`] over a
103 /// `drmkit_session::Session` and every `/dev/input/event*` libinput
104 /// touches gets the same revocable lifetime as the DRM device -- which is
105 /// what makes a VT switch give the keyboard back.
106 ///
107 /// # Errors
108 ///
109 /// As [`Seat::open`].
110 pub fn open_with(
111 options: &SeatOptions,
112 opener: impl LibinputInterface + 'static,
113 ) -> Result<Self, InputError> {
114 let mut libinput = Libinput::new_with_udev(opener);
115
116 // Before assign_seat, which is what enumerates the seat: a handler
117 // installed after it would miss every device-added diagnostic, which
118 // is the set a consumer most wants when nothing is responding.
119 //
120 // SAFETY: the context is live and owned here, and `install` only
121 // stores a function pointer against it.
122 unsafe {
123 libinput_log::install(libinput.as_raw().cast_mut().cast(), options.log_priority);
124 }
125
126 libinput
127 .udev_assign_seat(&options.seat)
128 .map_err(|()| InputError::Seat(options.seat.clone()))?;
129
130 Ok(Self {
131 libinput,
132 keyboards: Vec::new(),
133 leds: None,
134 })
135 }
136
137 /// Read whatever the devices have to say, appending to `out`.
138 ///
139 /// Device arrivals and departures are handled here and not reported: they
140 /// are this type's bookkeeping, and a consumer that wants to know which
141 /// devices exist has udev for it.
142 ///
143 /// # Errors
144 ///
145 /// [`InputError::Dispatch`] if libinput cannot read its descriptor.
146 pub fn dispatch(&mut self, out: &mut Vec<InputEvent>) -> Result<(), InputError> {
147 self.libinput.dispatch().map_err(InputError::Dispatch)?;
148 while let Some(event) = self.libinput.next() {
149 match event {
150 input::Event::Device(event) => self.on_device(event),
151 other => out.extend(convert(other)),
152 }
153 }
154 Ok(())
155 }
156
157 /// Close every device, keeping the seat.
158 ///
159 /// For a session pause. The descriptors are about to stop working, and
160 /// libinput reading a revoked one produces a stream of errors rather than
161 /// events.
162 pub fn suspend(&mut self) {
163 self.libinput.suspend();
164 // Held references keep the devices alive past the suspend, and the
165 // resume re-adds every one of them -- so letting go here is what stops
166 // the list doubling on the second switch.
167 self.keyboards.clear();
168 }
169
170 /// Reopen everything after a [`suspend`](Self::suspend).
171 ///
172 /// Devices arrive as ordinary additions on the next
173 /// [`dispatch`](Self::dispatch), and the lights are restored with them.
174 ///
175 /// # Errors
176 ///
177 /// [`InputError::Seat`] if libinput cannot re-enumerate.
178 pub fn resume(&mut self) -> Result<(), InputError> {
179 self.libinput
180 .resume()
181 .map_err(|()| InputError::Seat("resume".to_owned()))
182 }
183
184 /// Drive the lock lights on every keyboard.
185 ///
186 /// xkb owns the latch and the device owns the light; this is the wire
187 /// between them. Read [`Keyboard::leds`](crate::Keyboard::leds) after a
188 /// key and push it here when it has changed.
189 pub fn set_leds(&mut self, leds: Leds) {
190 self.leds = Some(leds);
191 let mask = mask(leds);
192 for keyboard in &mut self.keyboards {
193 keyboard.led_update(mask);
194 }
195 }
196
197 /// How many keyboard-capable devices are on the seat.
198 #[must_use]
199 pub fn keyboard_count(&self) -> usize {
200 self.keyboards.len()
201 }
202
203 fn on_device(&mut self, event: DeviceEvent) {
204 match event {
205 DeviceEvent::Added(added) => {
206 let mut device = added.device();
207 if !device.has_capability(DeviceCapability::Keyboard) {
208 return;
209 }
210 if let Some(leds) = self.leds {
211 // A keyboard that turns up mid-session, or every one of
212 // them after a resume, otherwise comes up dark while xkb
213 // goes on saying Caps Lock is held.
214 device.led_update(mask(leds));
215 }
216 self.keyboards.push(device);
217 }
218 DeviceEvent::Removed(removed) => {
219 let device = removed.device();
220 self.keyboards.retain(|held| *held != device);
221 }
222 _ => {}
223 }
224 }
225}
226
227impl AsFd for Seat {
228 fn as_fd(&self) -> BorrowedFd<'_> {
229 self.libinput.as_fd()
230 }
231}
232
233/// Opening devices with a plain `open`.
234///
235/// What a process that already has permission wants, and the only thing that
236/// works without a session manager.
237struct Direct;
238
239impl LibinputInterface for Direct {
240 fn open_restricted(&mut self, path: &Path, flags: i32) -> Result<std::os::fd::OwnedFd, i32> {
241 // The flags are libinput's, in the C `open(2)` encoding. CLOEXEC is
242 // added because an input descriptor surviving an exec is a descriptor
243 // some child can read every keystroke through.
244 let flags = rustix::fs::OFlags::from_bits_retain(flags.cast_unsigned())
245 | rustix::fs::OFlags::CLOEXEC;
246 rustix::fs::open(path, flags, rustix::fs::Mode::empty())
247 .map_err(|errno| -errno.raw_os_error())
248 }
249
250 fn close_restricted(&mut self, fd: std::os::fd::OwnedFd) {
251 drop(fd);
252 }
253}
254
255fn mask(leds: Leds) -> Led {
256 let mut mask = Led::empty();
257 mask.set(Led::CAPSLOCK, leds.caps_lock);
258 mask.set(Led::NUMLOCK, leds.num_lock);
259 mask.set(Led::SCROLLLOCK, leds.scroll_lock);
260 mask
261}
262
263/// The device's name, for a consumer that treats a built-in trackpad
264/// differently from an external mouse.
265fn device_name(event: &impl EventTrait) -> Option<String> {
266 Some(event.device().name().into_owned()).filter(|name| !name.is_empty())
267}
268
269fn convert(event: input::Event) -> Option<InputEvent> {
270 match event {
271 input::Event::Keyboard(LibinputKeyboard::Key(key)) => Some(InputEvent::Key(KeyEvent {
272 time_ms: key.time(),
273 key: key.key(),
274 pressed: key.key_state() == KeyState::Pressed,
275 repeat: false,
276 sym: 0,
277 utf8: String::new(),
278 })),
279 input::Event::Pointer(pointer) => convert_pointer(pointer).map(InputEvent::Pointer),
280 input::Event::Touch(touch) => convert_touch(&touch).map(InputEvent::Touch),
281 input::Event::Switch(LibinputSwitchEvent::Toggle(toggle)) => {
282 let which = match toggle.switch()? {
283 LibinputSwitch::Lid => Switch::Lid,
284 LibinputSwitch::TabletMode => Switch::TabletMode,
285 // A switch a newer libinput knows about and this does not.
286 // Reporting it as one of the two here would be a lie.
287 _ => return None,
288 };
289 Some(InputEvent::Switch(SwitchEvent {
290 time_ms: toggle.time(),
291 which,
292 active: toggle.switch_state() == SwitchState::On,
293 }))
294 }
295 // Tablet tools, tablet pads and gestures are not carried. Nothing in
296 // the reference carries them either, and inventing an event shape for
297 // hardware there is nothing here to test against would be worse than
298 // the gap.
299 _ => None,
300 }
301}
302
303fn convert_pointer(event: LibinputPointer) -> Option<PointerEvent> {
304 match event {
305 LibinputPointer::Motion(motion) => Some(PointerEvent::Motion(PointerMotion {
306 time_ms: motion.time(),
307 dx: motion.dx(),
308 dy: motion.dy(),
309 device: device_name(&motion),
310 })),
311 LibinputPointer::MotionAbsolute(motion) => Some(PointerEvent::Absolute(PointerAbsolute {
312 time_ms: motion.time(),
313 x: motion.absolute_x_transformed(1),
314 y: motion.absolute_y_transformed(1),
315 device: device_name(&motion),
316 })),
317 LibinputPointer::Button(button) => Some(PointerEvent::Button(PointerButton {
318 time_ms: button.time(),
319 button: button.button(),
320 pressed: button.button_state() == ButtonState::Pressed,
321 })),
322 LibinputPointer::ScrollWheel(scroll) => Some(scrolled(&scroll, ScrollSource::Wheel)),
323 LibinputPointer::ScrollFinger(scroll) => Some(scrolled(&scroll, ScrollSource::Finger)),
324 LibinputPointer::ScrollContinuous(scroll) => {
325 Some(scrolled(&scroll, ScrollSource::Continuous))
326 }
327 // The pre-1.19 axis event, which libinput still emits alongside the
328 // three above for exactly as long as it takes clients to move. Taking
329 // both would count every scroll twice.
330 _ => None,
331 }
332}
333
334fn scrolled(
335 event: &(impl PointerScrollEvent + PointerEventTrait),
336 source: ScrollSource,
337) -> PointerEvent {
338 PointerEvent::Axis(PointerAxis {
339 time_ms: event.time(),
340 horizontal: axis(event, Axis::Horizontal),
341 vertical: axis(event, Axis::Vertical),
342 source,
343 })
344}
345
346/// A scroll axis, or zero if this event does not carry it.
347///
348/// Reading one that is absent is not an error, it is a value libinput has not
349/// set -- so it has to be asked about first.
350fn axis(event: &impl PointerScrollEvent, axis: Axis) -> f64 {
351 if event.has_axis(axis) {
352 event.scroll_value(axis)
353 } else {
354 0.0
355 }
356}
357
358fn convert_touch(event: &LibinputTouch) -> Option<TouchEvent> {
359 let (phase, time_ms) = match event {
360 LibinputTouch::Down(touch) => (TouchPhase::Down, touch.time()),
361 LibinputTouch::Up(touch) => (TouchPhase::Up, touch.time()),
362 LibinputTouch::Motion(touch) => (TouchPhase::Motion, touch.time()),
363 LibinputTouch::Frame(touch) => (TouchPhase::Frame, touch.time()),
364 LibinputTouch::Cancel(touch) => (TouchPhase::Cancel, touch.time()),
365 _ => return None,
366 };
367
368 // Only down and motion carry a position; up, frame and cancel say which
369 // point ended, not where it was.
370 let (x, y) = match event {
371 LibinputTouch::Down(touch) => (touch.x_transformed(1), touch.y_transformed(1)),
372 LibinputTouch::Motion(touch) => (touch.x_transformed(1), touch.y_transformed(1)),
373 _ => (0.0, 0.0),
374 };
375
376 // And a frame is the end of a batch rather than one point, so it has no
377 // slot. The reference reports frame and cancel with a zero timestamp as
378 // well, which loses the one thing a frame does carry.
379 let slot = match event {
380 LibinputTouch::Down(touch) => touch.slot(),
381 LibinputTouch::Up(touch) => touch.slot(),
382 LibinputTouch::Motion(touch) => touch.slot(),
383 LibinputTouch::Cancel(touch) => touch.slot(),
384 _ => None,
385 };
386
387 Some(TouchEvent {
388 time_ms,
389 slot: slot.map_or(-1, |slot| i32::try_from(slot).unwrap_or(-1)),
390 x,
391 y,
392 phase,
393 device: device_name(event),
394 })
395}