@@ -332,8 +332,9 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> { |
| 332 | 332 | const EDGE_DWELL_MS: u64 = 50; // How long cursor must be at edge to trigger |
| 333 | 333 | |
| 334 | 334 | // 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 |
| 337 | 338 | |
| 338 | 339 | // Connection storage: direction -> peer connection |
| 339 | 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 | 992 | // Set cooldown to prevent the IPC Move callback from initiating transfer |
| 992 | 993 | // (Hyprland fires IPC Move after movefocus, which would see at_edge=true |
| 993 | 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 | 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 | 1054 | tracing::warn!("Failed to return control: {}", e); |
| 1054 | 1055 | } else { |
| 1055 | 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 | 1059 | continue; |
| 1059 | 1060 | } |
| 1060 | 1061 | } |
| 1061 | 1062 | |
| 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 { |
| 1065 | 1066 | tracing::debug!("EDGE: {:?} - in cooldown, ignoring", direction); |
| 1066 | 1067 | continue; |
| 1067 | 1068 | } |
@@ -1212,7 +1213,7 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> { |
| 1212 | 1213 | tracing::warn!("Failed to return control: {}", e); |
| 1213 | 1214 | } else { |
| 1214 | 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 | 1218 | edge_dwell_start = None; |
| 1218 | 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 | | - 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 { |
| 1235 | 1236 | tracing::debug!("CURSOR EDGE: {:?} - in cooldown", edge_dir); |
| 1236 | 1237 | edge_dwell_start = None; |
| 1237 | 1238 | continue; |
@@ -1370,8 +1371,10 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> { |
| 1370 | 1371 | } |
| 1371 | 1372 | // Set cooldown to prevent bounce-back loop |
| 1372 | 1373 | // 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); |
| 1375 | 1378 | } |
| 1376 | 1379 | Message::LeaveAck => { |
| 1377 | 1380 | info!("Received LeaveAck"); |
@@ -1660,6 +1663,19 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> { |
| 1660 | 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 | 1680 | transfer::TransferEvent::StartInjection { from } => { |
| 1665 | 1681 | info!("Starting input injection from {:?}", from); |
@@ -1747,14 +1763,16 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> { |
| 1747 | 1763 | let current_state = transfer_manager.state().await; |
| 1748 | 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 | 1767 | // (prevents the Super+Arrow keypress that triggered the transfer from |
| 1752 | 1768 | // causing a double navigation on the receiving machine) |
| 1753 | | - if let transfer::TransferState::ReceivedControl { entered_at, .. } = ¤t_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, .. } = ¤t_state { |
| 1771 | + const RECEIVED_CONTROL_IPC_COOLDOWN_MS: u128 = 300; |
| 1755 | 1772 | 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); |
| 1758 | 1776 | response_tx.send(IpcResponse::Ok { message: "in cooldown".to_string() }).ok(); |
| 1759 | 1777 | continue; |
| 1760 | 1778 | } |
@@ -1909,7 +1927,7 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> { |
| 1909 | 1927 | IpcResponse::Error { message: format!("Return failed: {}", e) } |
| 1910 | 1928 | } else { |
| 1911 | 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 | 1931 | IpcResponse::Transferred { to_machine: neighbor_name.unwrap() } |
| 1914 | 1932 | } |
| 1915 | 1933 | } else { |
@@ -1938,25 +1956,20 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> { |
| 1938 | 1956 | } |
| 1939 | 1957 | } |
| 1940 | 1958 | } 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 |
| 1944 | 1962 | } else { |
| 1945 | 1963 | false |
| 1946 | 1964 | }; |
| 1947 | 1965 | |
| 1948 | 1966 | 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() } |
| 1960 | 1973 | } else if barrier_enabled.load(std::sync::atomic::Ordering::SeqCst) { |
| 1961 | 1974 | IpcResponse::Error { message: "Barrier enabled".to_string() } |
| 1962 | 1975 | } else { |
@@ -1981,19 +1994,35 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> { |
| 1981 | 1994 | } |
| 1982 | 1995 | } |
| 1983 | 1996 | } 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 |
| 1990 | 2004 | }; |
| 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 |
| 1995 | 2025 | } |
| 1996 | | - IpcResponse::DoLocalMove |
| 1997 | 2026 | } |
| 1998 | 2027 | } // end of else block for Initiating check |
| 1999 | 2028 | } |