Skip to content

feat: add "both" mouse warp axis for multi-row monitor layouts#231

Draft
adelin-b wants to merge 81 commits into
BarutSRB:mainfrom
adelin-b:feat/mouse-warp-both-axis
Draft

feat: add "both" mouse warp axis for multi-row monitor layouts#231
adelin-b wants to merge 81 commits into
BarutSRB:mainfrom
adelin-b:feat/mouse-warp-both-axis

Conversation

@adelin-b
Copy link
Copy Markdown
Contributor

@adelin-b adelin-b commented Apr 11, 2026

Summary

  • Adds a both case to MouseWarpAxis enabling simultaneous horizontal and vertical mouse warping
  • Uses geometric adjacency detection instead of the ordered monitor list, supporting true 2D layouts
  • Solves the problem where stacked multi-monitor setups (e.g., ultrawide above a row of monitors) can't use mouse warp in both directions

Problem

With the current horizontal / vertical axis options, users with multi-row monitor layouts like:

              [ultrawide]
[side-left]  [main display]  [side-right]

...must choose between horizontal warp (left/right between bottom row) OR vertical warp (up/down to ultrawide), but not both. Setting mouseWarpAxis: "both" previously fell back to horizontal since the enum didn't have that case.

Solution

MouseWarpAxis.swift:

  • Added case both with display metadata (name, symbols, sort order)
  • both uses horizontal-primary sorting for the monitor order list

MouseWarpHandler.swift:

  • Added mouseWarpGeometricNeighbor(for:direction:among:) — finds the nearest monitor sharing an edge in a given direction, with axis overlap check. This replaces the ordered-list lookup for both mode.
  • Added mouseWarpToAdjacentMonitor(_:edge:transferRatio:warpAxis:margin:) — warps directly to a specific monitor.
  • handleMouseWarpMoved: checks all 4 edges (left/right → horizontal warp, top/bottom → vertical warp)
  • mouseWarpBackToMonitor: clamps cursor in both X and Y when escaping
  • mouseWarpClampCursorToNearestMonitor: uses monitorApproximation for both
  • mouseWarpDestinationPoint: handles all edge/direction combinations for both
  • Vertical warp fallback paths now also activate for .both axis

How it works

Unlike horizontal/vertical which use the ordered mouseWarpMonitorOrder list, both uses geometric adjacency — it finds the nearest monitor that shares an edge boundary with Y-overlap (for horizontal neighbors) or X-overlap (for vertical neighbors). This correctly handles 2D layouts where a simple ordered list cannot capture spatial relationships.

Test plan

  • Set mouseWarpAxis: "both" in settings.json
  • Verify horizontal warp works between side-by-side monitors (left/right edges)
  • Verify vertical warp works between stacked monitors (top/bottom edges)
  • Verify cursor clamping at monitor corners (no escape to dead zones)
  • Verify existing horizontal and vertical modes are unaffected
  • Test with 2+ monitors in non-linear arrangements

Related

Summary by CodeRabbit

  • New Features
    • Added a new "both" axis option for mouse warping that enables simultaneous horizontal and vertical coordinate adjustment when transitioning between monitors at their edges.
    • Implements geometric neighbor detection to automatically select the appropriate adjacent monitor based on cursor proximity to monitor boundaries.

Barut and others added 30 commits April 6, 2026 14:43
Replace the Niri axis solver, viewport geometry helpers, and monitor restore assignment matcher with Zig implementations behind a small C ABI surface.

Add a dedicated COmniWMKernels header target, a reproducible kernel build script, focused Swift and Zig regression coverage, and the documentation/build updates needed to rebuild and test the new library.
Stabilize the layout and CLI regression coverage after the Zig kernel port.

Enable animations explicitly in the shared layout-plan fixture, make the scratchpad async reveal test use the controller-local AX frame override after setup refreshes settle, add deterministic viewport settling in the Niri animation-toggle regressions, widen the injected CLI watch timeouts under aggregate load, and ignore Zig build cache output.
Pin shared build metadata for the macOS target, Zig toolchain, and Ghostty archive digest.

Add reusable preflight helpers plus canonical make targets for build, test, verify, and release-check workflows.

Refactor Zig and packaging scripts to consume the pinned metadata and fail fast on provenance or configuration mismatches.
Introduce per-target SwiftLint configs and target-specific thresholds so
the
stricter root lint policy can be rolled out without forcing legacy-heavy
modules to fail immediately.

Clean up the surfaced violations across Core, UI, IPC, Quake Terminal,
and
tests by replacing unsafe casts and `try!` usage, removing unnecessary
`async`, tightening access control, and marking unsupported
`init(coder:)`
paths unavailable.

Refresh the contributor docs with the supported `make build`, `make
test`,
`make verify`, and `make release-check` workflows, and align the AX/Niri
regressions with the cleanup.
Rewrite the scratchpad async fronting test to use the real async AX frame-apply path instead of the synchronous frameApplyOverrideForTests hook.

Add a small layout-plan test helper that installs an AppAXContext and disables the default synchronous override for async-sensitive tests.

This keeps the production scratchpad code unchanged while stabilizing the previously failing scratchpad test and the full Swift suite.
Move the pure Dwindle frame solver behind the COmniWMKernels C ABI and have Swift flatten the workspace tree into a kernel snapshot before reapplying solved frames to existing nodes.

This removes the Swift gap and min-size recursion, adds the new Zig solver entry point, and expands Dwindle regression coverage for single-window, fullscreen, gap, placeholder, and min-size behaviors.

Tested with: swift test --filter DwindleLayoutEngineTests
Fix typo in README for 'Scratchpad'
Stop using pending reveal transactions for workspaceInactive reveals so cross-workspace activation no longer re-hides windows or re-suppresses frame writes after transient verification misses.

Keep delayed reveal verification for floating and scratchpad-style async restores, and add regression coverage for the layout refresh, inactive-workspace activation, and workspace-bar focus paths.

Fixes BarutSRB#204
Fixes BarutSRB#207
Move Niri's deterministic bulk projection/layout slice out of Swift and into the Zig kernel library behind `omniwm_niri_layout_solve`.

Swift keeps ownership of the Niri tree, viewport state, monitor selection, and AppKit policy while flattening the current columns and windows into compact snapshot arrays, invoking one bulk solve, and applying the returned canonical/rendered rects, resolved spans, and hidden-edge classifications back onto the existing nodes.

This removes the old Swift-side projection math for container rects, window frames, visibility and overflow handling, and single-window aspect fitting, adds ABI coverage for empty and bounded caller-allocated buffers, and updates the architecture docs to document the Niri leaf-kernel boundary.

Tests:
- zig build test
- swift test --filter NiriLayoutEngineTests
- swift test --filter NiriConstraintSolverTests
- swift test --filter ViewportGeometryTests
- swift test --filter MonitorRestoreAssignmentsTests
- swift test --filter NiriLayoutKernelABITests
Move the deterministic reconcile reducer and restore-intent solve behind a compact Zig-backed C ABI.

Keep RuntimeStore, Planner, event normalization, trace recording, invariant validation, and runtime object mutation Swift-owned while StateReducer becomes a thin marshal/decode seam over omniwm_reconcile_plan and omniwm_reconcile_restore_intent.

Thread persisted hydration through the same solve, add direct ABI coverage plus Zig unit tests for reconcile behavior, and update architecture docs to document the new leaf-kernel boundary.

Testing:
- zig build test
- swift test --filter ReconcileStateTests
- swift test --filter ReconcileKernelABITests
- make kernels-test
- make test
Move keyboard focus border ownership and lifecycle decisions into the
BorderCoordinator reconcile loop so border visibility, ordering, and
fallback behavior stay consistent across focus changes, CGS events,
workspace transitions, and fullscreen/teardown paths.

- track managed and fallback owners with generation-scoped state,
  fallback leases, live-motion revalidation, and bounded trace records
  so stale frame/teardown events are ignored instead of reviving old
  borders
- derive ordering metadata and corner radius from validated window
  server state while keeping BorderManager and BorderWindow focused on
  rendering and preventing hidden config changes from replaying stale
  target state
- route WMController, AXEventHandler, layout refresh, workspace,
  service lifecycle, and Niri animation border updates through explicit
  reconcile sources and add regression tests plus architecture notes
  for the new behavior
Use one exact snappy spring preset for normal-mode animations.

Remove balanced/gentle presets and per-call spring tuning.

Make Reduce Motion resolve to the exact reducedMotion preset.

Switch Niri movement, workspace switching, overview animation, and window-close motion to plain snappy.

Add tests for spring config resolution, Niri config wiring, and close-animation settling.

Update architecture docs to reflect the new preset model.
Replace Dwindle's cubic window motion with a dedicated no-bounce spring
configuration and thread display refresh rate through workspace
snapshots
so motion settles consistently across monitors.

Snap canonical and animated Dwindle frames to physical pixels, seed new
split insertions from the split edge, and make fullscreen exclusive per
workspace to avoid overlapping fullscreen leaves.

Unify Dwindle settings application through the handler snapshot context,
remove the unused cubic animation path, and harden AX frame writes by
rejecting invalid target geometry without retrying.

Add regression coverage for Dwindle animation lifecycle, pixel snapping,
refresh-rate plumbing, insertion seeds, fullscreen exclusivity, and AX
invalid-frame rejection.

Verified with:
- swift test --filter
'DwindleLayoutEngineTests|SpringAnimationTests|AXManagerTests'
- swift test --skip-build
Record hotkeys directly from the responder chain instead of relying on a
local event monitor.

This lets the recorder capture command-based key equivalents before
AppKit swallows them, which fixes top-row workspace shortcuts on
Czech/QWERTZ and other non-QWERTY layouts.

Add regression tests that verify command plus top-row keys are stored by
physical key code, including through performKeyEquivalent. Fixes BarutSRB#171
Refresh resolved Niri monitor settings whenever global Niri config changes so monitors inheriting defaults update immediately from the settings window.

Route monitor-specific Niri refreshes through the same helper and add regression coverage for live center-focused-column and single-window-aspect-ratio propagation.
Accept AXStandardWindow subrole windows during AX enumeration even when the AX role is non-standard or missing, so Emacs-style windows remain tracked across active-space changes and wake/unlock full rescans.

Fixes BarutSRB#197
Route quake terminal hide/show through the shared focus pipeline so manual close restores the latest valid focus target instead of replaying a stale app snapshot.

Add regression coverage for manual close, focus-loss auto-hide, and external focus fallback paths.

Fixes BarutSRB#212
Move the deterministic Overview projection path out of Swift and into the
existing Zig kernels library.

Replace the generic workspace and Niri overview projection math in
OverviewLayoutCalculator with a bulk omniwm_overview_projection_solve FFI
call. The new kernel owns frame normalization, scale fitting, projected
window and column geometry, drop-zone generation, total content height,
and scroll bounds, while Swift keeps snapshot extraction, search,
navigation, thumbnails, and result application.

Extend the checked-in C ABI with explicit overview snapshot and result
structs, add the new Zig solver module, and update the universal archive
build step to run ranlib after lipo so the rebuilt static library links
cleanly.

Add direct ABI regression coverage plus extra Overview projection
characterization tests, and verify the change with:
- make kernels-build
- make kernels-test
- swift test --filter OverviewProjectionKernelABITests
- swift test --filter Overview
Replace the deterministic WindowRuleEngine base-decision tree with a compact Zig-backed window decision kernel behind omniwm_window_decision_solve.

Keep rule compilation, regex matching, title gating, metadata ownership, and manual overrides in Swift while reattaching workspace and rule-effect metadata after the kernel decode.

Add ABI coverage, decision regression tests, higher-level admission/manual-override coverage, and architecture notes for the new leaf-kernel boundary.
Seed native fullscreen restore snapshots before command-driven, direct-activation, and full-rescan fullscreen transitions so restored windows can replay their managed geometry after exiting AppKit fullscreen.

Keep restore records alive through the first relayout commit, suppress Niri and Dwindle animations during that restore pass, and force-apply the captured frame before clearing lifecycle state.

Expand AX event, refresh routing, and layout refresh tests to cover command-driven, direct-activation, replacement-token, and restore-plan finalization flows.
Normalize refresh, focus, and activation controller inputs into an explicit orchestration snapshot/event/plan boundary and route deterministic planning through OrchestrationCore.

Thin WMController, LayoutRefreshController, and AXEventHandler to adapter/executor roles, keep runtime ownership and platform effects in Swift, and add seam-level regression tests for refresh coalescing, focus supersede/defer, and native fullscreen restore activation planning.
Replace the Swift orchestration reducer with a Swift encoder/decoder
around
the new `omniwm_orchestration_step` C ABI. The Zig kernel now owns the
deterministic refresh and managed-focus state transitions, while Swift
continues to own macOS-facing effects like AX focus, borders, workspace
activation, refresh tasks, and retry scheduling.

Add the orchestration ABI surface to `COmniWMKernels`, including
flattened
refresh/focus snapshots, activation observations, window-removal
payloads,
decisions, ordered actions, and ABI layout reporting. The Swift wrapper
grows
caller-owned output buffers on `BUFFER_TOO_SMALL` and decodes the
returned
snapshot, decision, and action plan back into existing OmniWM types.

Mirror kernel-owned managed-focus state back into
`FocusBridgeCoordinator`
and `WorkspaceManager`, and route activation handling through normalized
kernel events. This moves retry budget progression, retry exhaustion,
owned-application fallback, native fullscreen restore activation, and
managed
activation confirmation into the reducer boundary.

Preserve refresh merge behavior across the port, including affected
workspace
sets, window-removal payloads, post-layout attachments, visibility
follow-ups,
and cancelled-refresh restart state. Expand orchestration and ABI tests
to
cover the new kernel boundary and update architecture docs for the
kernel-backed orchestration flow.
Barut and others added 18 commits April 19, 2026 00:25
Replace axis-only fallback clamping with nearest-monitor selection,
preserve the orthogonal cursor coordinate across seams, and warp using
hardware plus a matching synthetic mouse-moved event to keep
focus-follows-mouse working reliably.

Also suppress cursor automation while the lock screen is active and add
regression coverage for clamp selection, cross-monitor landing geometry,
warp sequencing, suppression, and focus-follows-mouse reevaluation after
a warp.
Some apps (notably WeChat / com.tencent.xinWeChat) close their windows
via NSWindow.orderOut and reopen via orderFront, which produces no
kCGSpaceWindowCreated event. The window's existing OmniWM entry may
also have been removed earlier by a stray kAXUIElementDestroyedNotification
fired by the AX layer. After that, activation is the only signal that
a manageable window still exists, but the prior code in
handleAppActivation simply emitted a non_managed_focus_changed event
and never attempted admission.

Insert a recovery path in handleAppActivation, mirroring the existing
native-fullscreen restore branch: when the focused window resolves to
a valid AX ref but workspaceManager has no entry for it, run
processCreatedWindow to admit it via the standard create-window
pipeline. If admission succeeds, route through the managed-activation
branch; otherwise fall through unchanged to the original .unmanaged
behavior.

Verified locally with WeChat: the previously unmanaged window is now
admitted as tiling on activation, and reconcile-debug shows
window_admitted firing for the WeChat pid. Side benefit: previously
silently-missed apps like Finder and System Settings are also picked
up.
…tivation

Two cases for the new recovery branch in handleAppActivation:

- activationAdmitsManageableWindowMissedByCreateEvent: pid has no entry,
  focused AX ref + window-server info + facts all indicate a manageable
  window. After activation, the window is admitted as tiling, becomes
  the focused token, and isNonManagedFocusActive is false. This is the
  WeChat-style scenario: orderOut/orderFront produces no
  kCGSpaceWindowCreated, so admission has to happen on activation.

- activationFallsBackToNonManagedWhenRecoveryAdmissionFails: same setup
  but no windowInfoProvider, so prepareCreateCandidate returns nil and
  the recovery branch leaves the workspace manager untouched. The
  original .unmanaged fallback still runs and isNonManagedFocusActive
  flips to true.

Both pass; full AXEventHandlerTests suite has 3 pre-existing failures
unrelated to this change (verified by rerunning the suite with the
patch stashed — they fail identically without the fix).
Bundle several correctness and performance passes across the relayout,
border, managed-restore, display-link, and mouse-warp paths.

Managed restore
- Route snapshot updates through confirmed-frame callbacks instead of
  pre-apply layout writes; AXManager's cached no-op and delayed-reveal
  success paths now publish through the same hook.
- Remove eager snapshot writes from Niri, Dwindle, and diff execution.
- Add a Phase B fast path that short-circuits rebuilds when the
  persisted snapshot is already semantically current, cache topology
  and fast-path identities, and invalidate on removal/rekey.

Relayout scoping
- Thread affectedWorkspaceIds through workspace-scoped immediate
  relayout and workspace-transition callsites so local actions no
  longer fall back to all active workspaces across monitors; covers
  scratchpad assignment and overview cross-workspace drag.
- Gate workspace-bar refreshes: only queue rebuilds for relayout
  reasons that change bar-visible state; geometry-only relayouts skip.
- Defer Niri frame application to animation ticks when a scroll
  animation is already active (new layout-plan flag; executor skips
  eager frame writes and hands off to the display-link tick).

Border cache
- Split managed-border reconcile invalidation rules by source so
  manual rerenders reuse validated metadata and eligibility when the
  owner and AX window are unchanged.
- Wire AX minimize/restore notifications into reconciliation so
  managed borders invalidate cleanly on miniaturize/restore; include
  corner radius in ordering-cache coherence.
- Add hot-path metrics for fast-path hits, eligibility-cache hits,
  and miss reasons.

Display-link and CGS
- Replace per-tick reverse scan of displayLinksByDisplay with a
  displayIdByLink cache (O(1) lookup); centralize bookkeeping and
  cover it on monitor disconnect.
- Separate retained window-notification ownership from sticky CGS
  request state; add optional unsubscribe support for newer SkyLight.
- Guard display-link scheduling per display across scroll, dwindle,
  and close-animation entry points.
- Move workspace-session scratch allocation from page_allocator to a
  per-call ArenaAllocator.

Mouse warp
- Extend the last-monitor warp fallback to horizontal axis, matching
  the existing vertical behavior.

Housekeeping
- Add MANIFESTO.md.
- Drop unused demo GIFs from assets/ (~110MB).
- Add performance phase-0 baseline template under docs/performance/.

Validation
- swift test --filter CGSEventObserverTests
- swift test --filter BorderCoordinatorTests
- swift test --filter LayoutRefreshControllerTests
- swift test --filter WorkspaceSessionKernelABITests
- swift test --filter RefreshRoutingTests
- zig build test
A trackpad gesture used to "scroll perfectly" on the first swipe and
then
visually snap back to the originally focused column on the next swipe.
The
visible behavior was the result of three independent bugs that
compounded:

1. NiriTopologyKernel.applyTopologyViewport reset selectionProgress to 0
   unconditionally. Every gesture tick triggers a relayout that runs
this
   function, so the per-tick accumulation in updateGesture was wiped out
   before it could ever cross the column-step threshold.
activeColumnIndex
   therefore never advanced during the gesture, and the post-gesture
   relayout pulled the viewport back to the original column. Skip the
   reset while a gesture is in flight.

2. updateGesture accumulated selectionProgress in raw deltaPixels but
   compared it against avgColumnWidth, which is in viewport pixels. For
   trackpad input these differ by normFactor (= viewportWidth /
   viewGestureWorkingAreaMovement, ~2.12 for a 2544px viewport), so
users
   needed roughly 2× a column's worth of finger motion to trigger a
step.
   Multiply deltaPixels by normFactor when accumulating so the
comparison
   is in matching units.

3. handleGestureEvent's empty-touches / missing-context guards called
   abortActiveGestureIfNeeded, which drops the gesture without snapping.
   In practice macOS often delivers a .changed event with empty touches
   immediately before .ended when the user lifts their fingers, so
almost
   every committed gesture was aborted before the .ended path could
   finalize it. The viewport was then left at an off-column offset.
   Introduce finalizeOrAbortActiveGesture, which finalizes (snap-to-
   column with velocity-aware projection) when a gesture is committed
   and only aborts otherwise.
Delete reconcile trace dumps, hot-path metrics, and debug trace plumbing from runtime, IPC, and CLI paths.

Remove trace/debug-only tests and obsolete performance baseline docs while keeping test synchronizers and real failure stderr.

Strip Swift and Zig source comments outside Package.swift as part of the cleanup pass.
Replace the Swift focus, refresh, and navigation planners with kernel-backed orchestration and navigation bridges.

Update layout, monitor, border, overview, and IPC integrations to consume the new kernel outputs.

Refresh ABI and behavior tests for orchestration, workspace navigation, IPC validation, and related layout flows.
Keep wheel-driven viewport offsets static while preserving them through
topology syncs, and reanchor the active column when scroll selection
moves so the view does not jump. Split mouse wheel scrolling from
trackpad gesture handling so trackpad release creates the expected
settle
spring while wheel scrolls remain non-gesture offsets.

Prepare column widths before layout/topology projection, normalize
trackpad gesture velocity for snapping, and avoid managed-border fast
paths for windows that need observed AX frames.

Add coverage for wheel scrolling, trackpad settle animation, viewport
reanchoring, and snap-target behavior.
Track workspace-bar projection invalidations in the Niri layout engine
and carry the pending workspace ids through refresh execution effects.
Mark topology-mutating kernel plans and direct cross-workspace
transfers, then clear the durable dirty state only after the existing
coalesced workspace-bar refresh path is requested.

Add refresh-routing coverage for Niri move-column, reorder, consume,
expel, direct workspace transfers, geometry-only negatives, focus-only
negatives, and pending invalidation persistence across skipped scoped
relayouts.
Preserve managed entries through native fullscreen transitions and debounce recently destroyed window IDs so transient AX enumeration gaps do not purge or re-admit stale windows.

Keep Niri restore state scoped to the captured workspace and topology, rekey cached column membership on replacement tokens, and avoid restoring from display-sized fullscreen frames when a tiled Niri cache is available.

Reject incomplete persisted restore keys and add regressions for restore seeding, cache migration, stale replacement handling, and cross-workspace Niri membership.
Carry the physical hidden edge through the Niri layout kernel so Swift can hide offscreen columns on the monitor-local side that avoids neighboring displays. Pass live monitor contexts into controller-driven Niri layout and harden regression coverage for centered two-monitor Niri workspaces.

Tests:
- git diff --check
- make kernels-test
- swift test --filter NiriLayoutKernelABITests
- swift test --filter NiriLayoutEngineTests
Add GPL-2.0-only SPDX license identifiers to repository source files, matching the existing LICENSE file.

Tests:
- git diff --check
- swift package dump-package
Add a focused-removal animation policy that keeps the Niri viewport static while focus recovery selects the surviving window.

Suppress close, survivor-move, column, and scroll handoff animations for that path so final frames apply immediately instead of waiting for a display-link tick.

Thread removal diagnostics through intake, topology planning, animation directives, frame application, and scroll ticks, and extend AX/layout/Niri regression coverage for static recovery and disabled animations.
Track same-app focus preemptions and suppress transient same-PID activations while focused-removal recovery is tied to the active refresh cycle.

Preserve removed-window and animation-policy metadata through orchestration so the Niri topology kernel can apply strict-left, viewport-preserving recovery deterministically.

Add Swift and Zig regression coverage for static focused removals, coalesced creates, and orchestration ABI payload preservation.
…ission

fix(ax): admit windows on activation when CGS create event is missing
@rilez
Copy link
Copy Markdown

rilez commented Apr 24, 2026

Would love to contribute upstream! Been slammed at work + having to rely a lot on AI for this, as Swift is wayyy out of my wheelhouse. Hoping to take another stab at it this weekend. My approach is to replace the notion of 'axes' with a view where users arrange their physical monitor layout, as my setup's a pretty tricky edge case.
image

@BarutSRB
Copy link
Copy Markdown
Owner

@rilez Oh yeah, that will be extremely difficult to do given your setup and how stupidly macOS WindowServer behaves, it doesn't let you completely hide windows, they have to show at least a sliver of its window, and given that OmniWM "hides" to the sides the windows they will bleed or get taken into the other monitor if the monitor is set in macOS system settings to be side by side, I was thinking to experiment with a fake virtual monitor and use it as a hiding monitor to place hidden windows there and given my research it should work but it is a heavy solution and I am busy doing other stuff for OmniWM currently but that is probably your best bet for your monitor setup.

@adelin-b
Copy link
Copy Markdown
Contributor Author

@rilez My branch handle this kind of setups.

It shows you two screen layout :

  • One with the desired screen layout target
  • One with how you should position your screen to avoid windows on the side to be replaced.

@rilez
Copy link
Copy Markdown

rilez commented Apr 26, 2026

Sorry I wasn't clear: the idea was to still arrange monitors in macOS settings according to your docs/guidelines, and replace OmniWM's notion of 'axes' with a similar canvas that defines the actual physical layout. I had a quick and dirty implementation of this more or less working, but probably better for me to @adelin-b work and see, thanks!

@adelin-b
Copy link
Copy Markdown
Contributor Author

Yeah, So what I do is that I warp the mouse correcly between desktops.
Target (Will handle the mouse warping to respect the target the layout)
[ 1 ]
[ 4 ][ 2 ][ 3 ]

Macos will require this:
[ 1 ]
[ 2 ]
[ 3 ]
[ 4 ]

Add bidirectional mouse warping for multi-row monitor setups (e.g.,
ultrawide above a row of monitors). Includes a visual grid editor in
Settings for configuring the virtual monitor layout, plus a one-click
staircase auto-arrange with restore-previous support.

Mouse Warp:
- Add `case both` to MouseWarpAxis for simultaneous H+V warping
- Add MouseWarpGridEntry for virtual monitor positions
- Grid-based warp uses geometric adjacency on virtual frames,
  independent of macOS staircase display arrangement
- Virtual coordinate transfer ratios for correct cursor positioning
  across monitors of different sizes
- Cursor disambiguation: picks correct target when multiple monitors
  share a vertical edge based on cursor X position

Settings UI:
- Visual grid editor with draggable monitor tiles (shown when axis=Both)
- Snap-to-edge with visual guide lines (edges, centers, both axes)
- macOS Display Arrangement adjacency lint against real NSScreen frames
- One-click staircase auto-arrange via CGConfigureDisplayOrigin
  (.forSession) with previous-state save/restore
- Y-axis flip so layout matches physical arrangement (top=above)
- Selected Monitor section moved above warp controls

Rebased onto upstream/main: integrates upstream's mouse warp
improvements (focus-follows-mouse synthetic event after warp,
lock-screen suppression, orthogonal coord preservation across seams,
nearest-monitor selection) with the new "both" axis logic.
Workspace bar redesign dropped — superseded by upstream PR BarutSRB#227.
The Auto-arrange Staircase button now opens a confirmation dialog that
explains what will happen, where the previous arrangement is saved, and
that the change is session-only until the user confirms it in System
Settings. Restores still happen via "Restore Previous" without prompting.

Also surfaces a failure alert when CGCompleteDisplayConfiguration fails
instead of silently doing nothing.
@adelin-b adelin-b force-pushed the feat/mouse-warp-both-axis branch from e09f4d2 to f1baefd Compare April 26, 2026 22:26
@BarutSRB
Copy link
Copy Markdown
Owner

Let me know when it's ready for a review I am pushing a huge refactoring today that should consolidate state and make it much clearer to understand the codebase, I have now better understanding of the problem. Thank you both ❤️❤️❤️

@BarutSRB
Copy link
Copy Markdown
Owner

BarutSRB commented May 3, 2026

@rilez @adelin-b check the current head I removed let's call it "anti-mouse warp" as I added that a long time ago to make myself not accidentally move the cursor in the direction of where the monitor was assigned in macOS settings without thinking of other ways users might want their monitor setups, should at least for rilez setup make it easier and would make your PR easier.

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.

6 participants