From 263eab31e373e8306af9c31091a067ead19d5872 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sat, 13 Jun 2026 13:31:15 +0000 Subject: [PATCH] fix(m3): release held mouse buttons/keys when a session ends (stuck-click after reconnect) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pointer/keyboard injector is host-lifetime (one EIS connection for every punktfunk/1 session), so its existing release_all only fires on EIS disconnect — never when a *client* session ends. A button still down at an abrupt client disconnect therefore stayed latched in the compositor: Mutter keeps the destroyed press's implicit pointer grab, so after reconnect a stuck left-button-down turns every motion into a drag (windows move, text selects) while a fresh click's press is swallowed — clicking buttons and text inputs does nothing. Only the one held button is affected; keyboard and the other buttons are fine, exactly as reported. Fix: input_thread now tracks the buttons/keys the client holds and, when the session ends, synthesizes the matching up-events through the host-lifetime injector (whose EIS connection — and the dangling grab — outlive the session). Backend-agnostic (normal inject path), so it covers libei/EIS, wlr and uinput alike. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/punktfunk-host/src/m3.rs | 52 +++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/crates/punktfunk-host/src/m3.rs b/crates/punktfunk-host/src/m3.rs index 7f08008..1d7b68b 100644 --- a/crates/punktfunk-host/src/m3.rs +++ b/crates/punktfunk-host/src/m3.rs @@ -1167,6 +1167,14 @@ fn input_thread( let mut rumble_state = [(0u16, 0u16); MAX_WIRE_PADS]; let mut rumble_seen = [false; MAX_WIRE_PADS]; let mut last_refresh = std::time::Instant::now(); + // Pointer buttons / keys the client currently holds down. The injector is host-lifetime, so a + // press left dangling by an abrupt client disconnect stays latched in the compositor across the + // reconnect (Mutter keeps the implicit pointer grab of the still-pressed button — a stuck + // left-button-down then turns every later click into a drag: windows move, but clicking buttons + // and text inputs does nothing). We synthesize the matching up-events when this session ends — + // see the release loop after the `break`. + let mut held_buttons: Vec = Vec::new(); + let mut held_keys: Vec = Vec::new(); loop { match rx.recv_timeout(std::time::Duration::from_millis(4)) { Ok(ev) => match ev.kind { @@ -1182,6 +1190,18 @@ fn input_thread( } } _ => { + // Track press/release so a mid-press disconnect can be undone below. + match ev.kind { + InputKind::MouseButtonDown if !held_buttons.contains(&ev.code) => { + held_buttons.push(ev.code) + } + InputKind::MouseButtonUp => held_buttons.retain(|&c| c != ev.code), + InputKind::KeyDown if !held_keys.contains(&ev.code) => { + held_keys.push(ev.code) + } + InputKind::KeyUp => held_keys.retain(|&c| c != ev.code), + _ => {} + } // Pointer/keyboard → the host-lifetime injector service (one persistent // portal session for every punktfunk/1 session). A send error only means the // service thread is gone (host shutting down) — dropping the event is fine, @@ -1222,6 +1242,38 @@ fn input_thread( } } } + // Session ended (client gone). Release anything still held through the host-lifetime injector — + // its EIS connection (and any implicit grab Mutter holds for our pressed button) outlives this + // session, so without this a button pressed at disconnect stays latched and breaks clicks for + // the next session. Mirror of the injector's own release_all, but keyed off the session, which + // is where a client actually vanishes mid-press. + if !held_buttons.is_empty() || !held_keys.is_empty() { + tracing::debug!( + buttons = held_buttons.len(), + keys = held_keys.len(), + "input: releasing held buttons/keys at session end" + ); + } + for code in held_buttons { + let _ = inj_tx.send(InputEvent { + kind: InputKind::MouseButtonUp, + _pad: [0; 3], + code, + x: 0, + y: 0, + flags: 0, + }); + } + for code in held_keys { + let _ = inj_tx.send(InputEvent { + kind: InputKind::KeyUp, + _pad: [0; 3], + code, + x: 0, + y: 0, + flags: 0, + }); + } } /// The audio thread: desktop capture → Opus (48 kHz stereo, 5 ms, CBR — same tuning as the