@@ -129,6 +129,7 @@ fn find_input_devices() -> Vec<PathBuf> { |
| 129 | 129 | } |
| 130 | 130 | |
| 131 | 131 | // Method 2: Scan all /dev/input/event* and check capabilities |
| 132 | + // Be more restrictive - only grab devices that are ACTUALLY keyboards or mice |
| 132 | 133 | if let Ok(entries) = fs::read_dir("/dev/input") { |
| 133 | 134 | for entry in entries.flatten() { |
| 134 | 135 | let name = entry.file_name().to_string_lossy().to_string(); |
@@ -140,10 +141,28 @@ fn find_input_devices() -> Vec<PathBuf> { |
| 140 | 141 | |
| 141 | 142 | // Try to open and check if it's a keyboard or mouse |
| 142 | 143 | if let Ok(dev) = Device::open(&path) { |
| 143 | | - let has_keys = dev.supported_keys().map(|k| k.iter().count() > 0).unwrap_or(false); |
| 144 | + let dev_name = dev.name().unwrap_or("unknown").to_lowercase(); |
| 145 | + |
| 146 | + // Skip devices that are clearly NOT keyboard/mouse |
| 147 | + if dev_name.contains("power") |
| 148 | + || dev_name.contains("sleep") |
| 149 | + || dev_name.contains("button") |
| 150 | + || dev_name.contains("wmi") |
| 151 | + || dev_name.contains("hotkey") |
| 152 | + || dev_name.contains("consumer control") |
| 153 | + || dev_name.contains("video bus") |
| 154 | + || dev_name.contains("dualsense") |
| 155 | + || dev_name.contains("dualshock") |
| 156 | + || dev_name.contains("controller touchpad") |
| 157 | + { |
| 158 | + tracing::debug!("Skipping non-keyboard/mouse device: {}", dev_name); |
| 159 | + continue; |
| 160 | + } |
| 161 | + |
| 162 | + let has_keys = dev.supported_keys().map(|k| k.iter().count() > 10).unwrap_or(false); |
| 144 | 163 | let has_rel = dev.supported_relative_axes().map(|r| r.iter().count() > 0).unwrap_or(false); |
| 145 | 164 | |
| 146 | | - // Include if it has keys (keyboard) or relative axes (mouse) |
| 165 | + // A real keyboard has many keys (>10), a real mouse has relative axes |
| 147 | 166 | if has_keys || has_rel { |
| 148 | 167 | let dev_name = dev.name().unwrap_or("unknown"); |
| 149 | 168 | tracing::debug!("Found input device: {} at {} (keys={}, rel={})", |
@@ -181,6 +200,8 @@ fn run_evdev_grabber( |
| 181 | 200 | Idle, |
| 182 | 201 | Grabbed, |
| 183 | 202 | Recovery, |
| 203 | + /// Post-recovery: waiting for Super key release while keeping synthetic key-down active |
| 204 | + PostRecovery, |
| 184 | 205 | } |
| 185 | 206 | |
| 186 | 207 | let mut devices: HashMap<PathBuf, Device> = HashMap::new(); |
@@ -191,6 +212,9 @@ fn run_evdev_grabber( |
| 191 | 212 | let mut super_was_held_at_start = false; // Track if Super was held when recovery started |
| 192 | 213 | let mut recovery_hotkey_sent = false; |
| 193 | 214 | |
| 215 | + // Virtual keyboard for synthetic Super key-down (kept alive in PostRecovery state) |
| 216 | + let mut synthetic_keyboard: Option<evdev::uinput::VirtualDevice> = None; |
| 217 | + |
| 194 | 218 | // Key codes |
| 195 | 219 | const KEY_LEFTMETA: u16 = 125; |
| 196 | 220 | const KEY_RIGHTMETA: u16 = 126; |
@@ -211,6 +235,9 @@ fn run_evdev_grabber( |
| 211 | 235 | devices.clear(); |
| 212 | 236 | tracing::info!("Opening and grabbing input devices..."); |
| 213 | 237 | |
| 238 | + // Small delay before starting grabs to let any pending events settle |
| 239 | + std::thread::sleep(std::time::Duration::from_millis(50)); |
| 240 | + |
| 214 | 241 | for path in &device_paths { |
| 215 | 242 | match Device::open(path) { |
| 216 | 243 | Ok(mut dev) => { |
@@ -228,6 +255,8 @@ fn run_evdev_grabber( |
| 228 | 255 | tracing::warn!("Cannot grab {}: {}", name, e); |
| 229 | 256 | } |
| 230 | 257 | } |
| 258 | + // Small delay between grabs to avoid overwhelming libinput |
| 259 | + std::thread::sleep(std::time::Duration::from_millis(10)); |
| 231 | 260 | } |
| 232 | 261 | Err(e) => { |
| 233 | 262 | tracing::warn!("Failed to open {}: {}", path.display(), e); |
@@ -422,8 +451,95 @@ fn run_evdev_grabber( |
| 422 | 451 | // End recovery mode if hotkey was detected or Super was released |
| 423 | 452 | if should_end_recovery { |
| 424 | 453 | tracing::info!("Recovery mode ended ({})", end_reason); |
| 425 | | - devices.clear(); |
| 426 | 454 | recovery_active.store(0, Ordering::SeqCst); |
| 455 | + |
| 456 | + // If we detected a hotkey and Super is still held, we need to send a |
| 457 | + // synthetic Super key-down via uinput. This informs libinput that Super |
| 458 | + // is pressed, since it never saw the original key-down (it was grabbed). |
| 459 | + // CRITICAL: We must keep the virtual device alive until Super is released, |
| 460 | + // otherwise the kernel will auto-send key-up when the device is destroyed. |
| 461 | + if recovery_hotkey_sent && super_held { |
| 462 | + tracing::info!("RECOVERY: Super still held, entering PostRecovery to maintain synthetic key-down"); |
| 463 | + |
| 464 | + // Create virtual keyboard and send Super key-down |
| 465 | + match create_synthetic_keyboard_with_super_down() { |
| 466 | + Ok(virt_dev) => { |
| 467 | + synthetic_keyboard = Some(virt_dev); |
| 468 | + // Keep devices open to monitor for Super release |
| 469 | + state = State::PostRecovery; |
| 470 | + tracing::info!("PostRecovery: synthetic keyboard created, monitoring for Super release"); |
| 471 | + } |
| 472 | + Err(e) => { |
| 473 | + tracing::error!("Failed to create synthetic keyboard: {}", e); |
| 474 | + devices.clear(); |
| 475 | + state = State::Idle; |
| 476 | + } |
| 477 | + } |
| 478 | + } else { |
| 479 | + // Super already released or no hotkey detected, clean up normally |
| 480 | + devices.clear(); |
| 481 | + state = State::Idle; |
| 482 | + } |
| 483 | + } |
| 484 | + } |
| 485 | + |
| 486 | + State::PostRecovery => { |
| 487 | + // In post-recovery mode, we're keeping the synthetic keyboard alive |
| 488 | + // with Super key-down. Monitor physical keyboard for Super release. |
| 489 | + if is_active { |
| 490 | + // New grab starting, clean up and transition |
| 491 | + tracing::info!("PostRecovery: new grab requested, cleaning up"); |
| 492 | + if let Some(ref mut virt_dev) = synthetic_keyboard { |
| 493 | + // Send Super key-up before destroying |
| 494 | + let key_up = evdev::InputEvent::new(evdev::EventType::KEY, KEY_LEFTMETA, 0); |
| 495 | + let syn = evdev::InputEvent::new(evdev::EventType::SYNCHRONIZATION, 0, 0); |
| 496 | + let _ = virt_dev.emit(&[key_up, syn]); |
| 497 | + } |
| 498 | + synthetic_keyboard = None; |
| 499 | + devices.clear(); |
| 500 | + state = State::Idle; |
| 501 | + continue; |
| 502 | + } |
| 503 | + |
| 504 | + // Monitor for Super key release on physical keyboard |
| 505 | + let mut super_released = false; |
| 506 | + for dev in devices.values_mut() { |
| 507 | + if let Ok(events) = dev.fetch_events() { |
| 508 | + for ev in events { |
| 509 | + if let InputEventKind::Key(key) = ev.kind() { |
| 510 | + let keycode = key.code(); |
| 511 | + let released = ev.value() == 0; |
| 512 | + |
| 513 | + // Check for Super release |
| 514 | + if (keycode == KEY_LEFTMETA || keycode == KEY_RIGHTMETA) && released { |
| 515 | + super_released = true; |
| 516 | + break; |
| 517 | + } |
| 518 | + } |
| 519 | + } |
| 520 | + } |
| 521 | + if super_released { |
| 522 | + break; |
| 523 | + } |
| 524 | + } |
| 525 | + |
| 526 | + // Handle Super release outside the borrow |
| 527 | + if super_released { |
| 528 | + tracing::info!("PostRecovery: Super released, sending synthetic key-up and cleaning up"); |
| 529 | + |
| 530 | + // Send synthetic Super key-up |
| 531 | + if let Some(ref mut virt_dev) = synthetic_keyboard { |
| 532 | + let key_up = evdev::InputEvent::new(evdev::EventType::KEY, KEY_LEFTMETA, 0); |
| 533 | + let syn = evdev::InputEvent::new(evdev::EventType::SYNCHRONIZATION, 0, 0); |
| 534 | + if let Err(e) = virt_dev.emit(&[key_up, syn]) { |
| 535 | + tracing::warn!("Failed to send synthetic Super key-up: {}", e); |
| 536 | + } |
| 537 | + } |
| 538 | + |
| 539 | + // Clean up |
| 540 | + synthetic_keyboard = None; |
| 541 | + devices.clear(); |
| 542 | + super_held = false; |
| 427 | 543 | state = State::Idle; |
| 428 | 544 | } |
| 429 | 545 | } |
@@ -530,6 +646,84 @@ pub fn send_synthetic_key_ups(keycodes: &[u16]) -> Result<(), std::io::Error> { |
| 530 | 646 | Ok(()) |
| 531 | 647 | } |
| 532 | 648 | |
| 649 | +/// Send synthetic key-down events via uinput for specified keycodes. |
| 650 | +/// This creates a temporary virtual keyboard, sends the events, and destroys it. |
| 651 | +/// Used to inform libinput about keys that are physically held after ungrab. |
| 652 | +/// NOTE: The device is destroyed after this function returns, which will trigger |
| 653 | +/// an automatic key-up. Use `create_synthetic_keyboard_with_super_down` if you |
| 654 | +/// need to keep the key pressed. |
| 655 | +pub fn send_synthetic_key_downs(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-downs 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 | + // Create a virtual keyboard device |
| 672 | + let mut device = VirtualDeviceBuilder::new()? |
| 673 | + .name("hyprkvm-synthetic") |
| 674 | + .with_keys(&keys)? |
| 675 | + .build()?; |
| 676 | + |
| 677 | + // Brief pause to let the device be recognized |
| 678 | + std::thread::sleep(std::time::Duration::from_millis(10)); |
| 679 | + |
| 680 | + // Send key-down events for each keycode |
| 681 | + for &keycode in keycodes { |
| 682 | + let key_down = evdev::InputEvent::new(evdev::EventType::KEY, keycode, 1); |
| 683 | + let syn = evdev::InputEvent::new(evdev::EventType::SYNCHRONIZATION, 0, 0); |
| 684 | + device.emit(&[key_down, syn])?; |
| 685 | + tracing::debug!("Sent synthetic key-down for keycode {}", keycode); |
| 686 | + } |
| 687 | + |
| 688 | + // Flush and brief pause before device is dropped |
| 689 | + std::thread::sleep(std::time::Duration::from_millis(10)); |
| 690 | + |
| 691 | + tracing::debug!("Synthetic key-downs sent successfully"); |
| 692 | + Ok(()) |
| 693 | +} |
| 694 | + |
| 695 | +/// Create a virtual keyboard with Super (Left Meta) key pressed. |
| 696 | +/// Returns the device which must be kept alive to maintain the key-down state. |
| 697 | +/// When the device is dropped, the kernel will automatically send key-up. |
| 698 | +fn create_synthetic_keyboard_with_super_down() -> Result<evdev::uinput::VirtualDevice, std::io::Error> { |
| 699 | + use evdev::uinput::VirtualDeviceBuilder; |
| 700 | + use evdev::{AttributeSet, Key}; |
| 701 | + |
| 702 | + tracing::debug!("Creating persistent synthetic keyboard with Super key-down"); |
| 703 | + |
| 704 | + // Build key set with Super keys |
| 705 | + let mut keys = AttributeSet::<Key>::new(); |
| 706 | + keys.insert(Key::new(125)); // KEY_LEFTMETA |
| 707 | + keys.insert(Key::new(126)); // KEY_RIGHTMETA |
| 708 | + |
| 709 | + // Create virtual keyboard |
| 710 | + let mut device = VirtualDeviceBuilder::new()? |
| 711 | + .name("hyprkvm-super-hold") |
| 712 | + .with_keys(&keys)? |
| 713 | + .build()?; |
| 714 | + |
| 715 | + // Brief pause to let the device be recognized |
| 716 | + std::thread::sleep(std::time::Duration::from_millis(20)); |
| 717 | + |
| 718 | + // Send Super key-down |
| 719 | + let key_down = evdev::InputEvent::new(evdev::EventType::KEY, 125, 1); // KEY_LEFTMETA |
| 720 | + let syn = evdev::InputEvent::new(evdev::EventType::SYNCHRONIZATION, 0, 0); |
| 721 | + device.emit(&[key_down, syn])?; |
| 722 | + |
| 723 | + tracing::info!("Synthetic keyboard created with Super key-down, device will be kept alive"); |
| 724 | + Ok(device) |
| 725 | +} |
| 726 | + |
| 533 | 727 | #[derive(Debug, thiserror::Error)] |
| 534 | 728 | pub enum EvdevGrabError { |
| 535 | 729 | #[error("No input devices found")] |