Rust · 15254 bytes Raw Blame History
1 //! Evdev-based input grabbing
2 //!
3 //! Grabs input devices at the kernel level using EVIOCGRAB.
4 //! This is the most reliable way to capture input on Linux.
5
6 use std::collections::HashMap;
7 use std::fs;
8 use std::os::unix::io::{AsRawFd, BorrowedFd};
9 use std::path::PathBuf;
10 use std::sync::atomic::{AtomicBool, Ordering};
11 use std::sync::mpsc;
12 use std::sync::Arc;
13 use std::thread;
14
15 use evdev::{Device, InputEventKind};
16 use rustix::fs::{fcntl_setfl, OFlags};
17
18 use super::grabber::GrabEvent;
19
20 /// Evdev-based input grabber
21 pub struct EvdevGrabber {
22 active: Arc<AtomicBool>,
23 event_rx: mpsc::Receiver<GrabEvent>,
24 _thread: thread::JoinHandle<()>,
25 }
26
27 impl EvdevGrabber {
28 /// Create a new evdev grabber
29 pub fn new() -> Result<Self, EvdevGrabError> {
30 let active = Arc::new(AtomicBool::new(false));
31 let active_clone = active.clone();
32
33 let (event_tx, event_rx) = mpsc::channel();
34
35 let thread = thread::Builder::new()
36 .name("evdev-grabber".to_string())
37 .spawn(move || {
38 if let Err(e) = run_evdev_grabber(active_clone, event_tx) {
39 tracing::error!("Evdev grabber error: {}", e);
40 }
41 })
42 .map_err(|e| EvdevGrabError::Thread(e.to_string()))?;
43
44 Ok(Self {
45 active,
46 event_rx,
47 _thread: thread,
48 })
49 }
50
51 /// Start grabbing input
52 pub fn start(&self) {
53 tracing::info!("Starting evdev input grab");
54 self.active.store(true, Ordering::SeqCst);
55 }
56
57 /// Stop grabbing input
58 pub fn stop(&self) {
59 tracing::info!("Stopping evdev input grab");
60 self.active.store(false, Ordering::SeqCst);
61 }
62
63 /// Check if currently grabbing
64 pub fn is_active(&self) -> bool {
65 self.active.load(Ordering::SeqCst)
66 }
67
68 /// Try to receive a grab event (non-blocking)
69 pub fn try_recv(&self) -> Option<GrabEvent> {
70 self.event_rx.try_recv().ok()
71 }
72 }
73
74 fn find_input_devices() -> Vec<PathBuf> {
75 let mut devices = Vec::new();
76 let mut seen_paths = std::collections::HashSet::new();
77
78 // Method 1: Look in /dev/input/by-id for keyboard and mouse devices
79 if let Ok(entries) = fs::read_dir("/dev/input/by-id") {
80 for entry in entries.flatten() {
81 let name = entry.file_name().to_string_lossy().to_lowercase();
82 // Look for keyboard and mouse event devices (not hidraw)
83 if name.contains("event") &&
84 (name.contains("kbd") || name.contains("keyboard") ||
85 name.contains("mouse") || name.contains("pointer")) {
86 if let Ok(path) = entry.path().canonicalize() {
87 if seen_paths.insert(path.clone()) {
88 devices.push(path);
89 }
90 }
91 }
92 }
93 }
94
95 // Method 2: Scan all /dev/input/event* and check capabilities
96 if let Ok(entries) = fs::read_dir("/dev/input") {
97 for entry in entries.flatten() {
98 let name = entry.file_name().to_string_lossy().to_string();
99 if name.starts_with("event") {
100 let path = entry.path();
101 if seen_paths.contains(&path) {
102 continue;
103 }
104
105 // Try to open and check if it's a keyboard or mouse
106 if let Ok(dev) = Device::open(&path) {
107 let has_keys = dev.supported_keys().map(|k| k.iter().count() > 0).unwrap_or(false);
108 let has_rel = dev.supported_relative_axes().map(|r| r.iter().count() > 0).unwrap_or(false);
109
110 // Include if it has keys (keyboard) or relative axes (mouse)
111 if has_keys || has_rel {
112 let dev_name = dev.name().unwrap_or("unknown");
113 tracing::debug!("Found input device: {} at {} (keys={}, rel={})",
114 dev_name, path.display(), has_keys, has_rel);
115 devices.push(path);
116 }
117 }
118 }
119 }
120 }
121
122 devices
123 }
124
125 fn run_evdev_grabber(
126 active: Arc<AtomicBool>,
127 event_tx: mpsc::Sender<GrabEvent>,
128 ) -> Result<(), EvdevGrabError> {
129 let device_paths = find_input_devices();
130
131 if device_paths.is_empty() {
132 return Err(EvdevGrabError::NoDevices);
133 }
134
135 tracing::info!("Found {} input device paths", device_paths.len());
136 for path in &device_paths {
137 tracing::debug!(" {}", path.display());
138 }
139
140 // We'll open and grab devices fresh each time we activate
141 let mut devices: HashMap<PathBuf, Device> = HashMap::new();
142 let mut grabbed = false;
143 let mut last_active = false;
144
145 loop {
146 let is_active = active.load(Ordering::SeqCst);
147
148 // Handle grab/ungrab transitions
149 if is_active != last_active {
150 last_active = is_active;
151
152 if is_active {
153 // Open and grab all devices fresh
154 devices.clear();
155 tracing::info!("Opening and grabbing input devices...");
156
157 for path in &device_paths {
158 match Device::open(path) {
159 Ok(mut dev) => {
160 let name = dev.name().unwrap_or("unknown").to_string();
161
162 // Set non-blocking mode
163 // SAFETY: dev owns the fd and will outlive this borrow
164 let fd = unsafe { BorrowedFd::borrow_raw(dev.as_raw_fd()) };
165 if let Err(e) = fcntl_setfl(fd, OFlags::NONBLOCK) {
166 tracing::warn!("Failed to set non-blocking on {}: {}", name, e);
167 }
168
169 // Drain any pending events before grabbing to start fresh
170 let _ = dev.fetch_events();
171
172 // Try to grab immediately after opening
173 match dev.grab() {
174 Ok(()) => {
175 tracing::info!("Grabbed: {} ({})", name, path.display());
176 devices.insert(path.clone(), dev);
177 }
178 Err(e) => {
179 tracing::warn!("Cannot grab {} ({}): {}", name, path.display(), e);
180 // Don't add to devices if we can't grab
181 }
182 }
183 }
184 Err(e) => {
185 tracing::warn!("Failed to open {}: {}", path.display(), e);
186 }
187 }
188 }
189
190 if devices.is_empty() {
191 tracing::error!("Failed to grab any input devices!");
192 } else {
193 tracing::info!("Successfully grabbed {} devices", devices.len());
194 }
195 grabbed = true;
196 } else {
197 // Ungrab and close all devices
198 tracing::info!("Releasing {} input devices", devices.len());
199
200 // Keys we care about: modifiers and arrows
201 let modifier_keys: &[u16] = &[
202 125, 126, // KEY_LEFTMETA, KEY_RIGHTMETA (Super)
203 42, 54, // KEY_LEFTSHIFT, KEY_RIGHTSHIFT
204 29, 97, // KEY_LEFTCTRL, KEY_RIGHTCTRL
205 56, 100, // KEY_LEFTALT, KEY_RIGHTALT
206 ];
207 let arrow_keys: &[u16] = &[
208 103, 108, 105, 106, // KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT
209 ];
210
211 for (path, mut dev) in devices.drain() {
212 // First ungrab so libinput can receive events
213 if let Err(e) = dev.ungrab() {
214 tracing::warn!("Failed to ungrab {}: {}", path.display(), e);
215 continue;
216 }
217 tracing::debug!("Released {}", path.display());
218
219 // Query actual physical key state AFTER ungrab
220 let key_state = match dev.get_key_state() {
221 Ok(state) => state,
222 Err(e) => {
223 tracing::debug!("Could not query key state for {}: {}", path.display(), e);
224 continue;
225 }
226 };
227
228 // For ARROW keys: if NOT pressed, send UP to clear stale state
229 // (Hyprland thought they were pressed from before grab)
230 for &keycode in arrow_keys {
231 let key = evdev::Key::new(keycode);
232 if !key_state.contains(key) {
233 // Key is not pressed - send UP to clear stale state
234 let key_event = evdev::InputEvent::new(
235 evdev::EventType::KEY, keycode, 0,
236 );
237 let syn_event = evdev::InputEvent::new(
238 evdev::EventType::SYNCHRONIZATION, 0, 0,
239 );
240 let _ = dev.send_events(&[key_event, syn_event]);
241 }
242 // If key IS pressed, don't send anything - user is still holding it
243 }
244
245 // For MODIFIER keys: if pressed, send UP then DOWN (fresh edge)
246 // This gives Hyprland a clean state transition
247 for &keycode in modifier_keys {
248 let key = evdev::Key::new(keycode);
249 if key_state.contains(key) {
250 // Key is pressed - send UP then DOWN for fresh edge
251 let up_event = evdev::InputEvent::new(
252 evdev::EventType::KEY, keycode, 0,
253 );
254 let down_event = evdev::InputEvent::new(
255 evdev::EventType::KEY, keycode, 1,
256 );
257 let syn_event = evdev::InputEvent::new(
258 evdev::EventType::SYNCHRONIZATION, 0, 0,
259 );
260 let _ = dev.send_events(&[up_event, syn_event, down_event, syn_event]);
261 tracing::debug!("Sent UP+DOWN for held modifier key {}", keycode);
262 }
263 // If not pressed, don't send anything - already released
264 }
265
266 // Device is dropped here, closing the fd
267 }
268 grabbed = false;
269 }
270 }
271
272 // Read events if grabbed
273 if grabbed && !devices.is_empty() {
274 // Accumulate motion deltas across all devices and events
275 let mut motion_dx: f64 = 0.0;
276 let mut motion_dy: f64 = 0.0;
277 let mut scroll_h: f64 = 0.0;
278 let mut scroll_v: f64 = 0.0;
279
280 for (_path, dev) in &mut devices {
281 // Non-blocking read
282 if let Ok(events) = dev.fetch_events() {
283 for ev in events {
284 // Log raw key events from kernel for debugging
285 if let InputEventKind::Key(key) = ev.kind() {
286 tracing::debug!("RAW EVDEV: key={} value={} (1=press, 0=release, 2=repeat)",
287 key.code(), ev.value());
288 }
289 match convert_event(&ev) {
290 Some(GrabEvent::PointerMotion { dx, dy }) => {
291 // Accumulate motion instead of sending immediately
292 motion_dx += dx;
293 motion_dy += dy;
294 }
295 Some(GrabEvent::Scroll { horizontal, vertical }) => {
296 // Accumulate scroll
297 scroll_h += horizontal;
298 scroll_v += vertical;
299 }
300 Some(other) => {
301 // Key events etc. - send immediately
302 if event_tx.send(other).is_err() {
303 return Ok(());
304 }
305 }
306 None => {}
307 }
308 }
309 }
310 }
311
312 // Send accumulated motion as single event (if any)
313 if motion_dx != 0.0 || motion_dy != 0.0 {
314 if event_tx.send(GrabEvent::PointerMotion { dx: motion_dx, dy: motion_dy }).is_err() {
315 return Ok(());
316 }
317 }
318
319 // Send accumulated scroll as single event (if any)
320 if scroll_h != 0.0 || scroll_v != 0.0 {
321 if event_tx.send(GrabEvent::Scroll { horizontal: scroll_h, vertical: scroll_v }).is_err() {
322 return Ok(());
323 }
324 }
325 }
326
327 // Minimal sleep to avoid busy-looping while keeping latency low
328 thread::sleep(std::time::Duration::from_micros(100));
329 }
330 }
331
332 fn convert_event(ev: &evdev::InputEvent) -> Option<GrabEvent> {
333 match ev.kind() {
334 InputEventKind::Key(key) => {
335 let keycode = key.code() as u32;
336 let pressed = ev.value() == 1;
337 let released = ev.value() == 0;
338
339 if pressed {
340 Some(GrabEvent::KeyDown { keycode })
341 } else if released {
342 Some(GrabEvent::KeyUp { keycode })
343 } else {
344 None // Repeat events, ignore
345 }
346 }
347 InputEventKind::RelAxis(axis) => {
348 use evdev::RelativeAxisType;
349 match axis {
350 RelativeAxisType::REL_X => {
351 Some(GrabEvent::PointerMotion {
352 dx: ev.value() as f64,
353 dy: 0.0,
354 })
355 }
356 RelativeAxisType::REL_Y => {
357 Some(GrabEvent::PointerMotion {
358 dx: 0.0,
359 dy: ev.value() as f64,
360 })
361 }
362 RelativeAxisType::REL_WHEEL => {
363 Some(GrabEvent::Scroll {
364 horizontal: 0.0,
365 vertical: ev.value() as f64 * -15.0, // Invert and scale
366 })
367 }
368 RelativeAxisType::REL_HWHEEL => {
369 Some(GrabEvent::Scroll {
370 horizontal: ev.value() as f64 * 15.0,
371 vertical: 0.0,
372 })
373 }
374 _ => None,
375 }
376 }
377 _ => None,
378 }
379 }
380
381 #[derive(Debug, thiserror::Error)]
382 pub enum EvdevGrabError {
383 #[error("No input devices found")]
384 NoDevices,
385
386 #[error("Thread error: {0}")]
387 Thread(String),
388
389 #[error("Device error: {0}")]
390 Device(String),
391 }
392