Skip to content

fix(display): skip relaunch wave on resolution-only screen-parameters changes#569

Merged
stonerl merged 1 commit into
stonerl:developmentfrom
nightah:fix/display-spacing-bug
May 12, 2026
Merged

fix(display): skip relaunch wave on resolution-only screen-parameters changes#569
stonerl merged 1 commit into
stonerl:developmentfrom
nightah:fix/display-spacing-bug

Conversation

@nightah
Copy link
Copy Markdown
Collaborator

@nightah nightah commented May 12, 2026

What does this PR do?

Stops DisplaySettingsManager.applyActiveDisplaySpacing from firing on NSApplication.didChangeScreenParametersNotification events that only reflect resolution or other-parameter changes on the same display, so a resolution flip no longer risks triggering a full menu-bar-app relaunch wave.

PR Type

  • Bugfix
  • CI/CD related changes
  • Code style update (formatting, renaming)
  • Documentation
  • Feature
  • Refactor
  • Performance improvement
  • Test addition or update
  • Other (please describe):

Does this PR introduce a breaking change?

  • Yes
  • No

If yes, please describe the impact and migration path:

N/A

What is the current behavior?

PR #529 (commit 1ed2ce67) wired the spacing apply to didChangeScreenParametersNotification. That notification fires on every screen-parameter event, including resolution changes, lid open/close, and GPU/sleep transitions on an already-connected display. Each fire armed a preflight settling window and ran applyOffset. When the on-disk NSStatusItem byHost values disagreed with the target (Control Center restart, sleep/wake clobber, mid-flight oscillation across profiles) the call escalated to a full relaunch wave even though the user had only changed a setting of the display, not changed to a different one.

Issue Number: #551

What is the new behavior?

DisplaySettingsManager now caches the active menu bar display UUID at every applyActiveDisplaySpacing call and the didChangeScreenParametersNotification sink consults a pure static predicate shouldSkipSpacingApply(currentActiveDisplayUUID:lastAppliedActiveDisplayUUID:) before falling through. When the UUIDs match (resolution change, lid event, or GPU transition on the same display) the sink logs "Active menu bar display unchanged" and returns. When they differ (laptop docked, active menu bar moved to another monitor, first event after launch) the existing apply path runs as before. The $configurations sink and external Settings-URI path are unchanged because those reflect deliberate user intent and already rely on the no-op guard inside MenuBarItemSpacingManager.applyOffset to short-circuit when target equals on-disk.

PR Checklist

  • I’ve built and run the app locally
  • I’ve checked for console errors or crashes
  • I've run relevant tests and they pass
  • I've added or updated tests (if applicable)
  • I've updated documentation as needed

Other information

Adds ThawTests/DisplaySettingsManagerSpacingGateTests with eight cases pinning the predicate (matching, differing, either-nil, both-nil, repeated-stable) and the field semantics on a fresh DisplaySettingsManager instance. The file is picked up automatically by the ThawTests synchronized-folder reference, so Thaw.xcodeproj/project.pbxproj requires no edits.

Notes for reviewers

The predicate is intentionally a static pure function so it can be unit-tested without AppState, Bridging, or live NSScreen state. End-to-end driving of the notification sink would depend on the host machine's display topology and the 1 s .debounce, so the manual verification steps below cover that surface instead:

  1. Change the active display's resolution in System Settings: log shows "Active menu bar display unchanged"; no relaunch wave.
  2. Move the active menu bar to a different physical display: apply runs as before.
  3. Drag the per-display spacing slider in Settings > Displays: configurations sink fires and re-applies.
  4. Drive a per-display setting via the Settings URI scheme: external path runs as before.
  5. Unplug/replug the active display: Bridging.getActiveMenuBarDisplayUUID() returns the same UUID, so no redundant wave on reconnect.

Fixes #551

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved display spacing handling to prevent unnecessary recalculations when screen parameters change.
  • Tests

    • Added comprehensive test coverage for display spacing behavior.

Review Change Stack

… changes

PR stonerl#529 (commit `1ed2ce67`, "feat: per-display menu bar spacing with dynamic apply") wired `DisplaySettingsManager.applyActiveDisplaySpacing` to `NSApplication.didChangeScreenParametersNotification` so each display's saved spacing follows the active menu bar around. That notification fires for every screen-parameter event though, including resolution changes, lid open/close, and GPU/sleep transitions on a display that is already connected. Each fire armed a preflight settling window and ran `applyOffset`, and any time the on-disk byHost values disagreed with the target (Control Center restart, sleep/wake clobber, mid-flight oscillation across profiles) the call escalated to a full relaunch wave even though the user had only changed a setting of the display, not changed to a different display.

`DisplaySettingsManager` now records the active menu bar display UUID inside `applyActiveDisplaySpacing` and the `didChangeScreenParametersNotification` sink compares the current UUID against that cache via a new `shouldSkipSpacingApply(currentActiveDisplayUUID:lastAppliedActiveDisplayUUID:)` static predicate. When the UUIDs match (the resolution/lid/GPU case) the sink logs "Active menu bar display unchanged" and returns; when they differ (laptop docked, active menu bar moved to another monitor, first event after launch) the existing apply path runs as before. The `$configurations` and external Settings-URI paths are untouched: those reflect deliberate user intent and continue to rely on the no-op guard in `MenuBarItemSpacingManager.applyOffset` to short-circuit when target equals on-disk.

Adds `ThawTests/DisplaySettingsManagerSpacingGateTests` with eight cases pinning the predicate (matching, differing, either-nil, both-nil, repeated-stable) and the field semantics on a fresh `DisplaySettingsManager` instance. The new file is picked up automatically by the `ThawTests` synchronized-folder reference so no project edits are required.

Fixes stonerl#551

Signed-off-by: Amir Zarrinkafsh <3339418+nightah@users.noreply.github.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

📝 Walkthrough

Walkthrough

Added a display UUID tracking mechanism to DisplaySettingsManager that prevents menu bar spacing from being unnecessarily re-applied when screen configuration changes occur without changing the active display. The implementation consists of a stored property, a pure guard predicate, integration into the notification handler, test coverage for the predicate logic, and field-semantics validation.

Changes

Display Spacing Re-apply Gate

Layer / File(s) Summary
Spacing gate predicate and storage
Thaw/Settings/Models/DisplaySettingsManager.swift
Adds lastAppliedActiveDisplayUUID property to store the active display UUID at last spacing application, and introduces the pure shouldSkipSpacingApply(currentActiveDisplayUUID:lastAppliedActiveDisplayUUID:) predicate to test whether spacing re-apply should be skipped.
Screen parameters observation with spacing gate
Thaw/Settings/Models/DisplaySettingsManager.swift
Updates the didChangeScreenParametersNotification sink to fetch the current active display UUID and check the predicate before re-applying spacing; logs and returns early when the UUID matches the stored value.
Spacing application state update
Thaw/Settings/Models/DisplaySettingsManager.swift
Modifies applyActiveDisplaySpacing(reason:) to update lastAppliedActiveDisplayUUID to the current active display UUID, establishing the baseline for future guard checks.
Spacing gate predicate and field tests
ThawTests/DisplaySettingsManagerSpacingGateTests.swift
Tests the shouldSkipSpacingApply predicate across UUID equality, inequality, nil handling, and repeated-call stability; validates lastAppliedActiveDisplayUUID defaults to nil and correctly drives predicate behavior.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

A rabbit hops through screens so bright,
UUID checks keep spacing tight—
No needless reapply, just the right touch,
When displays stay true, we skip so much! 🐰✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Linked Issues check ❓ Inconclusive The PR addresses the immediate symptom from #551 (preventing unnecessary relaunch waves on display parameter changes) but the linked issue proposes a broader architectural change: moving itemSpacingOffset to General settings. The current fix only implements a temporary workaround via UUID caching, not the full proposed refactor. Clarify with the team whether this PR is intended as a partial fix addressing the relaunch wave symptom, or whether the full architectural change (moving spacing to General settings) remains a future objective for #551.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: preventing relaunch waves on resolution-only screen-parameter changes by skipping the spacing application when the display hasn't changed.
Description check ✅ Passed The description comprehensively covers all template sections including problem, solution, behavior changes, and verification steps. The checklist is properly filled and the explanation is detailed.
Out of Scope Changes check ✅ Passed All changes are in-scope: DisplaySettingsManager adds UUID caching and the shouldSkipSpacingApply predicate to address the relaunch wave issue, and new unit tests validate the predicate behavior. No unrelated refactoring or formatting changes detected.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Thaw/Settings/Models/DisplaySettingsManager.swift (1)

233-266: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Defer cache update until spacing apply succeeds.

Line 233 marks spacing as “applied” before applyOffset() completes. If applyOffset() fails, later same-display notifications will be skipped and retry is suppressed.

Suggested fix
 private func applyActiveDisplaySpacing(reason: String) {
     guard let appState else { return }
-    lastAppliedActiveDisplayUUID = Bridging.getActiveMenuBarDisplayUUID()
+    let activeUUIDAtApplyStart = Bridging.getActiveMenuBarDisplayUUID()
     let desired = Int(configurationForActiveDisplay().itemSpacingOffset.rounded())
     appState.spacingManager.offset = desired
     Task { [weak self] in
         guard let self else { return }
         appState.itemManager.startSettlingPeriod(reason: "spacingRelaunch:\(reason):preflight")
         do {
             let outcome = try await appState.spacingManager.applyOffset()
+            lastAppliedActiveDisplayUUID = activeUUIDAtApplyStart
             if outcome.didRelaunch {
                 appState.itemManager.startSettlingPeriod(
                     reason: "spacingRelaunch:\(reason)",
                     expectedBundleIDs: outcome.recoveredBundleIDs
                 )
                 appState.profileManager.reapplyActiveProfile()
             } else {
                 appState.itemManager.cancelSettlingPeriod(
                     reason: "spacingRelaunch:\(reason):noOp"
                 )
             }
         } catch {
             appState.itemManager.cancelSettlingPeriod(
                 reason: "spacingRelaunch:\(reason):error"
             )
             diagLog.error("applyActiveDisplaySpacing(\(reason)) failed: \(error)")
         }
     }
 }
🤖 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 `@Thaw/Settings/Models/DisplaySettingsManager.swift` around lines 233 - 266,
The code marks spacing as applied by setting lastAppliedActiveDisplayUUID before
awaiting appState.spacingManager.applyOffset(), which means if applyOffset()
throws the cache is updated incorrectly; remove the early assignment to
lastAppliedActiveDisplayUUID and instead set lastAppliedActiveDisplayUUID =
Bridging.getActiveMenuBarDisplayUUID() only after applyOffset() succeeds (inside
the do block, after handling outcome.didRelaunch/no-op but before leaving the
Task), so both successful branches update the cache and the catch block does
not; update references inside the Task that rely on lastAppliedActiveDisplayUUID
accordingly and keep the existing weak self guard and error handling intact.
🤖 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.

Outside diff comments:
In `@Thaw/Settings/Models/DisplaySettingsManager.swift`:
- Around line 233-266: The code marks spacing as applied by setting
lastAppliedActiveDisplayUUID before awaiting
appState.spacingManager.applyOffset(), which means if applyOffset() throws the
cache is updated incorrectly; remove the early assignment to
lastAppliedActiveDisplayUUID and instead set lastAppliedActiveDisplayUUID =
Bridging.getActiveMenuBarDisplayUUID() only after applyOffset() succeeds (inside
the do block, after handling outcome.didRelaunch/no-op but before leaving the
Task), so both successful branches update the cache and the catch block does
not; update references inside the Task that rely on lastAppliedActiveDisplayUUID
accordingly and keep the existing weak self guard and error handling intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 42ad854e-0485-42e5-92c5-4dbc3c46c3df

📥 Commits

Reviewing files that changed from the base of the PR and between 81fa976 and 2c650f3.

📒 Files selected for processing (2)
  • Thaw/Settings/Models/DisplaySettingsManager.swift
  • ThawTests/DisplaySettingsManagerSpacingGateTests.swift

Copy link
Copy Markdown
Owner

@stonerl stonerl left a comment

Choose a reason for hiding this comment

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

LGTM

@stonerl stonerl merged commit 6db1da3 into stonerl:development May 12, 2026
5 checks passed
@nightah nightah deleted the fix/display-spacing-bug branch May 13, 2026 00:41
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.

Menu bar item spacing is modeled as per-display despite being global

2 participants