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<()> {
332332
     const EDGE_DWELL_MS: u64 = 50; // How long cursor must be at edge to trigger
333333
 
334334
     // Cooldown after control returns to prevent immediate bounce-back
335
-    let mut last_control_return: Option<std::time::Instant> = None;
336
-    const CONTROL_RETURN_COOLDOWN_MS: u64 = 1000; // 1000ms cooldown after control returns (prevents bounce-back)
335
+    // Stores (timestamp, return_direction) - the direction is the arrow key the user pressed to return
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
337338
 
338339
     // Connection storage: direction -> peer connection
339340
     let peers: Arc<RwLock<HashMap<Direction, network::FramedConnection>>> =
@@ -991,7 +992,7 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
991992
                                         // Set cooldown to prevent the IPC Move callback from initiating transfer
992993
                                         // (Hyprland fires IPC Move after movefocus, which would see at_edge=true
993994
                                         // 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));
995996
                                     }
996997
                                     Err(e) => tracing::error!("  RECOVERY movefocus failed: {}", e),
997998
                                 }
@@ -1053,15 +1054,15 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
10531054
                                     tracing::warn!("Failed to return control: {}", e);
10541055
                                 } else {
10551056
                                     // 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));
10571058
                                 }
10581059
                                 continue;
10591060
                             }
10601061
                         }
10611062
 
1062
-                        // Check cooldown to prevent bounce-back loops
1063
-                        if let Some(last_return) = last_control_return {
1064
-                            if last_return.elapsed().as_millis() < CONTROL_RETURN_COOLDOWN_MS as u128 {
1063
+                        // Check cooldown to prevent bounce-back loops (only for matching direction)
1064
+                        if let Some((last_return, cooldown_dir)) = last_control_return {
1065
+                            if cooldown_dir == direction && last_return.elapsed().as_millis() < CONTROL_RETURN_COOLDOWN_MS as u128 {
10651066
                                 tracing::debug!("EDGE: {:?} - in cooldown, ignoring", direction);
10661067
                                 continue;
10671068
                             }
@@ -1212,7 +1213,7 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
12121213
                                                                 tracing::warn!("Failed to return control: {}", e);
12131214
                                                             } else {
12141215
                                                                 // 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));
12161217
                                                             }
12171218
                                                             edge_dwell_start = None;
12181219
                                                             continue;
@@ -1229,9 +1230,9 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
12291230
                                                         );
12301231
                                                     }
12311232
 
1232
-                                                    // Check cooldown to prevent bounce-back
1233
-                                                    if let Some(last_return) = last_control_return {
1234
-                                                        if last_return.elapsed().as_millis() < CONTROL_RETURN_COOLDOWN_MS as u128 {
1233
+                                                    // Check cooldown to prevent bounce-back (only for matching direction)
1234
+                                                    if let Some((last_return, cooldown_dir)) = last_control_return {
1235
+                                                        if cooldown_dir == edge_dir && last_return.elapsed().as_millis() < CONTROL_RETURN_COOLDOWN_MS as u128 {
12351236
                                                             tracing::debug!("CURSOR EDGE: {:?} - in cooldown", edge_dir);
12361237
                                                             edge_dwell_start = None;
12371238
                                                             continue;
@@ -1370,8 +1371,10 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
13701371
                                         }
13711372
                                         // Set cooldown to prevent bounce-back loop
13721373
                                         // When we receive Leave, control is returning to us
1373
-                                        last_control_return = Some(std::time::Instant::now());
1374
-                                        tracing::debug!("Set control return cooldown");
1374
+                                        // The return direction is opposite of peer direction (user pressed Down to return from Up peer)
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);
13751378
                                     }
13761379
                                     Message::LeaveAck => {
13771380
                                         info!("Received LeaveAck");
@@ -1660,6 +1663,19 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
16601663
                                 emu.keyboard.reset_all_keys();
16611664
                             }
16621665
                         }
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
+                        }
16631679
                     }
16641680
                     transfer::TransferEvent::StartInjection { from } => {
16651681
                         info!("Starting input injection from {:?}", from);
@@ -1747,14 +1763,16 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
17471763
                         let current_state = transfer_manager.state().await;
17481764
                         info!("IPC Move {:?}: state={:?}", direction, current_state);
17491765
 
1750
-                        // Early exit: if in ReceivedControl and within cooldown, ignore
1766
+                        // Early exit: if in ReceivedControl and within cooldown for the ENTRY direction, ignore
17511767
                         // (prevents the Super+Arrow keypress that triggered the transfer from
17521768
                         // causing a double navigation on the receiving machine)
1753
-                        if let transfer::TransferState::ReceivedControl { entered_at, .. } = &current_state {
1754
-                            const RECEIVED_CONTROL_IPC_COOLDOWN_MS: u128 = 1000;
1769
+                        // Only block the direction that would return to source, not all directions
1770
+                        if let transfer::TransferState::ReceivedControl { entered_at, from, .. } = &current_state {
1771
+                            const RECEIVED_CONTROL_IPC_COOLDOWN_MS: u128 = 300;
17551772
                             let time_in_state = entered_at.elapsed().as_millis();
1756
-                            if time_in_state < RECEIVED_CONTROL_IPC_COOLDOWN_MS {
1757
-                                tracing::info!("IPC Move {:?}: in ReceivedControl cooldown ({}ms), ignoring", direction, time_in_state);
1773
+                            // Only block if this is the same direction we received from (would return to source)
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);
17581776
                                 response_tx.send(IpcResponse::Ok { message: "in cooldown".to_string() }).ok();
17591777
                                 continue;
17601778
                             }
@@ -1909,7 +1927,7 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
19091927
                                         IpcResponse::Error { message: format!("Return failed: {}", e) }
19101928
                                     } else {
19111929
                                         // 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));
19131931
                                         IpcResponse::Transferred { to_machine: neighbor_name.unwrap() }
19141932
                                     }
19151933
                                 } else {
@@ -1938,25 +1956,20 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
19381956
                                     }
19391957
                                 }
19401958
                             } else {
1941
-                                // Not in ReceivedControl - check cooldown first
1942
-                                let in_cooldown = if let Some(last_return) = last_control_return {
1943
-                                    last_return.elapsed().as_millis() < CONTROL_RETURN_COOLDOWN_MS as u128
1959
+                                // Not in ReceivedControl - check cooldown first (only for matching direction)
1960
+                                let in_cooldown = if let Some((last_return, cooldown_dir)) = last_control_return {
1961
+                                    cooldown_dir == direction && last_return.elapsed().as_millis() < CONTROL_RETURN_COOLDOWN_MS as u128
19441962
                                 } else {
19451963
                                     false
19461964
                                 };
19471965
 
19481966
                                 if in_cooldown {
1949
-                                    tracing::info!("IPC Move {:?}: in cooldown, doing local movefocus", direction);
1950
-                                    let hypr_dir = match direction {
1951
-                                        Direction::Left => "l",
1952
-                                        Direction::Right => "r",
1953
-                                        Direction::Up => "u",
1954
-                                        Direction::Down => "d",
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
-                                    }
1967
+                                    // At edge with peer but in cooldown - ignore entirely
1968
+                                    // Don't do movefocus because Hyprland may wrap at edge causing bounce-back
1969
+                                    tracing::info!("IPC Move {:?}: at edge, in cooldown, ignoring (prevents wrap bounce-back)", direction);
1970
+                                    // Clear cooldown after blocking once, so subsequent legitimate presses work
1971
+                                    last_control_return = None;
1972
+                                    IpcResponse::Ok { message: "in cooldown at edge".to_string() }
19601973
                                 } else if barrier_enabled.load(std::sync::atomic::Ordering::SeqCst) {
19611974
                                     IpcResponse::Error { message: "Barrier enabled".to_string() }
19621975
                                 } else {
@@ -1981,19 +1994,35 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
19811994
                                 }
19821995
                             }
19831996
                         } else {
1984
-                            // Either not at edge, or at edge but no peer - do local movefocus
1985
-                            let hypr_dir = match direction {
1986
-                                Direction::Left => "l",
1987
-                                Direction::Right => "r",
1988
-                                Direction::Up => "u",
1989
-                                Direction::Down => "d",
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
19902004
                             };
1991
-                            info!("IPC Move {:?}: doing local movefocus {}", direction, hypr_dir);
1992
-                            match hypr_client.dispatch("movefocus", hypr_dir).await {
1993
-                                Ok(()) => info!("  movefocus succeeded"),
1994
-                                Err(e) => tracing::error!("  movefocus failed: {}", e),
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
2013
+                                let hypr_dir = match direction {
2014
+                                    Direction::Left => "l",
2015
+                                    Direction::Right => "r",
2016
+                                    Direction::Up => "u",
2017
+                                    Direction::Down => "d",
2018
+                                };
2019
+                                info!("IPC Move {:?}: doing local movefocus {}", direction, hypr_dir);
2020
+                                match hypr_client.dispatch("movefocus", hypr_dir).await {
2021
+                                    Ok(()) => info!("  movefocus succeeded"),
2022
+                                    Err(e) => tracing::error!("  movefocus failed: {}", e),
2023
+                                }
2024
+                                IpcResponse::DoLocalMove
19952025
                             }
1996
-                            IpcResponse::DoLocalMove
19972026
                         }
19982027
                     } // end of else block for Initiating check
19992028
                     }