diff --git a/input-capture/src/lib.rs b/input-capture/src/lib.rs index 4767503..b1ef6c0 100644 --- a/input-capture/src/lib.rs +++ b/input-capture/src/lib.rs @@ -171,6 +171,19 @@ impl InputCapture { self.capture.release().await } + /// Drain and return every key the capture has forwarded as + /// down-but-not-up. The caller is expected to synthesize key-up + /// events to the remote peer for each — otherwise the peer + /// retains phantom-held keys after capture is released. The + /// canonical case is the release-bind chord + /// (Ctrl+Shift+Alt+Meta): the down events were sent while + /// capture was active, but the matching up events arrive after + /// the local tap has flipped to passthrough and never reach + /// the peer. + pub fn take_pressed_keys(&mut self) -> HashSet { + std::mem::take(&mut self.pressed_keys) + } + /// destroy the input capture pub async fn terminate(&mut self) -> Result<(), CaptureError> { self.capture.terminate().await diff --git a/src/capture.rs b/src/capture.rs index 4300dea..8f739bd 100644 --- a/src/capture.rs +++ b/src/capture.rs @@ -8,7 +8,7 @@ use futures::StreamExt; use input_capture::{ CaptureError, CaptureEvent, CaptureHandle, InputCapture, InputCaptureError, Position, }; -use input_event::scancode; +use input_event::{Event, KeyboardEvent, scancode}; use lan_mouse_proto::ProtoEvent; use local_channel::mpsc::{Receiver, Sender, channel}; use tokio::task::{JoinHandle, spawn_local}; @@ -376,6 +376,41 @@ impl CaptureTask { async fn release_capture(&mut self, capture: &mut InputCapture) -> Result<(), CaptureError> { // If we have an active client, notify them we're leaving if let Some(handle) = self.active_client.take() { + // Synthesize key-up events for every key still held in the + // capture's pressed_keys set BEFORE sending Leave. Without + // this, pressing the release-bind chord (typically all four + // modifiers) leaves the peer with phantom held modifiers: + // the down events were forwarded while capture was active, + // but the matching up events arrive after the local tap + // flips to passthrough and never reach the peer. The peer + // then runs every subsequent keystroke through those held + // mods until its watchdog times out (1+ s) or our Leave + // arrives — and Leave can be lost over UDP/DTLS. + for key in capture.take_pressed_keys() { + let key_up = ProtoEvent::Input(Event::Keyboard(KeyboardEvent::Key { + time: 0, + key: key as u32, + state: 0, + })); + if let Err(e) = self.conn.send(key_up, handle).await { + log::warn!("failed to send key-up to client {handle}: {e}"); + } + } + // Reset the modifier mask too. The peer's input-emulation + // layer keeps a separate XKB-style modifier state that's + // updated by KeyboardEvent::Modifiers, distinct from the + // pressed_keys set drained above. Without this, an + // already-locked CapsLock would survive the release. + let mods_zero = ProtoEvent::Input(Event::Keyboard(KeyboardEvent::Modifiers { + depressed: 0, + latched: 0, + locked: 0, + group: 0, + })); + if let Err(e) = self.conn.send(mods_zero, handle).await { + log::warn!("failed to reset modifiers on client {handle}: {e}"); + } + log::info!("sending Leave event to client {handle}"); if let Err(e) = self.conn.send(ProtoEvent::Leave(0), handle).await { log::warn!("failed to send Leave to client {handle}: {e}");