Skip to content

feat: middle mouse button triggers and middle+right chord - #195

Open
mew1033 wants to merge 1 commit into
pablopunk:mainfrom
mew1033:middle-mouse-button-move
Open

mew1033 wants to merge 1 commit into
pablopunk:mainfrom
mew1033:middle-mouse-button-move

Conversation

@mew1033

@mew1033 mew1033 commented Nov 3, 2026

Copy link
Copy Markdown

Summary

Adds Middle as a mouse-button trigger for Move and Resize, plus a middle + right chord, both usable with a keyboard shortcut or on their own. The motivating setup: Move = middle button alone (the scroll wheel on a Logitech MX Master), Resize = middle + right held together, no keyboard involved. Existing left/right/both and keyboard triggers behave as before.

Related: #16 (mouse buttons as shortcuts), #69 (mouse-only chord).

How it works

  • MouseButton gains .middle and .middleRight and now describes a trigger as a set of physical buttons (buttons, isChord, canTriggerWithoutKeyboard), with the CGEvent types of each physical button. physicalButton(of:) maps an event back to left/right/middle and returns nil for side buttons, which share the otherMouse* event types and pass through untouched.
  • MouseOnlyActionManager replaces MouseChordActionManager and handles every mouse-only trigger in one state machine, so two mouse-only triggers can share a button. It watches the union of configured buttons: a single-button trigger starts once the pointer moves past swiftShiftClickDragThreshold, a chord starts on the second press, a press that can no longer complete any trigger is replayed to the app, and a click that never became a gesture is replayed on release. A gesture ends when one of its own buttons is released.
  • The keyboard + chord path (startTrackingWithChord, formerly startTrackingWithBothMouseButtons) works for any two-button chord; MouseTracker derives its drag mask from the trigger's buttons.
  • Settings: a third Middle option in the mouse picker. Toggling buttons works on the set (left + right = both, middle + right = the new chord; combinations without a trigger collapse to the tapped button). Mouse-only shortcuts accept the middle button or a chord, and two mouse-only shortcuts only conflict when they use the same buttons. Labels no longer wrap ("Middle" was breaking onto two lines).

Behavior note for any chord that includes the right button (true for the existing left + right chord too): a plain right-click is delivered on release rather than on press, because the app has to wait to see whether the other button follows.

No new files, no project-file changes.

Test plan

  • make build and make test (unit tests cover MouseButton parsing, the buttons ↔ trigger round trip, canTriggerWithoutKeyboard, and event → physical-button mapping incl. side buttons)
  • Move = middle (mouse only): wheel-press + drag moves windows on a Logitech MX Master
  • Resize = middle + right (mouse only): hold both and move; release either to stop
  • Plain middle click on a browser link opens a new tab; plain right-click still opens the context menu (on release); middle click in an ignored app passes through
  • Keyboard + Middle and keyboard + middle + right with the modifier held
  • Existing left + right chord unchanged
  • Mouse side buttons unaffected
  • Still works after switching Spaces and after sleep/wake

🤖 Generated with Claude Code

Adds `MouseButton.middle` and `MouseButton.middleRight` next to
left/right/both, so a window can be moved or resized by pressing the
middle button (e.g. the scroll wheel on a Logitech MX Master) anywhere
in it and dragging, or by holding middle + right together, with or
without a keyboard shortcut.

`MouseButton` now describes a trigger as a set of physical buttons
(`buttons`, `isChord`, `canTriggerWithoutKeyboard`) and knows the
CGEvent types of each physical button. `physicalButton(of:)` maps an
event back to left/right/middle and returns nil for side buttons, which
share the otherMouse* event types and must pass through untouched.

Mouse-only triggers are handled by one `MouseOnlyActionManager`
(replacing `MouseChordActionManager`), so several mouse-only triggers
can coexist even when they share a button, e.g. Move = middle and
Resize = middle + right. It watches the union of the configured
buttons: a single-button trigger starts once the pointer moves past the
click threshold, a chord starts on the second press, a press that can
no longer complete a trigger is replayed to the app, and a plain click
that never became a gesture is replayed on release.

The keyboard + chord path (`startTrackingWithChord`) is generalized
from left + right to any two-button chord, and `MouseTracker` derives
its drag mask from the trigger's buttons.

Settings: the mouse picker gains a Middle option; combinations without
a trigger collapse to the tapped button; mouse-only shortcuts accept the
middle button or a two-button chord, and two mouse-only shortcuts only
conflict when they use the same buttons. The button labels no longer
wrap at narrow widths.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@codesandbox

codesandbox Bot commented Nov 3, 2026

Copy link
Copy Markdown

Review or Edit in CodeSandbox

Open the branch in Web Editor • VS Code • Insiders

Open Preview

@vercel

vercel Bot commented Nov 3, 2026

Copy link
Copy Markdown

@mew1033 is attempting to deploy a commit to the Pablo Varela's projects Team on Vercel.

A member of the Team first needs to authorize it.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant