feat: add "both" mouse warp axis for multi-row monitor layouts#231
feat: add "both" mouse warp axis for multi-row monitor layouts#231adelin-b wants to merge 81 commits into
Conversation
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.
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 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. |
|
@rilez My branch handle this kind of setups. It shows you two screen layout :
|
|
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! |
|
Yeah, So what I do is that I warp the mouse correcly between desktops. Macos will require this: |
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.
e09f4d2 to
f1baefd
Compare
|
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 ❤️❤️❤️ |
|
@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. |

Summary
bothcase toMouseWarpAxisenabling simultaneous horizontal and vertical mouse warpingProblem
With the current
horizontal/verticalaxis options, users with multi-row monitor layouts like:...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 tohorizontalsince the enum didn't have that case.Solution
MouseWarpAxis.swift:case bothwith display metadata (name, symbols, sort order)bothuses horizontal-primary sorting for the monitor order listMouseWarpHandler.swift: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 forbothmode.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 escapingmouseWarpClampCursorToNearestMonitor: usesmonitorApproximationforbothmouseWarpDestinationPoint: handles all edge/direction combinations forboth.bothaxisHow it works
Unlike
horizontal/verticalwhich use the orderedmouseWarpMonitorOrderlist,bothuses 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
mouseWarpAxis: "both"in settings.jsonhorizontalandverticalmodes are unaffectedRelated
verticalaxis)Summary by CodeRabbit