Add software colour temperature control - #24
Conversation
ColorTemperatureCurve (TopologyCore, pure, 6 tests): kelvin (2700-9300) to per-channel gamma gains via the Tanner Helland blackbody fit, expressed relative to 6500K so neutral is exactly identity, gains only attenuate (dominant channel pinned at 1 — no highlight clipping). The dim scale and warmth gains share one CGSetDisplayTransferByFormula slot per display, so CoreGraphicsProvider gains setGammaAdjustment (dim x per-channel gains, one write) and every gamma-touching AppModel path now funnels through writeGamma, which injects the display's gains: dimming, software-brightness fallback, dim-lift on hardware-brightness return, and profile reapplication all preserve an active warmth. Black Out intentionally stays neutral. Appearance card gains a warm/cool slider with a 100K snap-to-native band. Session-only like the dim; restoreGamma on quit clears both. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 418b9a4d17
ℹ️ 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".
| colorTemperature[id] = abs(kelvin - ColorTemperatureCurve.neutralKelvin) < 100 ? nil : kelvin | ||
| guard !blackedOut.contains(id) else { return } // applied when the blackout lifts | ||
| let split = DimmingComposer.split(method: settings.dimmingMethod, level: softwareDim[id] ?? 1) | ||
| writeGamma(level: split.gammaLevel, id: id, cgID: cgID) |
There was a problem hiding this comment.
Restore ColorSync when returning the slider to Native
When a user drags the temperature back into the neutral band with no software dim active, this clears the cache but immediately calls setGammaAdjustment(dim: 1, red: 1, green: 1, blue: 1). CGSetDisplayTransferByFormula installs an identity formula rather than restoring the display's ColorSync calibration—the separate CGDisplayRestoreColorSyncSettings API is what restores those tables—so the UI reports “Native” while the ICC calibration remains replaced until the app quits.
Useful? React with 👍 / 👎.
| guard let cgID = observation.cgDisplayID, !blackedOut.contains(observation.recordID), | ||
| let dim = softwareDim[observation.recordID], dim < 1.0 else { return } | ||
| applyDim(level: dim, cgID: cgID) | ||
| applyDim(level: dim, id: observation.recordID, cgID: cgID) |
There was a problem hiding this comment.
Reapply temperature after an ICC profile change
If a non-neutral temperature is active without a software dim (the normal case for a hardware-controlled display), changing or resetting its ICC profile wipes the formula gamma adjustment, but this helper returns unless softwareDim[id] < 1. Both profile-writing paths call this helper, leaving colorTemperature and the slider non-neutral while the display is actually native; reapply the combined gamma whenever either dimming or temperature is active.
Useful? React with 👍 / 👎.
| if !colorTemperature.keys.allSatisfy(ids.contains) { | ||
| colorTemperature = colorTemperature.filter { ids.contains($0.key) } |
There was a problem hiding this comment.
Reapply cached temperature after display reconfiguration
For a still-connected display, this preserves colorTemperature across refresh(), but observeTopologyChanges() only refreshes state after resolution, rotation, mirroring, sleep, and similar reconfigurations. Core Graphics resets formula gamma during those events (as the nearby blackout documentation also notes), so the display loses its temperature while the retained cache keeps the slider showing it as active; the cached combined gamma needs to be reasserted after the reconfiguration completes.
Useful? React with 👍 / 👎.
Parity-map §3: warm/cool control via the existing gamma path.
setGammaAdjustmentwrite; every path that used to callsetGammaDimdirectly (software brightness, dim lift, profile reapply) preserves an active warmth instead of silently clearing it.321 package tests green; both app schemes build.