diff --git a/README.md b/README.md index ac8bd98..8d4c9f3 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,7 @@ Released binaries are not codesigned with a Developer ID, so after every upgrade |-----|--------| | Space | Click | | Enter | Double click | +| `m` | Triple click | | Delete | Right click | | Escape | Close overlay | | Backspace | Undo last step | @@ -114,7 +115,7 @@ An alternative grid mode that recursively subdivides. Instead of two-key combos - Enter bisect mode: press `b` from normal mode, or launch with `--bisect` - Each hint key splits the current region into a smaller grid (2x2 by default) -- Space / Enter / Delete act (click / double-click / right-click) at the center of the current region and exit +- Space / Enter / `m` / Delete act (click / double-click / triple-click / right-click) at the center of the current region and exit - Backspace pops back up one level - Subdivision stops automatically once a cell would fall below `min_cell_size` pixels — the region is highlighted and you can act or back out - Press `n` to switch back to normal mode @@ -126,7 +127,7 @@ An alternative mode for direct keyboard-driven cursor movement. Instead of selec - Enter free mode: press `v` from normal or bisect mode, or launch with `--free` (starts at mouse position) or `--free-center` (starts at screen center) - Move the cursor with `h` / `j` / `k` / `l` (left / down / up / right) - Press `=` to increase speed and `-` to decrease it; the current speed is shown in the top-left corner -- Space / Enter / Delete act (click / double-click / right-click) and exit +- Space / Enter / `m` / Delete act (click / double-click / triple-click / right-click) and exit - Backspace returns to the previous mode; Escape closes the overlay - Key repeat is supported: hold a movement key to move continuously @@ -138,7 +139,7 @@ An alternative mode for direct keyboard-driven cursor movement. Instead of selec Record multi-step mouse sequences for replay. -**Record:** Press `` ` `` to start recording. Navigate and act normally (Space to click, Enter to double-click, Tab to hover-only, `/` to drag). Press `` ` `` again to stop. You'll be prompted for an optional bind key and a name. +**Record:** Press `` ` `` to start recording. Navigate and act normally (Space to click, Enter to double-click, `m` to triple-click, Tab to hover-only, `/` to drag). Press `` ` `` again to stop. You'll be prompted for an optional bind key and a name. **Replay:** Press `@` then the bind key. Or press Tab to search by name, then Enter to select. @@ -178,6 +179,7 @@ bisect = "b" free_mode = "v" click = "space" double_click = "enter" +triple_click = "m" close = "escape" undo = "backspace" right_click = "delete" diff --git a/src/backend/macos.rs b/src/backend/macos.rs index 368b379..9dfd47b 100644 --- a/src/backend/macos.rs +++ b/src/backend/macos.rs @@ -656,6 +656,22 @@ impl Backend for MacosBackend { Ok(()) } + fn triple_click(&mut self, x: u32, y: u32) -> Result<()> { + self.hide_overlay(); + self.focus_target_app(); + for count in 1..=3 { + self.click_at( + K_CGEVENT_LEFT_MOUSE_DOWN, + K_CGEVENT_LEFT_MOUSE_UP, + K_CGMOUSE_BUTTON_LEFT, + x, + y, + count, + ); + } + Ok(()) + } + fn right_click(&mut self, x: u32, y: u32) -> Result<()> { self.hide_overlay(); self.focus_target_app(); diff --git a/src/backend/mod.rs b/src/backend/mod.rs index fff0a6e..f715903 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -9,6 +9,7 @@ pub enum KeyEvent { Char(char), Click, DoubleClick, + TripleClick, RightClick, Close, Undo, @@ -43,6 +44,9 @@ pub trait Backend { /// Tear down the overlay, double click at (x, y), then return. fn double_click(&mut self, x: u32, y: u32) -> Result<()>; + /// Tear down the overlay, triple click at (x, y), then return. + fn triple_click(&mut self, x: u32, y: u32) -> Result<()>; + /// Tear down the overlay, right click at (x, y), then return. fn right_click(&mut self, x: u32, y: u32) -> Result<()>; diff --git a/src/backend/wayland.rs b/src/backend/wayland.rs index 3f73018..e9077b4 100644 --- a/src/backend/wayland.rs +++ b/src/backend/wayland.rs @@ -239,6 +239,32 @@ impl Backend for WaylandBackend { Ok(()) } + fn triple_click(&mut self, x: u32, y: u32) -> Result<()> { + self.teardown_surface()?; + + if let Some(vp) = &self.state.vp { + vp.motion_absolute(timestamp(), x, y, self.state.screen_w, self.state.screen_h); + vp.frame(); + } + self.eq + .roundtrip(&mut self.state) + .context("roundtrip after motion")?; + + if let Some(vp) = &self.state.vp { + for _ in 0..3 { + let ts = timestamp(); + vp.button(ts, BTN_LEFT, wl_pointer::ButtonState::Pressed); + vp.frame(); + vp.button(ts, BTN_LEFT, wl_pointer::ButtonState::Released); + vp.frame(); + } + } + self.eq + .roundtrip(&mut self.state) + .context("roundtrip after click")?; + Ok(()) + } + /// Destroy the overlay so the compositor removes it from the surface stack, /// then re-send motion to trigger a focus update, then right click. fn right_click(&mut self, x: u32, y: u32) -> Result<()> { diff --git a/src/backend/x11.rs b/src/backend/x11.rs index ab0977b..7941d19 100644 --- a/src/backend/x11.rs +++ b/src/backend/x11.rs @@ -240,6 +240,14 @@ impl Backend for X11Backend { self.fake_button_click(BTN_LEFT) } + fn triple_click(&mut self, x: u32, y: u32) -> Result<()> { + self.teardown()?; + self.warp_and_sync(x, y)?; + self.fake_button_click(BTN_LEFT)?; + self.fake_button_click(BTN_LEFT)?; + self.fake_button_click(BTN_LEFT) + } + fn right_click(&mut self, x: u32, y: u32) -> Result<()> { self.teardown()?; self.warp_and_sync(x, y)?; diff --git a/src/config.rs b/src/config.rs index bed6dbb..2fe229e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -213,6 +213,7 @@ pub struct KeyBindings { pub free_mode: Key, pub click: Key, pub double_click: Key, + pub triple_click: Key, pub close: Key, pub undo: Key, pub macro_menu: Key, @@ -232,6 +233,7 @@ impl Default for KeyBindings { free_mode: Key::Char('v'), click: Key::Space, double_click: Key::Enter, + triple_click: Key::Char('m'), close: Key::Escape, undo: Key::Backspace, macro_menu: Key::Tab, @@ -264,6 +266,9 @@ impl KeyBindings { if key == self.double_click { return Some(KeyEvent::DoubleClick); } + if key == self.triple_click { + return Some(KeyEvent::TripleClick); + } if key == self.close { return Some(KeyEvent::Close); } diff --git a/src/macro_store.rs b/src/macro_store.rs index 9f49439..c83d3b3 100644 --- a/src/macro_store.rs +++ b/src/macro_store.rs @@ -7,6 +7,7 @@ pub enum MacroActionKind { Move(String), Click(String), DoubleClick(String), + TripleClick(String), RightClick(String), Drag(String, String), } diff --git a/src/mode/bisect.rs b/src/mode/bisect.rs index 2d0e6d7..f9ee39b 100644 --- a/src/mode/bisect.rs +++ b/src/mode/bisect.rs @@ -30,6 +30,10 @@ pub(super) fn handle_key( backend.double_click(cx, cy)?; Ok(ModeTransition::Exit) } + KeyEvent::TripleClick => { + backend.triple_click(cx, cy)?; + Ok(ModeTransition::Exit) + } KeyEvent::RightClick => { backend.right_click(cx, cy)?; Ok(ModeTransition::Exit) diff --git a/src/mode/free.rs b/src/mode/free.rs index 0d88301..b4c6c0c 100644 --- a/src/mode/free.rs +++ b/src/mode/free.rs @@ -25,6 +25,10 @@ pub(super) fn handle_key( backend.double_click(x, y)?; Ok(ModeTransition::Exit) } + KeyEvent::TripleClick => { + backend.triple_click(x, y)?; + Ok(ModeTransition::Exit) + } KeyEvent::RightClick => { backend.right_click(x, y)?; Ok(ModeTransition::Exit) diff --git a/src/mode/mod.rs b/src/mode/mod.rs index cecba3f..8d78ff5 100644 --- a/src/mode/mod.rs +++ b/src/mode/mod.rs @@ -266,6 +266,11 @@ pub(super) fn replay_macro( backend.double_click(x, y)?; } } + MacroActionKind::TripleClick(keys) => { + if let Some((x, y)) = keys_to_pos(keys, w, h) { + backend.triple_click(x, y)?; + } + } MacroActionKind::RightClick(keys) => { if let Some((x, y)) = keys_to_pos(keys, w, h) { backend.right_click(x, y)?; diff --git a/src/mode/normal.rs b/src/mode/normal.rs index 895d3d7..c008085 100644 --- a/src/mode/normal.rs +++ b/src/mode/normal.rs @@ -41,6 +41,12 @@ pub(super) fn handle_key( } Ok(ModeTransition::Exit) } + KeyEvent::TripleClick if drag_origin.is_none() => { + if let Some((x, y)) = target { + backend.triple_click(x, y)?; + } + Ok(ModeTransition::Exit) + } KeyEvent::RightClick if drag_origin.is_none() => { if let Some((x, y)) = target { backend.right_click(x, y)?; diff --git a/src/mode/recording.rs b/src/mode/recording.rs index af121b6..e88058c 100644 --- a/src/mode/recording.rs +++ b/src/mode/recording.rs @@ -131,7 +131,7 @@ pub(super) fn handle_key( InputState::Ready { .. } => Ok(ModeTransition::Stay), } } - KeyEvent::Click | KeyEvent::DoubleClick | KeyEvent::RightClick + KeyEvent::Click | KeyEvent::DoubleClick | KeyEvent::TripleClick | KeyEvent::RightClick if target.is_some() && drag_origin.is_none() => { let (x, y) = target.unwrap(); @@ -154,6 +154,13 @@ pub(super) fn handle_key( wait_ms, )); } + KeyEvent::TripleClick => { + backend.triple_click(x, y)?; + new_actions.push(MacroAction::new( + MacroActionKind::TripleClick(current_keys), + wait_ms, + )); + } KeyEvent::RightClick => { backend.right_click(x, y)?; new_actions.push(MacroAction::new(