Skip to content

Add mouse chord move gesture and update alerts#159

Merged
pablopunk merged 7 commits into
pablopunk:mainfrom
Vida-CruX:feature/add-press-left-right-mouse-together
Jun 11, 2026
Merged

Add mouse chord move gesture and update alerts#159
pablopunk merged 7 commits into
pablopunk:mainfrom
Vida-CruX:feature/add-press-left-right-mouse-together

Conversation

@Vida-CruX

@Vida-CruX Vida-CruX commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Implemented #69 , but there is one edge case worth calling out: if the cursor is already hovering over an interactive element, such as a link or button, the first mouse-down event may still reach the underlying app because Swift Shift only knows this is a “both mouse buttons” move gesture after the second button is pressed. Once both buttons are down, we start the move and cancel subsequent drag/up events, so most normal buttons/links should not activate because they usually require the completed click or mouse-up. However, controls that trigger behavior on mouseDown, or apps that open context menus immediately on right mouse down, may still react before Swift Shift takes over. I tried multiple approach but still, I didn't find a perfect solution.
You may want to mark this feature experimental.
I also haven’t tested this behavior on multi-display setups yet because I don't have external display to test. You may want to test it before release.

Summary by CodeRabbit

  • New Features

    • Move windows using both mouse buttons; per-shortcut toggles for keyboard vs. mouse triggers; persistent enablement for each shortcut.
  • Improvements

    • More reliable, coordinate-aware mouse tracking (including external updates) with queued updates and improved move/resize success handling.
    • Shortcut UI: persistent toggles, explicit clear, updated mouse-button picker, conflict warnings; update notifications behave better.
    • Main window slightly wider.
  • Removed

    • “Require mouse click” preference and its toggle.

Add an optional “Move with both mouse buttons” preference that lets users move windows without a keyboard shortcut.
  - Add MouseChordMoveManager to track left/right mouse button chords
  - Extend MouseTracker and WindowManager to support CoreGraphics mouse updates and cursor-based window lookup
  - Initialize and clean up mouse chord subscriptions with the app lifecycle
  - Improve Sparkle update presentation by temporarily activating the app, showing Dock attention for scheduled updates, and restoring accessory mode afterward
@codesandbox

codesandbox Bot commented Jun 4, 2026

Copy link
Copy Markdown

Review or Edit in CodeSandbox

Open the branch in Web EditorVS CodeInsiders

Open Preview

@vercel

vercel Bot commented Jun 4, 2026

Copy link
Copy Markdown

@Vida-CruX 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.

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 19678845-0091-4a54-8384-1e16d29f5612

📥 Commits

Reviewing files that changed from the base of the PR and between 75d58c6 and 1b4c1de.

📒 Files selected for processing (3)
  • Swift Shift/src/Manager/MouseTracker.swift
  • Swift Shift/src/Manager/ShortcutsManager.swift
  • Swift Shift/src/Manager/WindowManager.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Swift Shift/src/Manager/ShortcutsManager.swift

Walkthrough

This PR adds both-button mouse-chord move support and coordinate-space-aware mouse tracking, persists per-shortcut keyboard/mouse enable flags, introduces MouseChordActionManager to subscribe to left/right chord events, refactors ShortcutView and related preferences/UI, increases the main window width, updates AppDelegate to wire chord lifecycle, changes WindowManager.move to return AXError, and refactors UpdatesManager into an NSObject singleton implementing SPUStandardUserDriverDelegate.

Sequence Diagram(s)

sequenceDiagram
  participant User as User (both buttons)
  participant MouseChordActionManager
  participant ShortcutsManager
  participant MouseTracker
  participant WindowManager
  User->>MouseChordActionManager: left+right down
  MouseChordActionManager->>ShortcutsManager: check hasActiveShortcut / mouse-only config
  MouseChordActionManager->>MouseTracker: startTrackingForExternalMouseUpdates(initialLocation)
  MouseTracker->>WindowManager: move(window, to: point)
  User->>MouseChordActionManager: release chord
  MouseChordActionManager->>MouseTracker: stop/reset tracking
Loading

Possibly related PRs

  • pablopunk/SwiftShift#109: Related changes to MouseTracker drag/resize processing and WindowManager move/resize behavior.
  • pablopunk/SwiftShift#149: Related ShortcutsManager mouse subscription lifecycle and defensive unsubscribe/re-subscribe behavior.
  • pablopunk/SwiftShift#83: Related startup/shutdown shortcut cleanup and mouse-subscription wiring overlapping AppDelegate/ShortcutsManager changes.

Suggested reviewers

  • pablopunk
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add mouse chord move gesture and update alerts' accurately describes the main feature addition—mouse chord detection for window movement—and is concise and specific.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
Swift Shift/src/Manager/MouseTracker.swift (1)

34-43: ⚡ Quick win

Restrict external tracking to .move until resize math is coordinate-space aware.

Only the move path compensates for .coreGraphics. determineQuadrant and resizeWindowBasedOnMouseLocation still assume AppKit Y semantics, so a future .resize caller here will pick the wrong quadrant and invert vertical deltas. Tightening this entry point to .move would make the contract match the implementation.

✂️ Proposed fix
 `@discardableResult`
 func startTrackingForExternalMouseUpdates(for action: MouseAction, initialMouseLocation: NSPoint) -> Bool {
+    guard action == .move else {
+        assertionFailure("External mouse updates currently support .move only")
+        return false
+    }
     if currentAction != .none { stopTracking(for: currentAction) }
     prepareTracking(for: action, mouseLocation: initialMouseLocation, coordinateSpace: .coreGraphics)
     guard trackedWindow != nil else {
         return false
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Swift` Shift/src/Manager/MouseTracker.swift around lines 34 - 43, The
entrypoint startTrackingForExternalMouseUpdates(for:initialMouseLocation:)
currently allows any MouseAction but only the `.move` path compensates for
coreGraphics Y semantics; change it to reject non-move actions by returning
false when action != .move so external callers cannot start a `.resize` path
that will call determineQuadrant or resizeWindowBasedOnMouseLocation (which
assume AppKit Y) until those functions are made coordinate-space aware; keep
prepareTracking(...) and coreGraphics usage for `.move` but prevent other
actions at this API boundary.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Swift` Shift/src/Manager/ShortcutsManager.swift:
- Around line 706-726: The current logic conflates "window move is active" with
"chord sequence suppression" so stopChordMove(resetButtons: false) clears
isMoveActive on the first button-up and lets later drag/up events through;
change this by splitting those concerns: introduce a separate suppression flag
(e.g., isChordSuppressed) or suppression state in ShortcutsManager and set it
when the chord starts, have handleDrag(_:) and handleButtonUp(_:) check the new
isChordSuppressed (in addition to isMoveActive) and continue to cancel events
while either leftButtonIsDown or rightButtonIsDown is true, and only clear
isChordSuppressed when both buttons are released; leave
stopChordMove(resetButtons:) to stop the move behavior (isMoveActive) but do not
clear the suppression flag until both buttons are up (or add logic in
stopChordMove to conditionally preserve suppression when one button remains
down).

In `@Swift` Shift/src/Manager/UpdatesManager.swift:
- Around line 45-46: The clearUpdateAttention function currently sets
NSApp.dockTile.badgeLabel to an empty string which is unreliable; change it to
assign nil to NSApp.dockTile.badgeLabel so the Dock badge is properly cleared
across macOS versions. Locate the private func clearUpdateAttention() and
replace the empty-string assignment with a nil assignment to
NSApp.dockTile.badgeLabel.
- Around line 5-7: The Sparkle startup path should not require opening
UpdatesView: in UpdatesManager ensure the SPUStandardUpdaterController
initializer doesn't capture self during app command startup by passing
userDriverDelegate: nil (leave updaterDelegate as needed) so
UpdatesManager.shared.controller and .updater can initialize from
CheckUpdatesButton() without referencing UI; also update clearUpdateAttention()
to clear the Dock badge by setting NSApp.dockTile.badgeLabel = nil (not the
empty string) so the badge absence uses the optional contract.

In `@Swift` Shift/src/Manager/WindowManager.swift:
- Around line 46-52: The AX fast-path in getCurrentWindow filters out the self
process but the CGWindowList fallback does not, allowing Swift Shift windows to
be returned; update getTopWindowAtCursorUsingCGWindowList to accept an
excludingProcessID: pid_t? parameter (default nil) and skip entries whose
kCGWindowOwnerPID equals that PID, then call
getTopWindowAtCursorUsingCGWindowList(mouseLocation: mouseLocation,
excludingProcessID: NSRunningApplication.current.processIdentifier) from
getCurrentWindow so the self-process filter is applied to both paths.

---

Nitpick comments:
In `@Swift` Shift/src/Manager/MouseTracker.swift:
- Around line 34-43: The entrypoint
startTrackingForExternalMouseUpdates(for:initialMouseLocation:) currently allows
any MouseAction but only the `.move` path compensates for coreGraphics Y
semantics; change it to reject non-move actions by returning false when action
!= .move so external callers cannot start a `.resize` path that will call
determineQuadrant or resizeWindowBasedOnMouseLocation (which assume AppKit Y)
until those functions are made coordinate-space aware; keep prepareTracking(...)
and coreGraphics usage for `.move` but prevent other actions at this API
boundary.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 058a8850-859b-4994-afe9-cf9bce35bcf3

📥 Commits

Reviewing files that changed from the base of the PR and between 901243d and e636f41.

📒 Files selected for processing (8)
  • Swift Shift/src/AppDelegate.swift
  • Swift Shift/src/Manager/MouseTracker.swift
  • Swift Shift/src/Manager/Preferences.swift
  • Swift Shift/src/Manager/ShortcutsManager.swift
  • Swift Shift/src/Manager/UpdatesManager.swift
  • Swift Shift/src/Manager/WindowManager.swift
  • Swift Shift/src/View/PreferencesView.swift
  • Swift Shift/src/View/ShortcutView.swift
💤 Files with no reviewable changes (1)
  • Swift Shift/src/View/ShortcutView.swift

Comment thread Swift Shift/src/Manager/ShortcutsManager.swift
Comment thread Swift Shift/src/Manager/UpdatesManager.swift
Comment thread Swift Shift/src/Manager/UpdatesManager.swift Outdated
Comment thread Swift Shift/src/Manager/WindowManager.swift Outdated
@vercel

vercel Bot commented Jun 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
swiftshift-app Ready Ready Preview, Comment Jun 4, 2026 8:21am

@pablopunk

Copy link
Copy Markdown
Owner

I've got another proposal @Vida-CruX . Why only moving and not resizing? We can't do both, but we can let the users choose!

I've had some POC on another branch to refactor the settings to allow more mouse/trackpad movements, which is challenging because we keep adding options to that top list and it's a worse experience IMO for a minimal app

Here's my proposal instead: less buttons! (made from scratch for this PR, I might update it in the future if I add trackpad gestures). As you can see it allows the chord for both, with a safeguard so either you have shortcut+chord or just one chord at a time

CleanShot.2026-06-05.at.20.58.54.yafw.balanced.mp4

WDYT? I'll push it to your PR and we'll discuss here

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
Swift Shift/src/Manager/ShortcutsManager.swift (1)

824-835: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep suppressing the chord sequence until both buttons are released.

Line 825 and Line 834 stop the active move immediately, but suppression also drops at that point. With one button still held, later drag/up events can leak to the foreground app mid-sequence.

Proposed fix (separate move-active vs sequence-suppression)
 class MouseChordActionManager {
   static let shared = MouseChordActionManager()
@@
   private var activeAction: MouseAction?
+  private var isChordSuppressed = false
@@
   private func handleButtonDown(_ event: CGEvent) {
-    if activeAction != nil {
+    if activeAction != nil || isChordSuppressed {
       event.cancel()
       return
     }
@@
   private func handleButtonUp(_ event: CGEvent) {
-    if activeAction != nil {
+    if activeAction != nil || isChordSuppressed {
       event.cancel()
-      stopChordAction(resetButtons: false)
+      stopChordAction(resetButtons: false)
+      if !leftButtonIsDown && !rightButtonIsDown {
+        isChordSuppressed = false
+      }
     }
   }
@@
   private func handleDrag(_ event: CGEvent) {
-    if activeAction != nil {
+    if activeAction != nil || isChordSuppressed {
       guard leftButtonIsDown && rightButtonIsDown && !ShortcutsManager.shared.hasActiveShortcut else {
         stopChordAction(resetButtons: false)
+        if !leftButtonIsDown && !rightButtonIsDown {
+          isChordSuppressed = false
+        }
         return
       }
@@
   private func startChordActionIfReady(eventToCancelOnSuccess event: CGEvent) {
@@
     if MouseTracker.shared.startTrackingForExternalMouseUpdates(for: action, initialMouseLocation: event.location) {
       activeAction = action
+      isChordSuppressed = true
       event.cancel()
     }
   }

Also applies to: 861-871

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Swift` Shift/src/Manager/ShortcutsManager.swift around lines 824 - 835, The
chord handling currently calls stopChordAction(resetButtons: false) from
handleButtonUp and handleDrag as soon as the move ends, but that also lifts
sequence suppression and allows events to leak while one button remains; modify
the logic so stopping the active move is separated from lifting sequence
suppression—introduce either a new method (e.g., stopMoveAction()) or an
optional parameter on stopChordAction (e.g., keepSuppression: Bool) and use it
in handleButtonUp and handleDrag to stop the activeAction while keeping
suppression active until both leftButtonIsDown and rightButtonIsDown are false
and ShortcutsManager.shared.hasActiveShortcut is false; apply the same change to
the other similar block around lines 861–871 so suppression is only cleared when
both buttons are released.
🧹 Nitpick comments (1)
Swift Shift/src/Manager/ShortcutsManager.swift (1)

788-793: ⚡ Quick win

Avoid loading shortcut config from UserDefaults on every mouse event.

Line 789 re-resolves configuration for every down/up/drag event. This path is hot; cache the configured mouse-only action in-memory and refresh it in updateSubscriptions() / shortcut-change hooks.

Also applies to: 873-882

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Swift` Shift/src/Manager/ShortcutsManager.swift around lines 788 - 793, The
handle(_ event: CGEvent) path currently calls configuredMouseOnlyAction() on
every mouse event which hits UserDefaults frequently; cache the resolved
configured mouse-only action in a stored property (e.g., cachedMouseOnlyAction)
and update that cache from updateSubscriptions() and any shortcut-change hooks
instead of re-reading UserDefaults inside handle(_:), and replace calls to
configuredMouseOnlyAction() in handle(_:) and the similar block around the other
event handler (the code currently at the other decision branch) to use the
cached property; ensure stopChordAction(resetButtons:) and unsubscribe()
behavior remains the same when the cached value is nil.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@Swift` Shift/src/Manager/ShortcutsManager.swift:
- Around line 824-835: The chord handling currently calls
stopChordAction(resetButtons: false) from handleButtonUp and handleDrag as soon
as the move ends, but that also lifts sequence suppression and allows events to
leak while one button remains; modify the logic so stopping the active move is
separated from lifting sequence suppression—introduce either a new method (e.g.,
stopMoveAction()) or an optional parameter on stopChordAction (e.g.,
keepSuppression: Bool) and use it in handleButtonUp and handleDrag to stop the
activeAction while keeping suppression active until both leftButtonIsDown and
rightButtonIsDown are false and ShortcutsManager.shared.hasActiveShortcut is
false; apply the same change to the other similar block around lines 861–871 so
suppression is only cleared when both buttons are released.

---

Nitpick comments:
In `@Swift` Shift/src/Manager/ShortcutsManager.swift:
- Around line 788-793: The handle(_ event: CGEvent) path currently calls
configuredMouseOnlyAction() on every mouse event which hits UserDefaults
frequently; cache the resolved configured mouse-only action in a stored property
(e.g., cachedMouseOnlyAction) and update that cache from updateSubscriptions()
and any shortcut-change hooks instead of re-reading UserDefaults inside
handle(_:), and replace calls to configuredMouseOnlyAction() in handle(_:) and
the similar block around the other event handler (the code currently at the
other decision branch) to use the cached property; ensure
stopChordAction(resetButtons:) and unsubscribe() behavior remains the same when
the cached value is nil.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e9b43a38-bae0-4e7f-b47f-e06087c605c7

📥 Commits

Reviewing files that changed from the base of the PR and between e636f41 and 7f0d3ae.

📒 Files selected for processing (7)
  • Swift Shift/src/AppDelegate.swift
  • Swift Shift/src/Constants.swift
  • Swift Shift/src/Manager/MouseTracker.swift
  • Swift Shift/src/Manager/ShortcutsManager.swift
  • Swift Shift/src/View/PreferencesView.swift
  • Swift Shift/src/View/SettingsView.swift
  • Swift Shift/src/View/ShortcutView.swift
💤 Files with no reviewable changes (1)
  • Swift Shift/src/View/PreferencesView.swift
✅ Files skipped from review due to trivial changes (1)
  • Swift Shift/src/Constants.swift
🚧 Files skipped from review as they are similar to previous changes (2)
  • Swift Shift/src/AppDelegate.swift
  • Swift Shift/src/Manager/MouseTracker.swift

@pablopunk

Copy link
Copy Markdown
Owner

Update: made it so it's obvious why you cannot do this action

CleanShot.2026-06-05.at.21.11.50.yafw.balanced.mp4

@Vida-CruX

Copy link
Copy Markdown
Contributor Author

Looks great! Please go ahead and push it to this PR. I’m happy to test it from there.
Also, have you come up with any ideas for solving the issue where a mouse button is pressed but the release event is not handled correctly?

@pablopunk

pablopunk commented Jun 6, 2026

Copy link
Copy Markdown
Owner

@Vida-CruX it's pushed 😊

no i haven't touched the button detection logic

@pablopunk

Copy link
Copy Markdown
Owner

FYI I think when I was testing this PR the option to focus the window does not work anymore

Vida-CruX added 3 commits June 9, 2026 20:51
Keyboard + Both mouse path now cancels the first mouse down immediately, not only the second. Mouse-only chord path now cancels and stores the first down event. If the second button completes the chord, that first click is discarded. If it was just a normal click/drag, the saved event is replayed so regular mouse use still works. Replayed events are tagged so Swift Shift ignores its own synthetic mouse events.
@Vida-CruX

Copy link
Copy Markdown
Contributor Author

I tried a different approach, and the chord mouse feature now seems to work well.I also refactored MouseTracker, which should improve resize performance, especially in Electron apps.
I haven’t done a full pass yet, so there may still be some niche bugs.

@pablopunk

Copy link
Copy Markdown
Owner

@Vida-CruX seems to be working most of the time. Anything else on this PR or reaady to merge?

@pablopunk

Copy link
Copy Markdown
Owner

@codex review this PR

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@Vida-CruX

Copy link
Copy Markdown
Contributor Author

@Vida-CruX seems to be working most of the time. Anything else on this PR or reaady to merge?

Looks good to me. I think it’s ready to merge.

@pablopunk pablopunk merged commit 278dce5 into pablopunk:main Jun 11, 2026
4 of 5 checks passed
@pablopunk

Copy link
Copy Markdown
Owner

@Vida-CruX released on 1.3.0. made it a bit more minimal where buttons are toggle-like now, so "both" is replaced by actually toggling both on

CleanShot 2026-06-11 at 17 35 09

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.

2 participants