tenseleyflow/hyprkvm / 29b886d

Browse files

fix: resolve CLI switch issues with input grabbing and Super key handling

This commit fixes two major issues with CLI-initiated switches:

1. Prompt spam when running `hyprkvm-ctl switch`:
- Filter non-keyboard/mouse devices (power buttons, game controllers, etc.)
- Add 50ms delay before grabbing and 10ms between device grabs
- Require >10 keys for keyboard detection (was >0)

2. Super key stuck after keyboard return from CLI switch:
- Track `keyboard_initiated` flag through transfer state machine
- Only send synthetic Super key-down for keyboard-initiated transfers
- Add PostRecovery state to maintain synthetic Super via uinput
- Keep virtual keyboard alive until physical Super key is released

The PostRecovery state ensures libinput knows Super is pressed after
ungrab, enabling subsequent Super+Arrow focus moves to work correctly.
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
29b886da97e4b02154db70832427a693c3d68cee
Parents
2590f42
Tree
68498dd

3 changed files

StatusFile+-
M hyprkvm-daemon/src/input/evdev_grab.rs 197 3
M hyprkvm-daemon/src/main.rs 22 12
M hyprkvm-daemon/src/transfer/manager.rs 18 4
hyprkvm-daemon/src/input/evdev_grab.rsmodified
@@ -129,6 +129,7 @@ fn find_input_devices() -> Vec<PathBuf> {
129129
     }
130130
 
131131
     // Method 2: Scan all /dev/input/event* and check capabilities
132
+    // Be more restrictive - only grab devices that are ACTUALLY keyboards or mice
132133
     if let Ok(entries) = fs::read_dir("/dev/input") {
133134
         for entry in entries.flatten() {
134135
             let name = entry.file_name().to_string_lossy().to_string();
@@ -140,10 +141,28 @@ fn find_input_devices() -> Vec<PathBuf> {
140141
 
141142
                 // Try to open and check if it's a keyboard or mouse
142143
                 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);
144163
                     let has_rel = dev.supported_relative_axes().map(|r| r.iter().count() > 0).unwrap_or(false);
145164
 
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
147166
                     if has_keys || has_rel {
148167
                         let dev_name = dev.name().unwrap_or("unknown");
149168
                         tracing::debug!("Found input device: {} at {} (keys={}, rel={})",
@@ -181,6 +200,8 @@ fn run_evdev_grabber(
181200
         Idle,
182201
         Grabbed,
183202
         Recovery,
203
+        /// Post-recovery: waiting for Super key release while keeping synthetic key-down active
204
+        PostRecovery,
184205
     }
185206
 
186207
     let mut devices: HashMap<PathBuf, Device> = HashMap::new();
@@ -191,6 +212,9 @@ fn run_evdev_grabber(
191212
     let mut super_was_held_at_start = false; // Track if Super was held when recovery started
192213
     let mut recovery_hotkey_sent = false;
193214
 
215
+    // Virtual keyboard for synthetic Super key-down (kept alive in PostRecovery state)
216
+    let mut synthetic_keyboard: Option<evdev::uinput::VirtualDevice> = None;
217
+
194218
     // Key codes
195219
     const KEY_LEFTMETA: u16 = 125;
196220
     const KEY_RIGHTMETA: u16 = 126;
@@ -211,6 +235,9 @@ fn run_evdev_grabber(
211235
                     devices.clear();
212236
                     tracing::info!("Opening and grabbing input devices...");
213237
 
238
+                    // Small delay before starting grabs to let any pending events settle
239
+                    std::thread::sleep(std::time::Duration::from_millis(50));
240
+
214241
                     for path in &device_paths {
215242
                         match Device::open(path) {
216243
                             Ok(mut dev) => {
@@ -228,6 +255,8 @@ fn run_evdev_grabber(
228255
                                         tracing::warn!("Cannot grab {}: {}", name, e);
229256
                                     }
230257
                                 }
258
+                                // Small delay between grabs to avoid overwhelming libinput
259
+                                std::thread::sleep(std::time::Duration::from_millis(10));
231260
                             }
232261
                             Err(e) => {
233262
                                 tracing::warn!("Failed to open {}: {}", path.display(), e);
@@ -422,8 +451,95 @@ fn run_evdev_grabber(
422451
                 // End recovery mode if hotkey was detected or Super was released
423452
                 if should_end_recovery {
424453
                     tracing::info!("Recovery mode ended ({})", end_reason);
425
-                    devices.clear();
426454
                     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;
427543
                     state = State::Idle;
428544
                 }
429545
             }
@@ -530,6 +646,84 @@ pub fn send_synthetic_key_ups(keycodes: &[u16]) -> Result<(), std::io::Error> {
530646
     Ok(())
531647
 }
532648
 
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
+
533727
 #[derive(Debug, thiserror::Error)]
534728
 pub enum EvdevGrabError {
535729
     #[error("No input devices found")]
hyprkvm-daemon/src/main.rsmodified
@@ -721,6 +721,7 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
721721
                                         cursor_pos,
722722
                                         screen_height,
723723
                                         screen_width,
724
+                                        true, // keyboard-initiated (recovery hotkey)
724725
                                     ).await {
725726
                                         tracing::error!("Failed to initiate transfer from recovery hotkey: {}", e);
726727
                                     }
@@ -805,6 +806,7 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
805806
                                 edge_event.position,
806807
                                 screen_height,
807808
                                 screen_width,
809
+                                false, // not keyboard-initiated (mouse edge)
808810
                             ).await {
809811
                                 tracing::warn!("Failed to initiate transfer: {}", e);
810812
                             }
@@ -907,6 +909,7 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
907909
                                                             (cx, cy),
908910
                                                             screen_height,
909911
                                                             screen_width,
912
+                                                            false, // not keyboard-initiated (cursor edge)
910913
                                                         ).await {
911914
                                                             tracing::warn!("Failed to initiate transfer: {}", e);
912915
                                                         }
@@ -1101,27 +1104,30 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
11011104
                             transfer_manager.abort().await;
11021105
                         }
11031106
                     }
1104
-                    transfer::TransferEvent::StartCapture { direction: cap_dir } => {
1105
-                        info!("Starting input capture for {:?}", cap_dir);
1107
+                    transfer::TransferEvent::StartCapture { direction: cap_dir, keyboard_initiated } => {
1108
+                        info!("StartCapture event received for {:?}, keyboard_initiated={}", cap_dir, keyboard_initiated);
11061109
                         capture_direction = Some(cap_dir);
11071110
 
1108
-                        // Send synthetic Super key-down as first event.
1109
-                        // The transfer was likely initiated via Super+Arrow keybinding,
1110
-                        // which means Super was already held when the grab started.
1111
-                        // The evdev grabber won't see the initial Super key-down,
1112
-                        // so we need to send it explicitly so the destination knows
1113
-                        // Super is pressed for subsequent keybindings.
1114
-                        {
1111
+                        // Only send synthetic Super key-down if the transfer was keyboard-initiated.
1112
+                        // When triggered via Super+Arrow keybinding, Super was already held when
1113
+                        // the grab started. The evdev grabber won't see the initial Super key-down,
1114
+                        // so we send it explicitly so the destination knows Super is pressed.
1115
+                        // For CLI-initiated switches, the user isn't holding Super, so don't send it.
1116
+                        if keyboard_initiated {
11151117
                             let mut peers_guard = peers.write().await;
11161118
                             if let Some(peer) = peers_guard.get_mut(&cap_dir) {
11171119
                                 let super_down = input::GrabEvent::KeyDown { keycode: 125 }; // KEY_LEFTMETA
11181120
                                 let payload = super_down.to_protocol(input_sequence);
11191121
                                 input_sequence += 1;
1120
-                                tracing::debug!("Sending synthetic Super key-down to destination");
1122
+                                tracing::debug!("Sending synthetic Super key-down to destination (keyboard-initiated)");
11211123
                                 if let Err(e) = peer.send(&Message::InputEvent(payload)).await {
11221124
                                     tracing::error!("Failed to send synthetic Super: {}", e);
11231125
                                 }
11241126
                             }
1127
+                        } else {
1128
+                            tracing::debug!("Skipping synthetic Super key-down (CLI-initiated switch)");
1129
+                            // Add delay for CLI-initiated switches to let the terminal settle
1130
+                            tokio::time::sleep(std::time::Duration::from_millis(100)).await;
11251131
                         }
11261132
 
11271133
                         input_grabber.start();
@@ -1354,6 +1360,7 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
13541360
                                             cursor_pos,
13551361
                                             screen_height,
13561362
                                             screen_width,
1363
+                                            true, // keyboard-initiated (IPC Move from keybind)
13571364
                                         ).await {
13581365
                                             IpcResponse::Error { message: format!("Transfer failed: {}", e) }
13591366
                                         } else {
@@ -1376,6 +1383,7 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
13761383
                                         cursor_pos,
13771384
                                         screen_height,
13781385
                                         screen_width,
1386
+                                        true, // keyboard-initiated (IPC Move from keybind)
13791387
                                     ).await {
13801388
                                         IpcResponse::Error { message: format!("Transfer failed: {}", e) }
13811389
                                     } else {
@@ -1568,14 +1576,16 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
15681576
                                         Err(_) => ((0, 0), 1920, 1080), // Fallback
15691577
                                     };
15701578
 
1571
-                                    // Initiate transfer
1572
-                                    match transfer_manager.initiate_transfer(dir, cursor_pos, screen_height, screen_width).await {
1579
+                                    // Initiate transfer (CLI-initiated, not keyboard)
1580
+                                    info!("IPC Switch: calling initiate_transfer");
1581
+                                    match transfer_manager.initiate_transfer(dir, cursor_pos, screen_height, screen_width, false).await {
15731582
                                         Ok(()) => {
15741583
                                             let machine_name = config.machines.neighbors
15751584
                                                 .iter()
15761585
                                                 .find(|n| n.direction == dir)
15771586
                                                 .map(|n| n.name.clone())
15781587
                                                 .unwrap_or_else(|| format!("{:?}", dir));
1588
+                                            info!("IPC Switch: initiate_transfer succeeded, returning response to CLI");
15791589
                                             IpcResponse::Transferred { to_machine: machine_name }
15801590
                                         }
15811591
                                         Err(e) => IpcResponse::Error {
hyprkvm-daemon/src/transfer/manager.rsmodified
@@ -23,6 +23,8 @@ pub enum TransferState {
2323
         target: Direction,
2424
         transfer_id: u64,
2525
         started_at: Instant,
26
+        /// True if transfer was triggered via keyboard (Super+Arrow)
27
+        keyboard_initiated: bool,
2628
     },
2729
 
2830
     /// We sent control away, forwarding input
@@ -30,6 +32,8 @@ pub enum TransferState {
3032
         target: Direction,
3133
         transfer_id: u64,
3234
         entered_at: Instant,
35
+        /// True if transfer was triggered via keyboard (Super+Arrow)
36
+        keyboard_initiated: bool,
3337
     },
3438
 
3539
     /// We received control from another machine
@@ -58,7 +62,9 @@ impl TransferState {
5862
 #[derive(Debug, Clone)]
5963
 pub enum TransferEvent {
6064
     /// Start capturing and forwarding input
61
-    StartCapture { direction: Direction },
65
+    /// `keyboard_initiated` is true if the transfer was triggered via keyboard (Super+Arrow),
66
+    /// false if triggered via CLI or other non-keyboard means
67
+    StartCapture { direction: Direction, keyboard_initiated: bool },
6268
     /// Stop capturing, return to local
6369
     StopCapture,
6470
     /// Start injecting received input
@@ -102,12 +108,15 @@ impl TransferManager {
102108
     }
103109
 
104110
     /// Initiate transfer to a direction (mouse or keyboard edge hit)
111
+    /// `keyboard_initiated` should be true if this was triggered via Super+Arrow keybind,
112
+    /// false if triggered via CLI, mouse edge, or other non-keyboard means
105113
     pub async fn initiate_transfer(
106114
         &self,
107115
         direction: Direction,
108116
         cursor_pos: (i32, i32),
109117
         screen_height: u32,
110118
         screen_width: u32,
119
+        keyboard_initiated: bool,
111120
     ) -> Result<(), TransferError> {
112121
         let mut state = self.state.write().await;
113122
 
@@ -147,6 +156,7 @@ impl TransferManager {
147156
             target: direction,
148157
             transfer_id,
149158
             started_at: Instant::now(),
159
+            keyboard_initiated,
150160
         };
151161
 
152162
         // Send Enter message
@@ -176,6 +186,7 @@ impl TransferManager {
176186
             TransferState::Initiating {
177187
                 target,
178188
                 transfer_id,
189
+                keyboard_initiated,
179190
                 ..
180191
             } => {
181192
                 if *transfer_id != ack.transfer_id {
@@ -196,22 +207,25 @@ impl TransferManager {
196207
                 }
197208
 
198209
                 tracing::info!(
199
-                    "Transfer accepted, cursor at {:?}",
200
-                    ack.actual_cursor_pos
210
+                    "Transfer accepted, cursor at {:?}, keyboard_initiated={}",
211
+                    ack.actual_cursor_pos,
212
+                    keyboard_initiated
201213
                 );
202214
 
203215
                 let direction = *target;
204216
                 let tid = *transfer_id;
217
+                let kbd_init = *keyboard_initiated;
205218
 
206219
                 *state = TransferState::RemoteActive {
207220
                     target: direction,
208221
                     transfer_id: tid,
209222
                     entered_at: Instant::now(),
223
+                    keyboard_initiated: kbd_init,
210224
                 };
211225
 
212226
                 // Start capturing input
213227
                 self.event_tx
214
-                    .send(TransferEvent::StartCapture { direction })
228
+                    .send(TransferEvent::StartCapture { direction, keyboard_initiated: kbd_init })
215229
                     .await
216230
                     .map_err(|_| TransferError::ChannelClosed)?;
217231