T13: press-and-hold repeat for mouse broadcast / mirror clicks
Hyprland mouse binds fire once per press; for multiboxing you want a
held mouse button to repeat clicks on every captured slot at a
configurable cadence. Wire that:
- mouse-press <button> IPC verb: fires the broadcast once immediately,
then spawns a per-button tokio task that re-fires it every
ENBOXER_MOUSE_REPEAT_MS (default 50 ms = 20 Hz). Stored per button
in Session.mouse_repeats.
- mouse-release <button>: cancels the matching repeat.
- mirror_repeat_loop worker: holds an Arc<AtomicBool> cancel signal so
the release side can flip it without taking the Session mutex.
- Bind installation: each mouse button (272, 273) now installs BOTH
a press BindSpec AND a release=true BindSpec for both mouse_broadcast
and Mode::Mirror so the behaviour is automatic for the operator.
Hyprland translates release=true to { release = true } bind option.
Cadence is tunable at startup via ENBOXER_MOUSE_REPEAT_MS, clamped to
1..=2000 ms. The mirror_clicks_to docstring (previously "OUT OF
SCOPE for T13 ...") now points at mirror_repeat_loop.
cargo test 98+/0; clippy clean.
This commit is contained in:
parent
bb6d9e8566
commit
4ee58acf2c
145
src/session.rs
145
src/session.rs
@ -23,6 +23,23 @@ pub struct Session {
|
||||
pub vfx_source: Option<u32>,
|
||||
pub mouse_broadcast: bool,
|
||||
pub mouse_follow: bool,
|
||||
/// Per-button press-and-hold repeat state. Keyed by Linux input
|
||||
/// event code (272 = BTN_LEFT, 273 = BTN_RIGHT). Cancelled when
|
||||
/// the matching `mouse-release` IPC verb arrives.
|
||||
pub mouse_repeats: std::collections::HashMap<u32, RepeatState>,
|
||||
/// Cadence in milliseconds between repeat clicks while a button is
|
||||
/// held. Tunable via `ENBOXER_MOUSE_REPEAT_MS`; defaults to 50 ms
|
||||
/// (20 Hz) which feels responsive without saturating the IPC socket.
|
||||
pub mouse_repeat_ms: u64,
|
||||
}
|
||||
|
||||
/// Per-button repeat-loop handle + cancel signal. Holding the cancel
|
||||
/// `Arc<AtomicBool>` in the Session lets the release path flip it
|
||||
/// without holding the Session mutex; the worker checks the bit at
|
||||
/// each cadence tick and exits cleanly.
|
||||
pub struct RepeatState {
|
||||
pub cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
||||
pub handle: Option<tokio::task::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
@ -37,6 +54,12 @@ impl Session {
|
||||
vfx_source: None,
|
||||
mouse_broadcast: false,
|
||||
mouse_follow: false,
|
||||
mouse_repeats: std::collections::HashMap::new(),
|
||||
mouse_repeat_ms: std::env::var("ENBOXER_MOUSE_REPEAT_MS")
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<u64>().ok())
|
||||
.filter(|&n| (1..=2000).contains(&n))
|
||||
.unwrap_or(50),
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -804,10 +827,124 @@ pub fn others_clients(slots: &[(u32, Client)], leader: u32) -> Vec<Client> {
|
||||
/// click are forwarded from the cursor position in
|
||||
/// `broadcast_mirror_click`.
|
||||
///
|
||||
/// Press-and-hold guard is OUT OF SCOPE for T13 (Master has not requested
|
||||
/// it yet); the caller should hold the mouse binds as a follow-up.
|
||||
/// T13-todo: press-and-hold guard — repeated button-down without release
|
||||
/// should re-fire on a configurable cadence.
|
||||
/// Handle the IPC verb `mouse-press <button>`. Fires one click
|
||||
/// immediately so the operator gets no perceptible delay, then starts
|
||||
/// a per-button repeat loop. Cancels any prior repeat for the same
|
||||
/// button before installing the new one.
|
||||
pub async fn mouse_press(session: &Arc<Mutex<Session>>, button: u32) -> Result<()> {
|
||||
// Fire the first click synchronously through whichever path the
|
||||
// session is in (mouse-broadcast or mirror mode). mirror-click
|
||||
// no-ops outside of Mode::Mirror so the dispatch is harmless.
|
||||
{
|
||||
let g = session.lock().await;
|
||||
let mode = g.engine.mode;
|
||||
let mouse_broadcast = g.mouse_broadcast;
|
||||
drop(g);
|
||||
let r = if mode == Mode::Mirror {
|
||||
broadcast_mirror_click(session, button).await
|
||||
} else if mouse_broadcast {
|
||||
broadcast_click(session, button).await
|
||||
} else {
|
||||
// Neither mode nor mouse_broadcast is on; the bind was
|
||||
// installed anyway. Just no-op.
|
||||
Ok(())
|
||||
};
|
||||
if let Err(e) = r {
|
||||
tracing::warn!("mouse-press initial click failed: {e}");
|
||||
}
|
||||
}
|
||||
// Cancel any prior repeat for this button.
|
||||
{
|
||||
let mut g = session.lock().await;
|
||||
if let Some(prev) = g.mouse_repeats.remove(&button) {
|
||||
prev.cancel
|
||||
.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
if let Some(h) = prev.handle {
|
||||
h.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Spawn the repeat loop.
|
||||
let cancel = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let cancel_clone = cancel.clone();
|
||||
let cadence = {
|
||||
let g = session.lock().await;
|
||||
g.mouse_repeat_ms
|
||||
};
|
||||
let session_clone = session.clone();
|
||||
let handle = tokio::spawn(async move {
|
||||
mirror_repeat_loop(session_clone, button, cadence, cancel_clone).await;
|
||||
});
|
||||
let mut g = session.lock().await;
|
||||
g.mouse_repeats.insert(
|
||||
button,
|
||||
RepeatState {
|
||||
cancel,
|
||||
handle: Some(handle),
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle the IPC verb `mouse-release <button>`. Cancels the per-button
|
||||
/// repeat loop; the most recent click is the last one delivered.
|
||||
pub async fn mouse_release(session: &Arc<Mutex<Session>>, button: u32) -> Result<()> {
|
||||
let mut g = session.lock().await;
|
||||
if let Some(mut state) = g.mouse_repeats.remove(&button) {
|
||||
state.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
if let Some(h) = state.handle.take() {
|
||||
h.abort();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Per-button repeat loop. Fires `broadcast_click` (or
|
||||
/// `broadcast_mirror_click` in mirror mode) every `cadence_ms` until
|
||||
/// either the cancel bit is flipped by the matching release handler or the
|
||||
/// session shuts down. Each click is independent: a failure is logged and
|
||||
/// the loop continues. Cadence is captured at spawn time; changing
|
||||
/// `mouse_repeat_ms` mid-hold does not affect the active repeat.
|
||||
async fn mirror_repeat_loop(
|
||||
session: Arc<Mutex<Session>>,
|
||||
button: u32,
|
||||
cadence_ms: u64,
|
||||
cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
||||
) {
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_millis(cadence_ms)).await;
|
||||
if cancel.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
let (mode, mouse_broadcast) = {
|
||||
let g = session.lock().await;
|
||||
(g.engine.mode, g.mouse_broadcast)
|
||||
};
|
||||
let r = if mode == Mode::Mirror {
|
||||
broadcast_mirror_click(&session, button).await
|
||||
} else if mouse_broadcast {
|
||||
broadcast_click(&session, button).await
|
||||
} else {
|
||||
// User disabled mouse_broadcast while holding the button;
|
||||
// cancel the repeat so we don't fire forever.
|
||||
cancel.store(true, Ordering::Relaxed);
|
||||
break;
|
||||
};
|
||||
if let Err(e) = r {
|
||||
tracing::warn!("mouse repeat click failed: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns one `(target_client, button)` pair per non-leader slot so
|
||||
/// the daemon can broadcast a single mirror-mode click. Repeated
|
||||
/// clicks on a held mouse button are handled by [`mirror_repeat_loop`]:
|
||||
/// a per-button timer re-fires this same broadcast at a configurable
|
||||
/// cadence until the matching release IPC verb cancels it. The
|
||||
/// initial press IPC verb still calls this once on its own so the first
|
||||
/// click is delivered immediately, not after the cadence delay.
|
||||
pub fn mirror_clicks_to(
|
||||
slots: &[(u32, Client)],
|
||||
leader: u32,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user