| 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 |