Rust · 33509 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, 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