From 0a03c36c7fc1282488dae4ec0f1758e3d7017b78 Mon Sep 17 00:00:00 2001 From: Trung Do Date: Mon, 25 May 2026 01:00:18 +0700 Subject: [PATCH] feat: implement middle click --- README.md | 6 ++++-- src/backend/macos.rs | 5 +++++ src/backend/mod.rs | 4 ++++ src/backend/wayland.rs | 27 +++++++++++++++++++++++++++ src/backend/x11.rs | 7 +++++++ src/config.rs | 5 +++++ src/macro_store.rs | 1 + src/mode/bisect.rs | 4 ++++ src/mode/free.rs | 4 ++++ src/mode/mod.rs | 5 +++++ src/mode/normal.rs | 6 ++++++ src/mode/recording.rs | 9 ++++++++- 12 files changed, 80 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8d4c9f3..b8dc55c 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,7 @@ Released binaries are not codesigned with a Developer ID, so after every upgrade | Enter | Double click | | `m` | Triple click | | Delete | Right click | +| Insert | Middle click (macOS is not supported) | | Escape | Close overlay | | Backspace | Undo last step | | Arrow keys | Scroll (up/down/left/right) | @@ -115,7 +116,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 / `m` / Delete act (click / double-click / triple-click / right-click) at the center of the current region and exit +- Space / Enter / `m` / Delete / Insert act (click / double-click / triple-click / right-click / middle-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 @@ -127,7 +128,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 / `m` / Delete act (click / double-click / triple-click / right-click) and exit +- Space / Enter / `m` / Delete / Insert act (click / double-click / triple-click / right-click / middle-click) and exit - Backspace returns to the previous mode; Escape closes the overlay - Key repeat is supported: hold a movement key to move continuously @@ -183,6 +184,7 @@ triple_click = "m" close = "escape" undo = "backspace" right_click = "delete" +middle_click = "insert" scroll_up = "up" scroll_down = "down" scroll_left = "left" diff --git a/src/backend/macos.rs b/src/backend/macos.rs index 9dfd47b..723c76c 100644 --- a/src/backend/macos.rs +++ b/src/backend/macos.rs @@ -686,6 +686,11 @@ impl Backend for MacosBackend { Ok(()) } + fn middle_click(&mut self, x: u32, y: u32) -> Result<()> { + unimplemented!(); + Ok(()) + } + fn drag_select(&mut self, x1: u32, y1: u32, x2: u32, y2: u32) -> Result<()> { self.hide_overlay(); self.focus_target_app(); diff --git a/src/backend/mod.rs b/src/backend/mod.rs index f715903..545ff4e 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -11,6 +11,7 @@ pub enum KeyEvent { DoubleClick, TripleClick, RightClick, + MiddleClick, Close, Undo, MacroMenu, @@ -50,6 +51,9 @@ pub trait Backend { /// Tear down the overlay, right click at (x, y), then return. fn right_click(&mut self, x: u32, y: u32) -> Result<()>; + /// Tear down the overlay, middle click at (x, y), then return. + fn middle_click(&mut self, x: u32, y: u32) -> Result<()>; + /// Tear down the overlay, drag from (x1,y1) to (x2,y2), then return. fn drag_select(&mut self, x1: u32, y1: u32, x2: u32, y2: u32) -> Result<()>; diff --git a/src/backend/wayland.rs b/src/backend/wayland.rs index e9077b4..542d937 100644 --- a/src/backend/wayland.rs +++ b/src/backend/wayland.rs @@ -25,6 +25,7 @@ use crate::config::{config, Key}; const BTN_LEFT: u32 = 0x110; const BTN_RIGHT: u32 = 0x111; +const BTN_MIDDLE: u32 = 0x112; const REPEAT_DELAY_MS: u64 = 300; const REPEAT_RATE_HZ: u64 = 25; @@ -291,6 +292,32 @@ impl Backend for WaylandBackend { Ok(()) } + /// Destroy the overlay so the compositor removes it from the surface stack, + /// then re-send motion to trigger a focus update, then middle click. + fn middle_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 { + let ts = timestamp(); + vp.button(ts, BTN_MIDDLE, wl_pointer::ButtonState::Pressed); + vp.frame(); + vp.button(ts, BTN_MIDDLE, wl_pointer::ButtonState::Released); + vp.frame(); + } + self.eq + .roundtrip(&mut self.state) + .context("roundtrip after click")?; + Ok(()) + } + fn scroll_up(&mut self) -> Result<()> { self.scroll(Axis::VerticalScroll, -15.0, -1) } diff --git a/src/backend/x11.rs b/src/backend/x11.rs index 7941d19..1aa12c2 100644 --- a/src/backend/x11.rs +++ b/src/backend/x11.rs @@ -13,6 +13,7 @@ use super::{Backend, KeyEvent}; use crate::config::{config, Key}; const BTN_LEFT: u8 = 1; +const BTN_MIDDLE: u8 = 2; const BTN_RIGHT: u8 = 3; const BTN_SCROLL_UP: u8 = 4; const BTN_SCROLL_DOWN: u8 = 5; @@ -254,6 +255,12 @@ impl Backend for X11Backend { self.fake_button_click(BTN_RIGHT) } + fn middle_click(&mut self, x: u32, y: u32) -> Result<()> { + self.teardown()?; + self.warp_and_sync(x, y)?; + self.fake_button_click(BTN_MIDDLE) + } + fn drag_select(&mut self, x1: u32, y1: u32, x2: u32, y2: u32) -> Result<()> { self.teardown()?; self.warp_and_sync(x1, y1)?; diff --git a/src/config.rs b/src/config.rs index 2fe229e..27d61e0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -219,6 +219,7 @@ pub struct KeyBindings { pub macro_menu: Key, pub macro_record: Key, pub right_click: Key, + pub middle_click: Key, pub scroll_up: Key, pub scroll_down: Key, pub scroll_left: Key, @@ -239,6 +240,7 @@ impl Default for KeyBindings { macro_menu: Key::Tab, macro_record: Key::Char('`'), right_click: Key::Delete, + middle_click: Key::Insert, scroll_up: Key::Up, scroll_down: Key::Down, scroll_left: Key::Left, @@ -284,6 +286,9 @@ impl KeyBindings { if key == self.right_click { return Some(KeyEvent::RightClick); } + if key == self.middle_click { + return Some(KeyEvent::MiddleClick); + } if key == self.scroll_up { return Some(KeyEvent::ScrollUp); } diff --git a/src/macro_store.rs b/src/macro_store.rs index c83d3b3..da06c12 100644 --- a/src/macro_store.rs +++ b/src/macro_store.rs @@ -9,6 +9,7 @@ pub enum MacroActionKind { DoubleClick(String), TripleClick(String), RightClick(String), + MiddleClick(String), Drag(String, String), } diff --git a/src/mode/bisect.rs b/src/mode/bisect.rs index f9ee39b..ce1e592 100644 --- a/src/mode/bisect.rs +++ b/src/mode/bisect.rs @@ -38,6 +38,10 @@ pub(super) fn handle_key( backend.right_click(cx, cy)?; Ok(ModeTransition::Exit) } + KeyEvent::MiddleClick => { + backend.middle_click(cx, cy)?; + Ok(ModeTransition::Exit) + } KeyEvent::ScrollUp => { backend.scroll_up()?; Ok(ModeTransition::Redraw) diff --git a/src/mode/free.rs b/src/mode/free.rs index b4c6c0c..2b0c99c 100644 --- a/src/mode/free.rs +++ b/src/mode/free.rs @@ -33,6 +33,10 @@ pub(super) fn handle_key( backend.right_click(x, y)?; Ok(ModeTransition::Exit) } + KeyEvent::MiddleClick => { + backend.middle_click(x, y)?; + Ok(ModeTransition::Exit) + } KeyEvent::ScrollUp => { backend.scroll_up()?; Ok(ModeTransition::Redraw) diff --git a/src/mode/mod.rs b/src/mode/mod.rs index 8d78ff5..f99b10a 100644 --- a/src/mode/mod.rs +++ b/src/mode/mod.rs @@ -276,6 +276,11 @@ pub(super) fn replay_macro( backend.right_click(x, y)?; } } + MacroActionKind::MiddleClick(keys) => { + if let Some((x, y)) = keys_to_pos(keys, w, h) { + backend.middle_click(x, y)?; + } + } MacroActionKind::Drag(start_keys, end_keys) => { if let (Some((x1, y1)), Some((x2, y2))) = (keys_to_pos(start_keys, w, h), keys_to_pos(end_keys, w, h)) diff --git a/src/mode/normal.rs b/src/mode/normal.rs index c008085..422560a 100644 --- a/src/mode/normal.rs +++ b/src/mode/normal.rs @@ -53,6 +53,12 @@ pub(super) fn handle_key( } Ok(ModeTransition::Exit) } + KeyEvent::MiddleClick if drag_origin.is_none() => { + if let Some((x, y)) = target { + backend.middle_click(x, y)?; + } + Ok(ModeTransition::Exit) + } KeyEvent::Char('/') if matches!( input_state, diff --git a/src/mode/recording.rs b/src/mode/recording.rs index e88058c..89e79e8 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::TripleClick | KeyEvent::RightClick + KeyEvent::Click | KeyEvent::DoubleClick | KeyEvent::TripleClick | KeyEvent::RightClick | KeyEvent::MiddleClick if target.is_some() && drag_origin.is_none() => { let (x, y) = target.unwrap(); @@ -168,6 +168,13 @@ pub(super) fn handle_key( wait_ms, )); } + KeyEvent::MiddleClick => { + backend.middle_click(x, y)?; + new_actions.push(MacroAction::new( + MacroActionKind::MiddleClick(current_keys), + wait_ms, + )); + } _ => {} } backend.reopen()?;