@@ -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, .. } = ¤t_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, .. } = ¤t_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 => { |