tenseleyflow/hyprkvm / 4ef6cee

Browse files

fix: direction-specific cooldowns to prevent eaten keypresses

- Reduce cooldowns from 1000ms to 300ms (spurious presses happen within ~100-400ms)
- ReceivedControl cooldown now only blocks the 'from' direction, not all directions
- Return cooldown blocks specific direction and clears after blocking once
- Prevents bounce-back at edge while allowing normal navigation
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
4ef6ceee6a5587ecf6f103021e5f32bda421fc5f
Parents
3d82f64
Tree
88f1466

1 changed file

StatusFile+-
M hyprkvm-daemon/src/main.rs 73 44
hyprkvm-daemon/src/main.rsmodified
@@ -332,8 +332,9 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
332
     const EDGE_DWELL_MS: u64 = 50; // How long cursor must be at edge to trigger
332
     const EDGE_DWELL_MS: u64 = 50; // How long cursor must be at edge to trigger
333
 
333
 
334
     // Cooldown after control returns to prevent immediate bounce-back
334
     // Cooldown after control returns to prevent immediate bounce-back
335
-    let mut last_control_return: Option<std::time::Instant> = None;
335
+    // Stores (timestamp, return_direction) - the direction is the arrow key the user pressed to return
336
-    const CONTROL_RETURN_COOLDOWN_MS: u64 = 1000; // 1000ms cooldown after control returns (prevents bounce-back)
336
+    let mut last_control_return: Option<(std::time::Instant, Direction)> = None;
337
+    const CONTROL_RETURN_COOLDOWN_MS: u64 = 300; // 300ms cooldown - spurious keypresses happen within ~100-400ms
337
 
338
 
338
     // Connection storage: direction -> peer connection
339
     // Connection storage: direction -> peer connection
339
     let peers: Arc<RwLock<HashMap<Direction, network::FramedConnection>>> =
340
     let peers: Arc<RwLock<HashMap<Direction, network::FramedConnection>>> =
@@ -991,7 +992,7 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
991
                                         // Set cooldown to prevent the IPC Move callback from initiating transfer
992
                                         // Set cooldown to prevent the IPC Move callback from initiating transfer
992
                                         // (Hyprland fires IPC Move after movefocus, which would see at_edge=true
993
                                         // (Hyprland fires IPC Move after movefocus, which would see at_edge=true
993
                                         // after we moved to the edge window)
994
                                         // after we moved to the edge window)
994
-                                        last_control_return = Some(std::time::Instant::now());
995
+                                        last_control_return = Some((std::time::Instant::now(), direction));
995
                                     }
996
                                     }
996
                                     Err(e) => tracing::error!("  RECOVERY movefocus failed: {}", e),
997
                                     Err(e) => tracing::error!("  RECOVERY movefocus failed: {}", e),
997
                                 }
998
                                 }
@@ -1053,15 +1054,15 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
1053
                                     tracing::warn!("Failed to return control: {}", e);
1054
                                     tracing::warn!("Failed to return control: {}", e);
1054
                                 } else {
1055
                                 } else {
1055
                                     // Set cooldown to prevent immediate re-transfer
1056
                                     // Set cooldown to prevent immediate re-transfer
1056
-                                    last_control_return = Some(std::time::Instant::now());
1057
+                                    last_control_return = Some((std::time::Instant::now(), direction));
1057
                                 }
1058
                                 }
1058
                                 continue;
1059
                                 continue;
1059
                             }
1060
                             }
1060
                         }
1061
                         }
1061
 
1062
 
1062
-                        // Check cooldown to prevent bounce-back loops
1063
+                        // Check cooldown to prevent bounce-back loops (only for matching direction)
1063
-                        if let Some(last_return) = last_control_return {
1064
+                        if let Some((last_return, cooldown_dir)) = last_control_return {
1064
-                            if last_return.elapsed().as_millis() < CONTROL_RETURN_COOLDOWN_MS as u128 {
1065
+                            if cooldown_dir == direction && last_return.elapsed().as_millis() < CONTROL_RETURN_COOLDOWN_MS as u128 {
1065
                                 tracing::debug!("EDGE: {:?} - in cooldown, ignoring", direction);
1066
                                 tracing::debug!("EDGE: {:?} - in cooldown, ignoring", direction);
1066
                                 continue;
1067
                                 continue;
1067
                             }
1068
                             }
@@ -1212,7 +1213,7 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
1212
                                                                 tracing::warn!("Failed to return control: {}", e);
1213
                                                                 tracing::warn!("Failed to return control: {}", e);
1213
                                                             } else {
1214
                                                             } else {
1214
                                                                 // Set cooldown to prevent immediate re-transfer
1215
                                                                 // Set cooldown to prevent immediate re-transfer
1215
-                                                                last_control_return = Some(std::time::Instant::now());
1216
+                                                                last_control_return = Some((std::time::Instant::now(), edge_dir));
1216
                                                             }
1217
                                                             }
1217
                                                             edge_dwell_start = None;
1218
                                                             edge_dwell_start = None;
1218
                                                             continue;
1219
                                                             continue;
@@ -1229,9 +1230,9 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
1229
                                                         );
1230
                                                         );
1230
                                                     }
1231
                                                     }
1231
 
1232
 
1232
-                                                    // Check cooldown to prevent bounce-back
1233
+                                                    // Check cooldown to prevent bounce-back (only for matching direction)
1233
-                                                    if let Some(last_return) = last_control_return {
1234
+                                                    if let Some((last_return, cooldown_dir)) = last_control_return {
1234
-                                                        if last_return.elapsed().as_millis() < CONTROL_RETURN_COOLDOWN_MS as u128 {
1235
+                                                        if cooldown_dir == edge_dir && last_return.elapsed().as_millis() < CONTROL_RETURN_COOLDOWN_MS as u128 {
1235
                                                             tracing::debug!("CURSOR EDGE: {:?} - in cooldown", edge_dir);
1236
                                                             tracing::debug!("CURSOR EDGE: {:?} - in cooldown", edge_dir);
1236
                                                             edge_dwell_start = None;
1237
                                                             edge_dwell_start = None;
1237
                                                             continue;
1238
                                                             continue;
@@ -1370,8 +1371,10 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
1370
                                         }
1371
                                         }
1371
                                         // Set cooldown to prevent bounce-back loop
1372
                                         // Set cooldown to prevent bounce-back loop
1372
                                         // When we receive Leave, control is returning to us
1373
                                         // When we receive Leave, control is returning to us
1373
-                                        last_control_return = Some(std::time::Instant::now());
1374
+                                        // The return direction is opposite of peer direction (user pressed Down to return from Up peer)
1374
-                                        tracing::debug!("Set control return cooldown");
1375
+                                        let return_direction = direction.opposite();
1376
+                                        last_control_return = Some((std::time::Instant::now(), return_direction));
1377
+                                        tracing::debug!("Set control return cooldown for direction {:?}", return_direction);
1375
                                     }
1378
                                     }
1376
                                     Message::LeaveAck => {
1379
                                     Message::LeaveAck => {
1377
                                         info!("Received LeaveAck");
1380
                                         info!("Received LeaveAck");
@@ -1660,6 +1663,19 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
1660
                                 emu.keyboard.reset_all_keys();
1663
                                 emu.keyboard.reset_all_keys();
1661
                             }
1664
                             }
1662
                         }
1665
                         }
1666
+
1667
+                        // CRITICAL: Set cooldown after receiving Leave from peer
1668
+                        // When peer sends Leave (e.g., user pressed Super+Down on cachyos to return),
1669
+                        // the Down key might still be held when we release the grab. Libinput sees
1670
+                        // Super (still held) + Down = fires IPC Move Down, causing unwanted navigation.
1671
+                        // This cooldown prevents that specific direction from triggering movefocus.
1672
+                        if let Some(capture_dir) = was_capturing_direction {
1673
+                            // Return direction is opposite of capture direction
1674
+                            // e.g., if we were capturing to Up, user pressed Down to return
1675
+                            let return_direction = capture_dir.opposite();
1676
+                            last_control_return = Some((std::time::Instant::now(), return_direction));
1677
+                            tracing::info!("Set cooldown after StopCapture for direction {:?} (peer-initiated return)", return_direction);
1678
+                        }
1663
                     }
1679
                     }
1664
                     transfer::TransferEvent::StartInjection { from } => {
1680
                     transfer::TransferEvent::StartInjection { from } => {
1665
                         info!("Starting input injection from {:?}", from);
1681
                         info!("Starting input injection from {:?}", from);
@@ -1747,14 +1763,16 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
1747
                         let current_state = transfer_manager.state().await;
1763
                         let current_state = transfer_manager.state().await;
1748
                         info!("IPC Move {:?}: state={:?}", direction, current_state);
1764
                         info!("IPC Move {:?}: state={:?}", direction, current_state);
1749
 
1765
 
1750
-                        // Early exit: if in ReceivedControl and within cooldown, ignore
1766
+                        // Early exit: if in ReceivedControl and within cooldown for the ENTRY direction, ignore
1751
                         // (prevents the Super+Arrow keypress that triggered the transfer from
1767
                         // (prevents the Super+Arrow keypress that triggered the transfer from
1752
                         // causing a double navigation on the receiving machine)
1768
                         // causing a double navigation on the receiving machine)
1753
-                        if let transfer::TransferState::ReceivedControl { entered_at, .. } = &current_state {
1769
+                        // Only block the direction that would return to source, not all directions
1754
-                            const RECEIVED_CONTROL_IPC_COOLDOWN_MS: u128 = 1000;
1770
+                        if let transfer::TransferState::ReceivedControl { entered_at, from, .. } = &current_state {
1771
+                            const RECEIVED_CONTROL_IPC_COOLDOWN_MS: u128 = 300;
1755
                             let time_in_state = entered_at.elapsed().as_millis();
1772
                             let time_in_state = entered_at.elapsed().as_millis();
1756
-                            if time_in_state < RECEIVED_CONTROL_IPC_COOLDOWN_MS {
1773
+                            // Only block if this is the same direction we received from (would return to source)
1757
-                                tracing::info!("IPC Move {:?}: in ReceivedControl cooldown ({}ms), ignoring", direction, time_in_state);
1774
+                            if *from == direction && time_in_state < RECEIVED_CONTROL_IPC_COOLDOWN_MS {
1775
+                                tracing::info!("IPC Move {:?}: in ReceivedControl cooldown ({}ms), ignoring (same as entry direction)", direction, time_in_state);
1758
                                 response_tx.send(IpcResponse::Ok { message: "in cooldown".to_string() }).ok();
1776
                                 response_tx.send(IpcResponse::Ok { message: "in cooldown".to_string() }).ok();
1759
                                 continue;
1777
                                 continue;
1760
                             }
1778
                             }
@@ -1909,7 +1927,7 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
1909
                                         IpcResponse::Error { message: format!("Return failed: {}", e) }
1927
                                         IpcResponse::Error { message: format!("Return failed: {}", e) }
1910
                                     } else {
1928
                                     } else {
1911
                                         // Set cooldown to prevent immediate re-transfer (bounce-back)
1929
                                         // Set cooldown to prevent immediate re-transfer (bounce-back)
1912
-                                        last_control_return = Some(std::time::Instant::now());
1930
+                                        last_control_return = Some((std::time::Instant::now(), direction));
1913
                                         IpcResponse::Transferred { to_machine: neighbor_name.unwrap() }
1931
                                         IpcResponse::Transferred { to_machine: neighbor_name.unwrap() }
1914
                                     }
1932
                                     }
1915
                                 } else {
1933
                                 } else {
@@ -1938,25 +1956,20 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
1938
                                     }
1956
                                     }
1939
                                 }
1957
                                 }
1940
                             } else {
1958
                             } else {
1941
-                                // Not in ReceivedControl - check cooldown first
1959
+                                // Not in ReceivedControl - check cooldown first (only for matching direction)
1942
-                                let in_cooldown = if let Some(last_return) = last_control_return {
1960
+                                let in_cooldown = if let Some((last_return, cooldown_dir)) = last_control_return {
1943
-                                    last_return.elapsed().as_millis() < CONTROL_RETURN_COOLDOWN_MS as u128
1961
+                                    cooldown_dir == direction && last_return.elapsed().as_millis() < CONTROL_RETURN_COOLDOWN_MS as u128
1944
                                 } else {
1962
                                 } else {
1945
                                     false
1963
                                     false
1946
                                 };
1964
                                 };
1947
 
1965
 
1948
                                 if in_cooldown {
1966
                                 if in_cooldown {
1949
-                                    tracing::info!("IPC Move {:?}: in cooldown, doing local movefocus", direction);
1967
+                                    // At edge with peer but in cooldown - ignore entirely
1950
-                                    let hypr_dir = match direction {
1968
+                                    // Don't do movefocus because Hyprland may wrap at edge causing bounce-back
1951
-                                        Direction::Left => "l",
1969
+                                    tracing::info!("IPC Move {:?}: at edge, in cooldown, ignoring (prevents wrap bounce-back)", direction);
1952
-                                        Direction::Right => "r",
1970
+                                    // Clear cooldown after blocking once, so subsequent legitimate presses work
1953
-                                        Direction::Up => "u",
1971
+                                    last_control_return = None;
1954
-                                        Direction::Down => "d",
1972
+                                    IpcResponse::Ok { message: "in cooldown at edge".to_string() }
1955
-                                    };
1956
-                                    match hypr_client.dispatch("movefocus", hypr_dir).await {
1957
-                                        Ok(_) => IpcResponse::Ok { message: "movefocus (cooldown)".to_string() },
1958
-                                        Err(e) => IpcResponse::Error { message: format!("movefocus failed: {}", e) },
1959
-                                    }
1960
                                 } else if barrier_enabled.load(std::sync::atomic::Ordering::SeqCst) {
1973
                                 } else if barrier_enabled.load(std::sync::atomic::Ordering::SeqCst) {
1961
                                     IpcResponse::Error { message: "Barrier enabled".to_string() }
1974
                                     IpcResponse::Error { message: "Barrier enabled".to_string() }
1962
                                 } else {
1975
                                 } else {
@@ -1981,7 +1994,22 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
1981
                                 }
1994
                                 }
1982
                             }
1995
                             }
1983
                         } else {
1996
                         } else {
1984
-                            // Either not at edge, or at edge but no peer - do local movefocus
1997
+                            // Either not at edge, or at edge but no peer
1998
+                            // Check cooldown to prevent spurious navigation after peer return
1999
+                            // (e.g., user pressed Down on cachyos to return, libinput still sees Down held)
2000
+                            let in_cooldown = if let Some((last_return, cooldown_dir)) = last_control_return {
2001
+                                cooldown_dir == direction && last_return.elapsed().as_millis() < CONTROL_RETURN_COOLDOWN_MS as u128
2002
+                            } else {
2003
+                                false
2004
+                            };
2005
+
2006
+                            if in_cooldown {
2007
+                                tracing::info!("IPC Move {:?}: in cooldown (not at edge), ignoring spurious keypress", direction);
2008
+                                // Clear cooldown after blocking once, so subsequent legitimate presses work
2009
+                                last_control_return = None;
2010
+                                IpcResponse::Ok { message: "in cooldown".to_string() }
2011
+                            } else {
2012
+                                // Do local movefocus
1985
                                 let hypr_dir = match direction {
2013
                                 let hypr_dir = match direction {
1986
                                     Direction::Left => "l",
2014
                                     Direction::Left => "l",
1987
                                     Direction::Right => "r",
2015
                                     Direction::Right => "r",
@@ -1995,6 +2023,7 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
1995
                                 }
2023
                                 }
1996
                                 IpcResponse::DoLocalMove
2024
                                 IpcResponse::DoLocalMove
1997
                             }
2025
                             }
2026
+                        }
1998
                     } // end of else block for Initiating check
2027
                     } // end of else block for Initiating check
1999
                     }
2028
                     }
2000
                     IpcRequest::Status => {
2029
                     IpcRequest::Status => {