Skip to main content

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 libinput cannot enumerate the seat through
94    /// udev. An unknown seat name is not an error; see [`SeatOptions::seat`].
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, as the reference reports it.
378    let slot = match event {
379        LibinputTouch::Down(touch) => touch.slot(),
380        LibinputTouch::Up(touch) => touch.slot(),
381        LibinputTouch::Motion(touch) => touch.slot(),
382        LibinputTouch::Cancel(touch) => touch.slot(),
383        _ => None,
384    };
385
386    Some(TouchEvent {
387        time_ms,
388        slot: slot.map_or(-1, |slot| i32::try_from(slot).unwrap_or(-1)),
389        x,
390        y,
391        phase,
392        device: device_name(event),
393    })
394}