Skip to content

Add software colour temperature control - #24

Merged
aquitaine merged 1 commit into
mainfrom
color-temperature
Jul 19, 2026
Merged

Add software colour temperature control#24
aquitaine merged 1 commit into
mainfrom
color-temperature

Conversation

@aquitaine

@aquitaine aquitaine commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Parity-map §3: warm/cool control via the existing gamma path.

  • ColorTemperatureCurve (pure, 6 tests): Helland blackbody fit normalised to the 6500 K neutral — identity at neutral, attenuation-only gains, monotonic blue on the warm side.
  • One gamma funnel: dim × warmth now go through a single setGammaAdjustment write; every path that used to call setGammaDim directly (software brightness, dim lift, profile reapply) preserves an active warmth instead of silently clearing it.
  • Warm/cool slider in the Appearance card, snaps to "Native" near 6500 K.

321 package tests green; both app schemes build.

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>
@aquitaine
aquitaine merged commit 75c6f4a into main Jul 19, 2026
@aquitaine
aquitaine deleted the color-temperature branch July 19, 2026 23:16
@aquitaine aquitaine mentioned this pull request Jul 19, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +575 to +578
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)

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 Badge 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 👍 / 👎.

Comment on lines 1150 to +1152
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment on lines +715 to +716
if !colorTemperature.keys.allSatisfy(ids.contains) {
colorTemperature = colorTemperature.filter { ids.contains($0.key) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

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.

2 participants