Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 44 additions & 5 deletions MonitorControl/Model/AppleDisplay.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,17 @@ class AppleDisplay: Display {

override var brightnessMaxValue: Float { self.effectiveBrightnessMax }

/// The built-in panel never raises macOS's native brightness OSD through `OSDManager`:
/// see the override of `showsBrightnessOSD` on `Display`. The menu-bar sun going yellow
/// when a boost is active, the XDR opt-in dialog at 100 %, and the live slider in the
/// menu are the visible feedback instead.
override var showsBrightnessOSD: Bool { false }
/// The built-in panel raises macOS's native brightness OSD through `OSDManager` only on
/// systems where that overlay still behaves. On macOS 27 it leaves the OSD permanently
/// drawn: `showImage:…:msecUntilFade:` neither honours the fade timer nor accepts a
/// later update that should replace it, so once raised it stays up while the app is
/// running. Everywhere else the OSD is shown, so pressing the brightness keys gives the
/// same visual feedback macOS itself would; the menu-bar sun going yellow when a boost
/// is active, the XDR opt-in dialog at 100 %, and the live slider in the menu are the
/// visible feedback instead where it is not.
override var showsBrightnessOSD: Bool {
ProcessInfo.processInfo.operatingSystemVersion.majorVersion < 27
}

/// True when the panel can do extended brightness but the user has not switched it on.
var canOfferXDR: Bool {
Expand Down Expand Up @@ -189,6 +195,39 @@ class AppleDisplay: Display {
self.applyBrightnessToPanel(self.getBrightness())
}

/// Brings the app's record of brightness back in line with the panel after a wake that
/// killed the XDR boost.
///
/// Sleep tears the boost down, and the resume a few seconds after waking can still
/// fail: the EDR window cannot be re-created, or macOS has withdrawn the extended
/// range. The stored preference then keeps claiming the panel is at, say, 150% while it
/// is really at the SDR maximum, and the slider follows the preference. Rather than
/// leave that lie in place — with the extended range still enabled and one drag away —
/// fall back to the standard range at whatever brightness the panel is actually
/// showing. XDR stays enabled, so pushing past 100% starts the boost again.
func reconcileXDRStateAfterWake() {
guard self.isXDREnabled else {
return
}
guard self.getBrightness() > 1.005, !self.isXDRBoosting else {
return
}
var actual = self.getAppleBrightness()
if !(0.001 ... 1.0).contains(actual) {
// A failed read leaves 0 behind, and while the boost ran the SDR side was pinned at
// the maximum, so fall back to that rather than trusting a dark-screen reading.
actual = 1.0
}
os_log("XDR boost did not survive the wake on display %{public}@; snapping brightness back to %{public}@.", type: .info, String(self.identifier), String(actual))
_ = self.setBrightness(actual)
if let sliderHandler = self.sliderHandler[.brightness] {
sliderHandler.setValue(actual, displayID: self.identifier)
}
// No menu rebuild is needed here, unlike on a disable: XDR stays enabled, so the menu
// items and the slider's extended range are unchanged, the live slider was just updated
// directly, and the menu-bar icon follows the engine's own boost notifications.
}

override func stepBrightness(isUp: Bool, isSmallIncrement: Bool) {
super.stepBrightness(isUp: isUp, isSmallIncrement: isSmallIncrement)
// Only act when the user is pushing up against the ceiling: that is when the XDR
Expand Down
12 changes: 6 additions & 6 deletions MonitorControl/Model/Display.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ class Display: Equatable {
/// Whether `stepBrightness` drives the native macOS brightness OSD for this display.
///
/// Defaults to `true` so external (DDC/HDMI) displays, which have no other feedback,
/// keep showing the macOS chiclet HUD. Apple displays override this to `false` because
/// on macOS 27 the private `OSDManager` we route through leaves the OSD permanently
/// drawn: `showImage:…:msecUntilFade:` neither honours the fade timer nor accepts a
/// later update that should replace it, so once raised it stays up while the app is
/// running. The XDR opt-in dialog and the menu-bar sun are the visible feedback on
/// the built-in panel instead.
/// keep showing the macOS chiclet HUD. Apple displays override this to report `false`
/// on macOS 27 only, where the private `OSDManager` we route through leaves the OSD
/// permanently drawn: `showImage:…:msecUntilFade:` neither honours the fade timer nor
/// accepts a later update that should replace it, so once raised it stays up while the
/// app is running. There the XDR opt-in dialog and the menu-bar sun are the visible
/// feedback on the built-in panel instead.
var showsBrightnessOSD: Bool { true }

func prefExists(key: PrefKey? = nil, for command: Command? = nil) -> Bool {
Expand Down
39 changes: 34 additions & 5 deletions MonitorControl/Support/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -289,18 +289,35 @@ class AppDelegate: NSObject, NSApplicationDelegate {
if self.reconfigureID != 0 {
let dispatchedReconfigureID = self.reconfigureID
os_log("Displays need reconfig after sober with reconfigureID %{public}@", type: .info, String(dispatchedReconfigureID))
// `configure()` resumes the XDR boost itself, before it restarts the refresh loop.
self.configure(dispatchedReconfigureID: dispatchedReconfigureID)
} else if Arm64DDC.isArm64 {
} else {
os_log("Displays don't need reconfig after sober but might need AVServices update", type: .info)
DisplayManager.shared.updateArm64AVServices()
// The boost was dropped on the way to sleep; a plain sleep/wake never reaches
// `configure()`. Resume before restarting the refresh loop: the loop's first pass
// would otherwise read the un-boosted panel and drag the stored brightness back
// down to the SDR maximum before the boost has been re-applied, silently
// cancelling the XDR setting. The loop itself must also come back on every
// architecture — it is what keeps sliders and stored values in step with the
// panels — and `updateArm64AVServices()` is a no-op where it does not apply.
self.resumeXDRForAllDisplays()
self.job(start: true)
}
self.startupActionWriteRepeatAfterSober()
self.updateMediaKeyTap()
// The boost was dropped on the way to sleep. A wake that also reconfigured the
// displays picks it up through `configure()`, but a plain sleep/wake never gets there,
// so the panel would be left at its SDR maximum with the slider still reading 150%.
self.resumeXDRForAllDisplays()
// The resume above can still fail — the EDR window cannot be re-created, or macOS
// has withdrawn the extended range — while the stored preference keeps claiming the
// panel sits above 100%. Ten seconds in (the resume ran three seconds after waking
// and the boost ramps in within a couple more), check whether the boost is actually
// running and, where it is not, snap the app's state back to what the panel really
// shows instead of leaving the slider reading a value the panel is not delivering.
DispatchQueue.main.asyncAfter(deadline: .now() + 10.0) { [weak self] in
guard let self = self, self.sleepID == 0, self.reconfigureID == 0 else {
return
}
self.reconcileXDRStateForAllDisplays()
}
Comment on lines +315 to +320

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Stale wake callback reconciles

If another sleep/wake cycle completes during this ten-second delay, both lifecycle IDs can return to zero, so the callback from the earlier wake passes this guard while the newer XDR resume is still settling. Because the boost may not yet be reported as active, reconciliation can replace the valid stored extended brightness with the panel's SDR value. Tie the callback to the wake cycle that scheduled it or cancel it when another sleep begins.

Prompt To Fix With AI
This is a comment left during a code review.
Path: MonitorControl/Support/AppDelegate.swift
Line: 315-320

Comment:
**Stale wake callback reconciles**

If another sleep/wake cycle completes during this ten-second delay, both lifecycle IDs can return to zero, so the callback from the earlier wake passes this guard while the newer XDR resume is still settling. Because the boost may not yet be reported as active, reconciliation can replace the valid stored extended brightness with the panel's SDR value. Tie the callback to the wake cycle that scheduled it or cancel it when another sleep begins.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex

}
}

Expand All @@ -314,6 +331,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
}

/// Brings every display's XDR state back in line with what its panel is really showing.
private func reconcileXDRStateForAllDisplays() {
for display in DisplayManager.shared.displays {
(display as? AppleDisplay)?.reconcileXDRStateAfterWake()
}
}

private func startupActionWriteRepeatAfterSober(dispatchedCounter: Int = 0) {
let counter = dispatchedCounter == 0 ? 10 : dispatchedCounter
self.startupActionWriteCounter = dispatchedCounter == 0 ? counter : self.startupActionWriteCounter
Expand Down Expand Up @@ -357,6 +381,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
}
let nextRefresh = refreshedSomething ? 0.1 : 1.0
// The brightness keys are handed back to macOS while the built-in panel sits below
// the top of the standard range. This is what takes them back once the panel is at
// 100% again — including when macOS itself moved it there with the native keys.
// Cheap by design: the tap is only rebuilt when the verdict actually changes.
self.mediaKeyTap.refreshBrightnessKeyEngagement()
DispatchQueue.main.asyncAfter(deadline: .now() + nextRefresh) {
self.job()
}
Expand Down
75 changes: 63 additions & 12 deletions MonitorControl/Support/MediaKeyTapManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ class MediaKeyTapManager: MediaKeyTapDelegate {
var keyRepeatTimers: [MediaKey: Timer] = [:]
/// Guards the "Accessibility is missing" alert so it is shown at most once per session.
private var didReportMissingAccessibility = false
/// The last verdict on holding the brightness keys back from macOS for the XDR range.
/// Kept in step by `updateMediaKeyTap()` so `refreshBrightnessKeyEngagement()` can tell
/// whether the verdict has actually changed.
private var holdingBrightnessKeysForXDR = false

func handle(mediaKey: MediaKey, event: KeyEvent?, modifiers: NSEvent.ModifierFlags?) {
let isPressed = event?.keyPressed ?? true
Expand Down Expand Up @@ -163,18 +167,17 @@ class MediaKeyTapManager: MediaKeyTapDelegate {
// Disengage brightness keys on sleep so MacBook native screen can be controlled meanwhile
let isTransient = app.sleepID != 0 || app.reconfigureID != 0
let disengageBrightness = !hasExternalDisplay || isTransient
if disengageBrightness, !prefs.bool(forKey: PrefKey.useFineScaleBrightness.rawValue) {
// Keep them anyway when the built-in panel can go past 100%. With no external display
// attached macOS consumes the brightness keys itself, so `stepBrightness` never runs
// and the XDR opt-in is unreachable except by opening the menu and dragging the
// slider to 100% — which defeats the point of having the keys. Only while awake and
// settled: during sleep and display reconfiguration the panel belongs to macOS.
let keepForXDR = !hasExternalDisplay && !isTransient
&& DisplayManager.shared.displays.contains { ($0 as? AppleDisplay)?.isXDRCapable == true }
if !keepForXDR {
let keysToDelete: [MediaKey] = [.brightnessUp, .brightnessDown]
keys.removeAll { keysToDelete.contains($0) }
}
// While the built-in XDR panel is at the top of the standard range, or boosting, the
// brightness keys must be kept from macOS: the next press up crosses into extended
// brightness — or offers to — which macOS will never do on its own. Below that the
// keys are handed back, and with them macOS's own brightness overlay and slider
// feedback, which this app cannot reproduce (the private OSD manager leaves the
// overlay permanently drawn on macOS 27).
let holdBrightnessKeysForXDR = MediaKeyTapManager.shouldHoldBrightnessKeysForXDR(displays: DisplayManager.shared.displays, hasExternalDisplay: hasExternalDisplay, isTransient: isTransient)
self.holdingBrightnessKeysForXDR = holdBrightnessKeysForXDR
if disengageBrightness, !prefs.bool(forKey: PrefKey.useFineScaleBrightness.rawValue), !holdBrightnessKeysForXDR {
let keysToDelete: [MediaKey] = [.brightnessUp, .brightnessDown]
keys.removeAll { keysToDelete.contains($0) }
}
// Remove volume related keys if audio device is controllable
if let defaultAudioDevice = app.coreAudio.defaultOutputDevice {
Expand Down Expand Up @@ -208,6 +211,54 @@ class MediaKeyTapManager: MediaKeyTapDelegate {
}
}

/// Whether the brightness keys must be held back from macOS even though it could handle
/// them itself.
///
/// With no external display attached, macOS drives the built-in panel's brightness
/// perfectly well and shows its own brightness overlay — feedback this app cannot
/// reproduce, since the private OSD manager leaves that overlay permanently drawn on
/// macOS 27. So while the panel sits below the top of the standard range the keys are
/// given back to the system and the native brightness UI returns.
///
/// They are taken back only while an XDR-capable panel is at 100% — where the next press
/// up has to cross into extended brightness, or offer to, and macOS has nothing to
/// offer — or while a boost is actually running, so the extended range can also be
/// stepped back down from the keys. Dummy displays are excluded: they exist to stand in
/// for hardware and never carry a boost.
static func shouldHoldBrightnessKeysForXDR(displays: [Display], hasExternalDisplay: Bool, isTransient: Bool) -> Bool {
guard !hasExternalDisplay, !isTransient else {
return false
}
return displays.contains { display in
guard let appleDisplay = display as? AppleDisplay, appleDisplay.isXDRCapable, !appleDisplay.isDummy else {
return false
}
return appleDisplay.isXDRBoosting || appleDisplay.getAppleBrightness() >= 0.999
}
}

/// Re-checks, from the one-second refresh loop, whether the brightness keys must be
/// held for the XDR range.
///
/// The verdict changes as the built-in panel crosses the top of the standard range —
/// including when macOS moves it there with the native keys while the app is not
/// listening — so this is what takes the keys back. Rebuilding the event tap churns the
/// session, so it only happens when the verdict actually changed.
func refreshBrightnessKeyEngagement() {
var hasExternalDisplay = false
for display in DisplayManager.shared.getAllDisplays() where !display.isBuiltIn() {
hasExternalDisplay = true
}
let hold = MediaKeyTapManager.shouldHoldBrightnessKeysForXDR(
displays: DisplayManager.shared.displays,
hasExternalDisplay: hasExternalDisplay,
isTransient: app.sleepID != 0 || app.reconfigureID != 0
)
if hold != self.holdingBrightnessKeysForXDR {
self.updateMediaKeyTap()
}
}

/// The name of the row that holds the Accessibility app list, which Apple renamed.
///
/// On macOS 27 the row — and the window it opens — is called "Device Control and Data
Expand Down
Loading
Loading