| 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, AtomicU64, Ordering}; |
| 11 | use std::sync::mpsc; |
| 12 | use std::sync::Arc; |
| 13 | use std::thread; |
| 14 | |
| 15 | use evdev::{Device, InputEventKind}; |
| 16 | use hyprkvm_common::Direction; |
| 17 | use rustix::fs::{fcntl_setfl, OFlags}; |
| 18 | |
| 19 | use super::grabber::GrabEvent; |
| 20 | |
| 21 | // Recovery mode ends when: |
| 22 | // 1. Super key is released (user stopped trying keybinds or will retry with fresh state) |
| 23 | // 2. The target key combo is detected |
| 24 | // 3. A new grab starts |
| 25 | |
| 26 | /// Evdev-based input grabber |
| 27 | pub struct EvdevGrabber { |
| 28 | active: Arc<AtomicBool>, |
| 29 | /// Flag indicating recovery mode is active (1 = active, 0 = inactive) |
| 30 | recovery_active: Arc<AtomicU64>, |
| 31 | /// The direction to watch for in recovery mode (encoded as u8: 1=Up, 2=Down, 3=Left, 4=Right, 0=none) |
| 32 | recovery_direction: Arc<AtomicU64>, |
| 33 | event_rx: mpsc::Receiver<GrabEvent>, |
| 34 | _thread: thread::JoinHandle<()>, |
| 35 | } |
| 36 | |
| 37 | impl EvdevGrabber { |
| 38 | /// Create a new evdev grabber |
| 39 | pub fn new() -> Result<Self, EvdevGrabError> { |
| 40 | let active = Arc::new(AtomicBool::new(false)); |
| 41 | let active_clone = active.clone(); |
| 42 | let recovery_active = Arc::new(AtomicU64::new(0)); |
| 43 | let recovery_clone = recovery_active.clone(); |
| 44 | let recovery_direction = Arc::new(AtomicU64::new(0)); |
| 45 | let recovery_dir_clone = recovery_direction.clone(); |
| 46 | |
| 47 | let (event_tx, event_rx) = mpsc::channel(); |
| 48 | |
| 49 | let thread = thread::Builder::new() |
| 50 | .name("evdev-grabber".to_string()) |
| 51 | .spawn(move || { |
| 52 | if let Err(e) = run_evdev_grabber(active_clone, recovery_clone, recovery_dir_clone, event_tx) { |
| 53 | tracing::error!("Evdev grabber error: {}", e); |
| 54 | } |
| 55 | }) |
| 56 | .map_err(|e| EvdevGrabError::Thread(e.to_string()))?; |
| 57 | |
| 58 | Ok(Self { |
| 59 | active, |
| 60 | recovery_active, |
| 61 | recovery_direction, |
| 62 | event_rx, |
| 63 | _thread: thread, |
| 64 | }) |
| 65 | } |
| 66 | |
| 67 | /// Start grabbing input |
| 68 | pub fn start(&self) { |
| 69 | tracing::info!("Starting evdev input grab"); |
| 70 | // Cancel any recovery mode when starting a new grab |
| 71 | self.recovery_active.store(0, Ordering::SeqCst); |
| 72 | self.active.store(true, Ordering::SeqCst); |
| 73 | } |
| 74 | |
| 75 | /// Stop grabbing input and enter recovery monitoring mode |
| 76 | /// `stale_direction` is the direction of the outgoing transfer - the key that's stale in libinput |
| 77 | pub fn stop(&self, stale_direction: Option<Direction>) { |
| 78 | // Encode direction: 1=Up, 2=Down, 3=Left, 4=Right, 0=none |
| 79 | let dir_code = match stale_direction { |
| 80 | Some(Direction::Up) => 1, |
| 81 | Some(Direction::Down) => 2, |
| 82 | Some(Direction::Left) => 3, |
| 83 | Some(Direction::Right) => 4, |
| 84 | None => 0, |
| 85 | }; |
| 86 | self.recovery_direction.store(dir_code, Ordering::SeqCst); |
| 87 | |
| 88 | tracing::info!("Stopping evdev input grab, entering recovery mode, watching for {:?}", |
| 89 | stale_direction); |
| 90 | |
| 91 | // Activate recovery mode (will stay active until Super is released or hotkey detected) |
| 92 | if stale_direction.is_some() { |
| 93 | self.recovery_active.store(1, Ordering::SeqCst); |
| 94 | } |
| 95 | // Deactivate grab |
| 96 | self.active.store(false, Ordering::SeqCst); |
| 97 | } |
| 98 | |
| 99 | /// Check if currently grabbing |
| 100 | pub fn is_active(&self) -> bool { |
| 101 | self.active.load(Ordering::SeqCst) |
| 102 | } |
| 103 | |
| 104 | /// Try to receive a grab event (non-blocking) |
| 105 | pub fn try_recv(&self) -> Option<GrabEvent> { |
| 106 | self.event_rx.try_recv().ok() |
| 107 | } |
| 108 | } |
| 109 | |
| 110 | fn find_input_devices() -> Vec<PathBuf> { |
| 111 | let mut devices = Vec::new(); |
| 112 | let mut seen_paths = std::collections::HashSet::new(); |
| 113 | |
| 114 | // Method 1: Look in /dev/input/by-id for keyboard and mouse devices |
| 115 | if let Ok(entries) = fs::read_dir("/dev/input/by-id") { |
| 116 | for entry in entries.flatten() { |
| 117 | let name = entry.file_name().to_string_lossy().to_lowercase(); |
| 118 | // Look for keyboard and mouse event devices (not hidraw) |
| 119 | if name.contains("event") && |
| 120 | (name.contains("kbd") || name.contains("keyboard") || |
| 121 | name.contains("mouse") || name.contains("pointer")) { |
| 122 | if let Ok(path) = entry.path().canonicalize() { |
| 123 | if seen_paths.insert(path.clone()) { |
| 124 | devices.push(path); |
| 125 | } |
| 126 | } |
| 127 | } |
| 128 | } |
| 129 | } |
| 130 | |
| 131 | // Method 2: Scan all /dev/input/event* and check capabilities |
| 132 | // Be more restrictive - only grab devices that are ACTUALLY keyboards or mice |
| 133 | if let Ok(entries) = fs::read_dir("/dev/input") { |
| 134 | for entry in entries.flatten() { |
| 135 | let name = entry.file_name().to_string_lossy().to_string(); |
| 136 | if name.starts_with("event") { |
| 137 | let path = entry.path(); |
| 138 | if seen_paths.contains(&path) { |
| 139 | continue; |
| 140 | } |
| 141 | |
| 142 | // Try to open and check if it's a keyboard or mouse |
| 143 | if let Ok(dev) = Device::open(&path) { |
| 144 | let dev_name = dev.name().unwrap_or("unknown").to_lowercase(); |
| 145 | |
| 146 | // Skip devices that are clearly NOT keyboard/mouse |
| 147 | // Also skip hyprkvm's own virtual injection devices (passthrough) |
| 148 | if dev_name.contains("power") |
| 149 | || dev_name.contains("sleep") |
| 150 | || dev_name.contains("button") |
| 151 | || dev_name.contains("wmi") |
| 152 | || dev_name.contains("hotkey") |
| 153 | || dev_name.contains("consumer control") |
| 154 | || dev_name.contains("video bus") |
| 155 | || dev_name.contains("dualsense") |
| 156 | || dev_name.contains("dualshock") |
| 157 | || dev_name.contains("controller touchpad") |
| 158 | || dev_name.contains("passthrough") |
| 159 | || dev_name.contains("hyprkvm") |
| 160 | { |
| 161 | tracing::debug!("Skipping non-keyboard/mouse device: {}", dev_name); |
| 162 | continue; |
| 163 | } |
| 164 | |
| 165 | let has_keys = dev.supported_keys().map(|k| k.iter().count() > 10).unwrap_or(false); |
| 166 | let has_rel = dev.supported_relative_axes().map(|r| r.iter().count() > 0).unwrap_or(false); |
| 167 | |
| 168 | // A real keyboard has many keys (>10), a real mouse has relative axes |
| 169 | if has_keys || has_rel { |
| 170 | let dev_name = dev.name().unwrap_or("unknown"); |
| 171 | tracing::debug!("Found input device: {} at {} (keys={}, rel={})", |
| 172 | dev_name, path.display(), has_keys, has_rel); |
| 173 | devices.push(path); |
| 174 | } |
| 175 | } |
| 176 | } |
| 177 | } |
| 178 | } |
| 179 | |
| 180 | devices |
| 181 | } |
| 182 | |
| 183 | fn run_evdev_grabber( |
| 184 | active: Arc<AtomicBool>, |
| 185 | recovery_active: Arc<AtomicU64>, |
| 186 | recovery_direction: Arc<AtomicU64>, |
| 187 | event_tx: mpsc::Sender<GrabEvent>, |
| 188 | ) -> Result<(), EvdevGrabError> { |
| 189 | let device_paths = find_input_devices(); |
| 190 | |
| 191 | if device_paths.is_empty() { |
| 192 | return Err(EvdevGrabError::NoDevices); |
| 193 | } |
| 194 | |
| 195 | tracing::info!("Found {} input device paths", device_paths.len()); |
| 196 | for path in &device_paths { |
| 197 | tracing::debug!(" {}", path.display()); |
| 198 | } |
| 199 | |
| 200 | // State machine states |
| 201 | #[derive(Debug, Clone, Copy, PartialEq)] |
| 202 | enum State { |
| 203 | Idle, |
| 204 | Grabbed, |
| 205 | Recovery, |
| 206 | /// Post-recovery: waiting for Super key release while keeping synthetic key-down active |
| 207 | PostRecovery, |
| 208 | } |
| 209 | |
| 210 | let mut devices: HashMap<PathBuf, Device> = HashMap::new(); |
| 211 | let mut state = State::Idle; |
| 212 | |
| 213 | // Key state tracking for recovery mode (Super + Arrow detection) |
| 214 | let mut super_held = false; |
| 215 | let mut super_was_held_at_start = false; // Track if Super was held when recovery started |
| 216 | let mut recovery_hotkey_sent = false; |
| 217 | |
| 218 | // Virtual keyboard for synthetic Super key-down (kept alive in PostRecovery state) |
| 219 | let mut synthetic_keyboard: Option<evdev::uinput::VirtualDevice> = None; |
| 220 | |
| 221 | // Key codes |
| 222 | const KEY_LEFTMETA: u16 = 125; |
| 223 | const KEY_RIGHTMETA: u16 = 126; |
| 224 | const KEY_UP: u16 = 103; |
| 225 | const KEY_DOWN: u16 = 108; |
| 226 | const KEY_LEFT: u16 = 105; |
| 227 | const KEY_RIGHT: u16 = 106; |
| 228 | |
| 229 | loop { |
| 230 | let is_active = active.load(Ordering::SeqCst); |
| 231 | let in_recovery = recovery_active.load(Ordering::SeqCst) == 1; |
| 232 | |
| 233 | // State transitions |
| 234 | match state { |
| 235 | State::Idle => { |
| 236 | if is_active { |
| 237 | // Transition to Grabbed |
| 238 | devices.clear(); |
| 239 | tracing::info!("Opening and grabbing input devices..."); |
| 240 | |
| 241 | // Small delay before starting grabs to let any pending events settle |
| 242 | std::thread::sleep(std::time::Duration::from_millis(50)); |
| 243 | |
| 244 | for path in &device_paths { |
| 245 | match Device::open(path) { |
| 246 | Ok(mut dev) => { |
| 247 | let name = dev.name().unwrap_or("unknown").to_string(); |
| 248 | let fd = unsafe { BorrowedFd::borrow_raw(dev.as_raw_fd()) }; |
| 249 | let _ = fcntl_setfl(fd, OFlags::NONBLOCK); |
| 250 | let _ = dev.fetch_events(); // Drain pending |
| 251 | |
| 252 | match dev.grab() { |
| 253 | Ok(()) => { |
| 254 | tracing::info!("Grabbed: {} ({})", name, path.display()); |
| 255 | devices.insert(path.clone(), dev); |
| 256 | } |
| 257 | Err(e) => { |
| 258 | tracing::warn!("Cannot grab {}: {}", name, e); |
| 259 | } |
| 260 | } |
| 261 | // Small delay between grabs to avoid overwhelming libinput |
| 262 | std::thread::sleep(std::time::Duration::from_millis(10)); |
| 263 | } |
| 264 | Err(e) => { |
| 265 | tracing::warn!("Failed to open {}: {}", path.display(), e); |
| 266 | } |
| 267 | } |
| 268 | } |
| 269 | |
| 270 | if devices.is_empty() { |
| 271 | tracing::error!("Failed to grab any input devices!"); |
| 272 | } else { |
| 273 | tracing::info!("Successfully grabbed {} devices", devices.len()); |
| 274 | state = State::Grabbed; |
| 275 | } |
| 276 | } |
| 277 | } |
| 278 | |
| 279 | State::Grabbed => { |
| 280 | if !is_active { |
| 281 | // Transition to Recovery (ungrab but keep devices open) |
| 282 | tracing::info!("Releasing grab, entering recovery mode"); |
| 283 | |
| 284 | for (_path, dev) in &mut devices { |
| 285 | if let Err(e) = dev.ungrab() { |
| 286 | tracing::warn!("Failed to ungrab: {}", e); |
| 287 | } |
| 288 | } |
| 289 | |
| 290 | // Reset recovery state |
| 291 | super_held = false; |
| 292 | super_was_held_at_start = false; |
| 293 | recovery_hotkey_sent = false; |
| 294 | |
| 295 | // Query current Super key state from physical keyboard |
| 296 | for dev in devices.values() { |
| 297 | if let Ok(key_state) = dev.get_key_state() { |
| 298 | if key_state.contains(evdev::Key::new(KEY_LEFTMETA)) |
| 299 | || key_state.contains(evdev::Key::new(KEY_RIGHTMETA)) |
| 300 | { |
| 301 | super_held = true; |
| 302 | super_was_held_at_start = true; |
| 303 | tracing::debug!("Super key is physically held at recovery start"); |
| 304 | } |
| 305 | } |
| 306 | } |
| 307 | |
| 308 | state = State::Recovery; |
| 309 | tracing::info!("Now in recovery mode, super_held={}, super_was_held_at_start={}", |
| 310 | super_held, super_was_held_at_start); |
| 311 | } else { |
| 312 | // Still grabbed - forward events |
| 313 | let mut motion_dx: f64 = 0.0; |
| 314 | let mut motion_dy: f64 = 0.0; |
| 315 | let mut scroll_h: f64 = 0.0; |
| 316 | let mut scroll_v: f64 = 0.0; |
| 317 | |
| 318 | for dev in devices.values_mut() { |
| 319 | if let Ok(events) = dev.fetch_events() { |
| 320 | for ev in events { |
| 321 | if let InputEventKind::Key(key) = ev.kind() { |
| 322 | tracing::debug!("RAW EVDEV: key={} value={}", |
| 323 | key.code(), ev.value()); |
| 324 | } |
| 325 | match convert_event(&ev) { |
| 326 | Some(GrabEvent::PointerMotion { dx, dy }) => { |
| 327 | motion_dx += dx; |
| 328 | motion_dy += dy; |
| 329 | } |
| 330 | Some(GrabEvent::Scroll { horizontal, vertical }) => { |
| 331 | scroll_h += horizontal; |
| 332 | scroll_v += vertical; |
| 333 | } |
| 334 | Some(other) => { |
| 335 | if event_tx.send(other).is_err() { |
| 336 | return Ok(()); |
| 337 | } |
| 338 | } |
| 339 | None => {} |
| 340 | } |
| 341 | } |
| 342 | } |
| 343 | } |
| 344 | |
| 345 | if motion_dx != 0.0 || motion_dy != 0.0 { |
| 346 | if event_tx.send(GrabEvent::PointerMotion { dx: motion_dx, dy: motion_dy }).is_err() { |
| 347 | return Ok(()); |
| 348 | } |
| 349 | } |
| 350 | if scroll_h != 0.0 || scroll_v != 0.0 { |
| 351 | if event_tx.send(GrabEvent::Scroll { horizontal: scroll_h, vertical: scroll_v }).is_err() { |
| 352 | return Ok(()); |
| 353 | } |
| 354 | } |
| 355 | } |
| 356 | } |
| 357 | |
| 358 | State::Recovery => { |
| 359 | if is_active { |
| 360 | // New grab starting, go back to grabbed state |
| 361 | // First close current devices, they'll be reopened fresh |
| 362 | devices.clear(); |
| 363 | recovery_active.store(0, Ordering::SeqCst); |
| 364 | state = State::Idle; |
| 365 | continue; |
| 366 | } |
| 367 | |
| 368 | if !in_recovery { |
| 369 | // Recovery mode was disabled externally |
| 370 | tracing::info!("Recovery mode ended (disabled)"); |
| 371 | devices.clear(); |
| 372 | state = State::Idle; |
| 373 | continue; |
| 374 | } |
| 375 | |
| 376 | // In recovery mode - monitor for Super+Arrow |
| 377 | // Read events WITHOUT grab (we're just observing) |
| 378 | let mut should_end_recovery = false; |
| 379 | let mut end_reason = ""; |
| 380 | |
| 381 | // Decode the direction we're watching for |
| 382 | let watch_dir_code = recovery_direction.load(Ordering::SeqCst); |
| 383 | let watch_direction: Option<Direction> = match watch_dir_code { |
| 384 | 1 => Some(Direction::Up), |
| 385 | 2 => Some(Direction::Down), |
| 386 | 3 => Some(Direction::Left), |
| 387 | 4 => Some(Direction::Right), |
| 388 | _ => None, |
| 389 | }; |
| 390 | |
| 391 | for dev in devices.values_mut() { |
| 392 | if let Ok(events) = dev.fetch_events() { |
| 393 | for ev in events { |
| 394 | if let InputEventKind::Key(key) = ev.kind() { |
| 395 | let keycode = key.code(); |
| 396 | let pressed = ev.value() == 1; |
| 397 | let released = ev.value() == 0; |
| 398 | |
| 399 | // Track Super key state |
| 400 | if keycode == KEY_LEFTMETA || keycode == KEY_RIGHTMETA { |
| 401 | if pressed { |
| 402 | super_held = true; |
| 403 | tracing::debug!("RECOVERY: Super pressed"); |
| 404 | } else if released { |
| 405 | super_held = false; |
| 406 | tracing::debug!("RECOVERY: Super released"); |
| 407 | |
| 408 | // End recovery when Super is released |
| 409 | // If Super was held at start, user has finished their keybind attempt |
| 410 | // If Super wasn't held at start, they did a fresh Super press+release |
| 411 | if super_was_held_at_start { |
| 412 | tracing::info!("RECOVERY: Super released (was held at start), ending recovery"); |
| 413 | should_end_recovery = true; |
| 414 | end_reason = "Super released"; |
| 415 | break; |
| 416 | } |
| 417 | } |
| 418 | } |
| 419 | |
| 420 | // Check for Super+Arrow, but ONLY for the direction we're watching |
| 421 | if pressed && super_held && !recovery_hotkey_sent { |
| 422 | let key_direction = match keycode { |
| 423 | KEY_UP => Some(Direction::Up), |
| 424 | KEY_DOWN => Some(Direction::Down), |
| 425 | KEY_LEFT => Some(Direction::Left), |
| 426 | KEY_RIGHT => Some(Direction::Right), |
| 427 | _ => None, |
| 428 | }; |
| 429 | |
| 430 | if let Some(dir) = key_direction { |
| 431 | // Only trigger if this matches the direction we're watching for |
| 432 | if watch_direction == Some(dir) { |
| 433 | tracing::info!("RECOVERY: Detected Super+{:?} (matches watch direction), sending hotkey event", dir); |
| 434 | if event_tx.send(GrabEvent::RecoveryHotkey { direction: dir }).is_err() { |
| 435 | return Ok(()); |
| 436 | } |
| 437 | recovery_hotkey_sent = true; |
| 438 | should_end_recovery = true; |
| 439 | end_reason = "hotkey detected"; |
| 440 | break; |
| 441 | } else { |
| 442 | tracing::debug!("RECOVERY: Ignoring Super+{:?} (watching for {:?})", dir, watch_direction); |
| 443 | } |
| 444 | } |
| 445 | } |
| 446 | } |
| 447 | } |
| 448 | } |
| 449 | if should_end_recovery { |
| 450 | break; |
| 451 | } |
| 452 | } |
| 453 | |
| 454 | // End recovery mode if hotkey was detected or Super was released |
| 455 | if should_end_recovery { |
| 456 | tracing::info!("Recovery mode ended ({})", end_reason); |
| 457 | recovery_active.store(0, Ordering::SeqCst); |
| 458 | |
| 459 | // If we detected a hotkey and Super is still held, we need to send a |
| 460 | // synthetic Super key-down via uinput. This informs libinput that Super |
| 461 | // is pressed, since it never saw the original key-down (it was grabbed). |
| 462 | // CRITICAL: We must keep the virtual device alive until Super is released, |
| 463 | // otherwise the kernel will auto-send key-up when the device is destroyed. |
| 464 | if recovery_hotkey_sent && super_held { |
| 465 | tracing::info!("RECOVERY: Super still held, entering PostRecovery to maintain synthetic key-down"); |
| 466 | |
| 467 | // Create virtual keyboard and send Super key-down |
| 468 | match create_synthetic_keyboard_with_super_down() { |
| 469 | Ok(virt_dev) => { |
| 470 | synthetic_keyboard = Some(virt_dev); |
| 471 | // Keep devices open to monitor for Super release |
| 472 | state = State::PostRecovery; |
| 473 | tracing::info!("PostRecovery: synthetic keyboard created, monitoring for Super release"); |
| 474 | } |
| 475 | Err(e) => { |
| 476 | tracing::error!("Failed to create synthetic keyboard: {}", e); |
| 477 | devices.clear(); |
| 478 | state = State::Idle; |
| 479 | } |
| 480 | } |
| 481 | } else { |
| 482 | // Super already released or no hotkey detected, clean up normally |
| 483 | devices.clear(); |
| 484 | state = State::Idle; |
| 485 | } |
| 486 | } |
| 487 | } |
| 488 | |
| 489 | State::PostRecovery => { |
| 490 | // In post-recovery mode, we're keeping the synthetic keyboard alive |
| 491 | // with Super key-down. Monitor physical keyboard for Super release. |
| 492 | if is_active { |
| 493 | // New grab starting, clean up and transition |
| 494 | tracing::info!("PostRecovery: new grab requested, cleaning up"); |
| 495 | if let Some(ref mut virt_dev) = synthetic_keyboard { |
| 496 | // Send Super key-up before destroying |
| 497 | let key_up = evdev::InputEvent::new(evdev::EventType::KEY, KEY_LEFTMETA, 0); |
| 498 | let syn = evdev::InputEvent::new(evdev::EventType::SYNCHRONIZATION, 0, 0); |
| 499 | let _ = virt_dev.emit(&[key_up, syn]); |
| 500 | } |
| 501 | synthetic_keyboard = None; |
| 502 | devices.clear(); |
| 503 | state = State::Idle; |
| 504 | continue; |
| 505 | } |
| 506 | |
| 507 | // Monitor for Super key release on physical keyboard |
| 508 | let mut super_released = false; |
| 509 | for dev in devices.values_mut() { |
| 510 | if let Ok(events) = dev.fetch_events() { |
| 511 | for ev in events { |
| 512 | if let InputEventKind::Key(key) = ev.kind() { |
| 513 | let keycode = key.code(); |
| 514 | let released = ev.value() == 0; |
| 515 | |
| 516 | // Check for Super release |
| 517 | if (keycode == KEY_LEFTMETA || keycode == KEY_RIGHTMETA) && released { |
| 518 | super_released = true; |
| 519 | break; |
| 520 | } |
| 521 | } |
| 522 | } |
| 523 | } |
| 524 | if super_released { |
| 525 | break; |
| 526 | } |
| 527 | } |
| 528 | |
| 529 | // Handle Super release outside the borrow |
| 530 | if super_released { |
| 531 | tracing::info!("PostRecovery: Super released, sending synthetic key-up and cleaning up"); |
| 532 | |
| 533 | // Send synthetic Super key-up |
| 534 | if let Some(ref mut virt_dev) = synthetic_keyboard { |
| 535 | let key_up = evdev::InputEvent::new(evdev::EventType::KEY, KEY_LEFTMETA, 0); |
| 536 | let syn = evdev::InputEvent::new(evdev::EventType::SYNCHRONIZATION, 0, 0); |
| 537 | if let Err(e) = virt_dev.emit(&[key_up, syn]) { |
| 538 | tracing::warn!("Failed to send synthetic Super key-up: {}", e); |
| 539 | } |
| 540 | } |
| 541 | |
| 542 | // Clean up |
| 543 | synthetic_keyboard = None; |
| 544 | devices.clear(); |
| 545 | super_held = false; |
| 546 | state = State::Idle; |
| 547 | } |
| 548 | } |
| 549 | } |
| 550 | |
| 551 | // Minimal sleep |
| 552 | thread::sleep(std::time::Duration::from_micros(100)); |
| 553 | } |
| 554 | } |
| 555 | |
| 556 | // Mouse button codes (from linux/input-event-codes.h) |
| 557 | const BTN_MOUSE: u16 = 0x110; |
| 558 | const BTN_LEFT: u16 = 0x110; |
| 559 | const BTN_RIGHT: u16 = 0x111; |
| 560 | const BTN_MIDDLE: u16 = 0x112; |
| 561 | const BTN_SIDE: u16 = 0x113; |
| 562 | const BTN_EXTRA: u16 = 0x114; |
| 563 | const BTN_FORWARD: u16 = 0x115; |
| 564 | const BTN_BACK: u16 = 0x116; |
| 565 | const BTN_TASK: u16 = 0x117; |
| 566 | |
| 567 | fn is_mouse_button(code: u16) -> bool { |
| 568 | code >= BTN_MOUSE && code <= BTN_TASK |
| 569 | } |
| 570 | |
| 571 | fn convert_event(ev: &evdev::InputEvent) -> Option<GrabEvent> { |
| 572 | match ev.kind() { |
| 573 | InputEventKind::Key(key) => { |
| 574 | let keycode = key.code(); |
| 575 | let pressed = ev.value() == 1; |
| 576 | let released = ev.value() == 0; |
| 577 | |
| 578 | // Check if this is a mouse button |
| 579 | if is_mouse_button(keycode) { |
| 580 | if pressed || released { |
| 581 | tracing::debug!("MOUSE BUTTON: code={:#x} pressed={}", keycode, pressed); |
| 582 | Some(GrabEvent::PointerButton { |
| 583 | button: keycode as u32, |
| 584 | pressed, |
| 585 | }) |
| 586 | } else { |
| 587 | None // Repeat events, ignore |
| 588 | } |
| 589 | } else { |
| 590 | // Regular keyboard key |
| 591 | if pressed { |
| 592 | Some(GrabEvent::KeyDown { keycode: keycode as u32 }) |
| 593 | } else if released { |
| 594 | Some(GrabEvent::KeyUp { keycode: keycode as u32 }) |
| 595 | } else { |
| 596 | None // Repeat events, ignore |
| 597 | } |
| 598 | } |
| 599 | } |
| 600 | InputEventKind::RelAxis(axis) => { |
| 601 | use evdev::RelativeAxisType; |
| 602 | match axis { |
| 603 | RelativeAxisType::REL_X => { |
| 604 | Some(GrabEvent::PointerMotion { |
| 605 | dx: ev.value() as f64, |
| 606 | dy: 0.0, |
| 607 | }) |
| 608 | } |
| 609 | RelativeAxisType::REL_Y => { |
| 610 | Some(GrabEvent::PointerMotion { |
| 611 | dx: 0.0, |
| 612 | dy: ev.value() as f64, |
| 613 | }) |
| 614 | } |
| 615 | RelativeAxisType::REL_WHEEL => { |
| 616 | tracing::debug!("SCROLL: REL_WHEEL value={}", ev.value()); |
| 617 | Some(GrabEvent::Scroll { |
| 618 | horizontal: 0.0, |
| 619 | vertical: ev.value() as f64 * -15.0, // Invert and scale |
| 620 | }) |
| 621 | } |
| 622 | RelativeAxisType::REL_HWHEEL => { |
| 623 | tracing::debug!("SCROLL: REL_HWHEEL value={}", ev.value()); |
| 624 | Some(GrabEvent::Scroll { |
| 625 | horizontal: ev.value() as f64 * 15.0, |
| 626 | vertical: 0.0, |
| 627 | }) |
| 628 | } |
| 629 | // High-resolution scroll (modern mice) |
| 630 | RelativeAxisType::REL_WHEEL_HI_RES => { |
| 631 | tracing::debug!("SCROLL: REL_WHEEL_HI_RES value={}", ev.value()); |
| 632 | // Hi-res scroll is 120 units per notch, scale down |
| 633 | Some(GrabEvent::Scroll { |
| 634 | horizontal: 0.0, |
| 635 | vertical: ev.value() as f64 * -0.125, // -15.0 / 120.0 |
| 636 | }) |
| 637 | } |
| 638 | RelativeAxisType::REL_HWHEEL_HI_RES => { |
| 639 | tracing::debug!("SCROLL: REL_HWHEEL_HI_RES value={}", ev.value()); |
| 640 | Some(GrabEvent::Scroll { |
| 641 | horizontal: ev.value() as f64 * 0.125, |
| 642 | vertical: 0.0, |
| 643 | }) |
| 644 | } |
| 645 | _ => None, |
| 646 | } |
| 647 | } |
| 648 | _ => None, |
| 649 | } |
| 650 | } |
| 651 | |
| 652 | /// Send synthetic key-up events via uinput for specified keycodes. |
| 653 | /// This creates a temporary virtual keyboard, sends the events, and destroys it. |
| 654 | /// Used to clear stale key state in libinput after releasing the evdev grab. |
| 655 | pub fn send_synthetic_key_ups(keycodes: &[u16]) -> Result<(), std::io::Error> { |
| 656 | use evdev::uinput::VirtualDeviceBuilder; |
| 657 | use evdev::{AttributeSet, Key}; |
| 658 | |
| 659 | if keycodes.is_empty() { |
| 660 | return Ok(()); |
| 661 | } |
| 662 | |
| 663 | tracing::debug!("Creating uinput device to send synthetic key-ups for {:?}", keycodes); |
| 664 | |
| 665 | // Build the key set for all keys we might send |
| 666 | let mut keys = AttributeSet::<Key>::new(); |
| 667 | for &keycode in keycodes { |
| 668 | keys.insert(Key::new(keycode)); |
| 669 | } |
| 670 | |
| 671 | // Also add common modifier keys in case we need them |
| 672 | keys.insert(Key::new(125)); // KEY_LEFTMETA |
| 673 | keys.insert(Key::new(126)); // KEY_RIGHTMETA |
| 674 | |
| 675 | // Create a virtual keyboard device |
| 676 | let mut device = VirtualDeviceBuilder::new()? |
| 677 | .name("hyprkvm-synthetic") |
| 678 | .with_keys(&keys)? |
| 679 | .build()?; |
| 680 | |
| 681 | // Brief pause to let the device be recognized |
| 682 | std::thread::sleep(std::time::Duration::from_millis(10)); |
| 683 | |
| 684 | // Send key-up events for each keycode |
| 685 | for &keycode in keycodes { |
| 686 | let key_up = evdev::InputEvent::new(evdev::EventType::KEY, keycode, 0); |
| 687 | let syn = evdev::InputEvent::new(evdev::EventType::SYNCHRONIZATION, 0, 0); |
| 688 | device.emit(&[key_up, syn])?; |
| 689 | tracing::debug!("Sent synthetic key-up for keycode {}", keycode); |
| 690 | } |
| 691 | |
| 692 | // Flush and brief pause before device is dropped |
| 693 | std::thread::sleep(std::time::Duration::from_millis(10)); |
| 694 | |
| 695 | tracing::debug!("Synthetic key-ups sent successfully"); |
| 696 | Ok(()) |
| 697 | } |
| 698 | |
| 699 | /// Send synthetic key-down events via uinput for specified keycodes. |
| 700 | /// This creates a temporary virtual keyboard, sends the events, and destroys it. |
| 701 | /// Used to inform libinput about keys that are physically held after ungrab. |
| 702 | /// NOTE: The device is destroyed after this function returns, which will trigger |
| 703 | /// an automatic key-up. Use `create_synthetic_keyboard_with_super_down` if you |
| 704 | /// need to keep the key pressed. |
| 705 | pub fn send_synthetic_key_downs(keycodes: &[u16]) -> Result<(), std::io::Error> { |
| 706 | use evdev::uinput::VirtualDeviceBuilder; |
| 707 | use evdev::{AttributeSet, Key}; |
| 708 | |
| 709 | if keycodes.is_empty() { |
| 710 | return Ok(()); |
| 711 | } |
| 712 | |
| 713 | tracing::debug!("Creating uinput device to send synthetic key-downs for {:?}", keycodes); |
| 714 | |
| 715 | // Build the key set for all keys we might send |
| 716 | let mut keys = AttributeSet::<Key>::new(); |
| 717 | for &keycode in keycodes { |
| 718 | keys.insert(Key::new(keycode)); |
| 719 | } |
| 720 | |
| 721 | // Create a virtual keyboard device |
| 722 | let mut device = VirtualDeviceBuilder::new()? |
| 723 | .name("hyprkvm-synthetic") |
| 724 | .with_keys(&keys)? |
| 725 | .build()?; |
| 726 | |
| 727 | // Brief pause to let the device be recognized |
| 728 | std::thread::sleep(std::time::Duration::from_millis(10)); |
| 729 | |
| 730 | // Send key-down events for each keycode |
| 731 | for &keycode in keycodes { |
| 732 | let key_down = evdev::InputEvent::new(evdev::EventType::KEY, keycode, 1); |
| 733 | let syn = evdev::InputEvent::new(evdev::EventType::SYNCHRONIZATION, 0, 0); |
| 734 | device.emit(&[key_down, syn])?; |
| 735 | tracing::debug!("Sent synthetic key-down for keycode {}", keycode); |
| 736 | } |
| 737 | |
| 738 | // Flush and brief pause before device is dropped |
| 739 | std::thread::sleep(std::time::Duration::from_millis(10)); |
| 740 | |
| 741 | tracing::debug!("Synthetic key-downs sent successfully"); |
| 742 | Ok(()) |
| 743 | } |
| 744 | |
| 745 | /// Create a virtual keyboard with Super (Left Meta) key pressed. |
| 746 | /// Returns the device which must be kept alive to maintain the key-down state. |
| 747 | /// When the device is dropped, the kernel will automatically send key-up. |
| 748 | fn create_synthetic_keyboard_with_super_down() -> Result<evdev::uinput::VirtualDevice, std::io::Error> { |
| 749 | use evdev::uinput::VirtualDeviceBuilder; |
| 750 | use evdev::{AttributeSet, Key}; |
| 751 | |
| 752 | tracing::debug!("Creating persistent synthetic keyboard with Super key-down"); |
| 753 | |
| 754 | // Build key set with Super keys |
| 755 | let mut keys = AttributeSet::<Key>::new(); |
| 756 | keys.insert(Key::new(125)); // KEY_LEFTMETA |
| 757 | keys.insert(Key::new(126)); // KEY_RIGHTMETA |
| 758 | |
| 759 | // Create virtual keyboard |
| 760 | let mut device = VirtualDeviceBuilder::new()? |
| 761 | .name("hyprkvm-super-hold") |
| 762 | .with_keys(&keys)? |
| 763 | .build()?; |
| 764 | |
| 765 | // Brief pause to let the device be recognized |
| 766 | std::thread::sleep(std::time::Duration::from_millis(20)); |
| 767 | |
| 768 | // Send Super key-down |
| 769 | let key_down = evdev::InputEvent::new(evdev::EventType::KEY, 125, 1); // KEY_LEFTMETA |
| 770 | let syn = evdev::InputEvent::new(evdev::EventType::SYNCHRONIZATION, 0, 0); |
| 771 | device.emit(&[key_down, syn])?; |
| 772 | |
| 773 | tracing::info!("Synthetic keyboard created with Super key-down, device will be kept alive"); |
| 774 | Ok(device) |
| 775 | } |
| 776 | |
| 777 | #[derive(Debug, thiserror::Error)] |
| 778 | pub enum EvdevGrabError { |
| 779 | #[error("No input devices found")] |
| 780 | NoDevices, |
| 781 | |
| 782 | #[error("Thread error: {0}")] |
| 783 | Thread(String), |
| 784 | |
| 785 | #[error("Device error: {0}")] |
| 786 | Device(String), |
| 787 | } |
| 788 |