Skip to content

Instantly share code, notes, and snippets.

@retroplasma
Created July 13, 2026 01:57
Show Gist options
  • Select an option

  • Save retroplasma/f5cb10977246338fbb2deb650f895e3d to your computer and use it in GitHub Desktop.

Select an option

Save retroplasma/f5cb10977246338fbb2deb650f895e3d to your computer and use it in GitHub Desktop.
Pseudo True Tone (e.g. for Macbook Neo)

Pseudo True Tone: Webcam + Night Shift

It roughly does what real True Tone does, but without the sensor. It uses the webcam to estimate the warmth of the environment and sets Night Shift settings accordingly.

It's for devices without True Tone, like the Macbook Neo.


It uses the camera briefly every 2 minutes (interval is adjustable). That is, unless your Mac is asleep or the screen brightness is at the lowest setting (lights off).

This project has a similar tradeoff like Unlag Neo. There is an indicator (image) at the top right of the screen.

  • Similar to Unlag Neo, there is a feature in Pseudo True Tone to turn off the capturing when apps are in full screen mode (useful for video playback).

  • You can also permanently pause the periodic capturing (in the menu) and press CMD+R in the menu when you want it to adjust Night Shift; so it can capture the environmental warmth when you need it, without periodic capture. Or you can just directly adjust the Night Shift slider from there as well.

  • This is a very userland thing. If you want to hide the indicator for this app then I think that's a deep dive into SIP-off-mode and maybe beyond. Looks like the cam indicator is even more hardcore than the screen-capture indicator. But I haven't looked into it really.

  • There is no feature right now that mirrors the time-based Night Shift schedule settings (to automatically turn it on in the evening). There is only a warmth offset slider. I also haven't tested if time-based Night Shift settings override/collide with this app or something like this.

Let's go:

There is a script (create_pseudo_true_tone_app.sh) below in this Gist. The script creates an .app which can be launched (no need for Dev Account or Xcode etc.).

The script can be used like this (Terminal):

chmod +x create_pseudo_true_tone_app.sh
./create_pseudo_true_tone_app.sh
# -> creates "Pseudo True Tone.app"

App that the script created:

image

Put Pseudo True Tone.app somewhere. In /Applications or something. Run it and give it the permission it wants.


Menu (using image menu item):

image

You can adjust a bunch of things here if you want.

There are many conceivable ways to improve it; consider this an experiment.


[[Click here for Debug info]]

Debug

If you want to hack create_pseudo_true_tone_app.sh and rebuild the app multiple times, macOS permission settings can require a manual reset: Unless you change the app name, you might need to manually remove the accessibility permission from the system settings and then re-enable it again when the app asks for it next time. If for some reason you also need to reset "~/Library/Preferences/Pseudo True Tone.plist" then it might help to run killall cfprefsd after deleting that file.

#!/bin/bash
set -Efeuxo pipefail
APP_NAME="Pseudo True Tone"
VERSION="0.1.12"
OUTPUT_DIR="${PWD}"
FORCE=0
usage() {
cat <<'USAGE'
Usage: ./create_pseudo_true_tone_app.sh [--force] [--output DIR]
Builds "Pseudo True Tone.app" using the Swift compiler included with macOS.
No Xcode project, package manager, administrator access, or network access is used.
Options:
--force Replace an existing app at the output path.
--output DIR Put the generated app in DIR (default: current directory).
-h, --help Show this help.
USAGE
}
while (($#)); do
case "$1" in
--force)
FORCE=1
shift
;;
--output)
[[ $# -ge 2 ]] || { echo "Error: --output needs a directory" >&2; exit 2; }
OUTPUT_DIR="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Error: unknown argument: $1" >&2
usage >&2
exit 2
;;
esac
done
if [[ "$(uname -s)" != "Darwin" ]]; then
echo "Error: this builder must be run on macOS." >&2
exit 1
fi
for tool in xcrun plutil sips iconutil; do
command -v "$tool" >/dev/null 2>&1 || {
echo "Error: missing '$tool'. Install Apple's Command Line Tools with: xcode-select --install" >&2
exit 1
}
done
mkdir -p "$OUTPUT_DIR"
OUTPUT_DIR="$(cd "$OUTPUT_DIR" && pwd)"
APP_PATH="${OUTPUT_DIR}/${APP_NAME}.app"
if [[ -e "$APP_PATH" && "$FORCE" -ne 1 ]]; then
echo "Error: '$APP_PATH' already exists. Use --force to replace it." >&2
exit 1
fi
BUILD_DIR="$(mktemp -d "${TMPDIR:-/tmp}/pseudo-true-tone.XXXXXX")"
STAGED_APP="$BUILD_DIR/${APP_NAME}.app"
set +x
FINAL_MSG="Error: build did not complete."
set -x
cleanup() {
local status=$?
set +x
trap - ERR INT TERM HUP
set -x
local pid
for pid in $(jobs -pr); do
kill "$pid" 2>/dev/null || true
done
wait 2>/dev/null || true
rm -rf -- "$BUILD_DIR"
echo "Deleted temp dir: $BUILD_DIR"
if [[ "$status" -eq 0 ]]; then
echo "$FINAL_MSG"
else
echo "$FINAL_MSG" >&2
fi
exit "$status"
}
set +x
trap 'status=$?; FINAL_MSG="Build failed near line ${LINENO}: ${BASH_COMMAND}"; exit "$status"' ERR
trap cleanup EXIT
trap 'FINAL_MSG="Build cancelled."; exit 130' INT TERM HUP
set -x
mkdir -p "$STAGED_APP/Contents/MacOS" "$STAGED_APP/Contents/Resources"
cat > "$BUILD_DIR/PTTBridge.h" <<'HEADER'
#ifndef PTTBridge_h
#define PTTBridge_h
#include <stdbool.h>
#include <stdint.h>
typedef struct {
bool available;
bool active;
bool enabled;
int32_t mode;
float strength;
float cct;
float minCCT;
float maxCCT;
float midCCT;
} PTTNightShiftState;
bool PTTNightShiftGetState(PTTNightShiftState *outState);
bool PTTNightShiftApplyCCT(float kelvin, float transitionSeconds, float *outStrength);
bool PTTNightShiftApplyStrength(float strength, float transitionSeconds);
bool PTTNightShiftRestore(PTTNightShiftState state);
bool PTTBuiltInDisplayGetBrightness(float *outBrightness);
bool PTTBuiltInDisplayIsAsleep(bool *outAsleep);
#ifdef __OBJC__
#import <AVFoundation/AVFoundation.h>
// Read-only runtime probe. Returns false when macOS doesn't expose the
// iOS-shaped AWB selectors on the concrete camera implementation.
bool PTTCameraReadWhiteBalance(AVCaptureDevice *device, float *outKelvin, float *outTint);
#endif
#endif
HEADER
cat > "$BUILD_DIR/PTTBridge.m" <<'OBJC'
#import "PTTBridge.h"
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
#import <CoreGraphics/CoreGraphics.h>
#import <dlfcn.h>
#import <math.h>
#import <string.h>
typedef struct {
int hour;
int minute;
} PTTTime;
typedef struct {
PTTTime fromTime;
PTTTime toTime;
} PTTSchedule;
typedef struct {
signed char active;
signed char enabled;
signed char sunSchedulePermitted;
int mode;
PTTSchedule schedule;
unsigned long long disableFlags;
} PTTBlueLightStatus;
typedef struct {
float minCCT;
float maxCCT;
float midCCT;
} PTTCCTRange;
// Partial runtime declaration for Apple's private CoreBrightness class.
// The framework is loaded with dlopen so an OS change fails cleanly at runtime.
@interface CBBlueLightClient : NSObject
+ (BOOL)supportsBlueLightReduction;
- (BOOL)getBlueLightStatus:(PTTBlueLightStatus *)status;
- (BOOL)getStrength:(float *)strength;
- (BOOL)getCCT:(float *)cct;
- (BOOL)getCCTRange:(PTTCCTRange *)range;
- (BOOL)setEnabled:(BOOL)enabled;
- (BOOL)setActive:(BOOL)active;
- (BOOL)setMode:(int)mode;
- (BOOL)setStrength:(float)strength commit:(BOOL)commit;
- (BOOL)setStrength:(float)strength withPeriod:(float)period commit:(BOOL)commit;
- (BOOL)setCCT:(float)cct commit:(BOOL)commit;
- (BOOL)setCCT:(float)cct withPeriod:(float)period commit:(BOOL)commit;
@end
static CBBlueLightClient *PTTClient(void) {
static CBBlueLightClient *client = nil;
static dispatch_once_t once;
dispatch_once(&once, ^{
void *handle = dlopen(
"/System/Library/PrivateFrameworks/CoreBrightness.framework/CoreBrightness",
RTLD_LAZY | RTLD_LOCAL
);
if (handle != NULL) {
Class cls = NSClassFromString(@"CBBlueLightClient");
if (cls != Nil) {
if (![cls respondsToSelector:@selector(supportsBlueLightReduction)] ||
[cls supportsBlueLightReduction]) {
client = [[cls alloc] init];
}
}
}
});
return client;
}
static float PTTClamp(float value, float low, float high) {
return fminf(high, fmaxf(low, value));
}
bool PTTNightShiftGetState(PTTNightShiftState *outState) {
if (outState == NULL) { return false; }
memset(outState, 0, sizeof(*outState));
CBBlueLightClient *client = PTTClient();
if (client == nil ||
![client respondsToSelector:@selector(setEnabled:)] ||
![client respondsToSelector:@selector(getStrength:)]) {
return false;
}
outState->available = true;
outState->minCCT = 2850.0f;
outState->maxCCT = 6500.0f;
outState->midCCT = 4500.0f;
if ([client respondsToSelector:@selector(getBlueLightStatus:)]) {
PTTBlueLightStatus status = {0};
if ([client getBlueLightStatus:&status]) {
outState->active = status.active != 0;
outState->enabled = status.enabled != 0;
outState->mode = status.mode;
}
}
float strength = 0.0f;
if ([client getStrength:&strength]) {
outState->strength = strength;
}
if ([client respondsToSelector:@selector(getCCT:)]) {
float cct = 0.0f;
if ([client getCCT:&cct]) {
outState->cct = cct;
}
}
if ([client respondsToSelector:@selector(getCCTRange:)]) {
PTTCCTRange range = {0};
if ([client getCCTRange:&range] &&
range.minCCT >= 1000.0f &&
range.maxCCT > range.minCCT) {
outState->minCCT = range.minCCT;
outState->maxCCT = range.maxCCT;
outState->midCCT = range.midCCT;
}
}
return true;
}
bool PTTNightShiftApplyCCT(float kelvin, float transitionSeconds, float *outStrength) {
CBBlueLightClient *client = PTTClient();
if (client == nil) { return false; }
PTTNightShiftState state = {0};
if (!PTTNightShiftGetState(&state)) { return false; }
float target = PTTClamp(kelvin, state.minCCT, state.maxCCT);
float period = PTTClamp(transitionSeconds, 0.0f, 10.0f);
float neutralMired = 1000000.0f / state.maxCCT;
float warmMired = 1000000.0f / state.minCCT;
float targetMired = 1000000.0f / target;
float requestedStrength = PTTClamp(
(targetMired - neutralMired) / (warmMired - neutralMired),
0.0f,
1.0f
);
BOOL applied = NO;
// Strength is the path used by current macOS CoreBrightness utilities and
// has proven more stable than asking the private framework for a raw CCT.
if (period > 0.01f &&
[client respondsToSelector:@selector(setStrength:withPeriod:commit:)]) {
applied = [client setStrength:requestedStrength withPeriod:period commit:YES];
} else if ([client respondsToSelector:@selector(setStrength:commit:)]) {
applied = [client setStrength:requestedStrength commit:YES];
}
// Fall back to the direct CCT selectors if the strength path changes.
if (!applied) {
if (period > 0.01f &&
[client respondsToSelector:@selector(setCCT:withPeriod:commit:)]) {
applied = [client setCCT:target withPeriod:period commit:YES];
} else if ([client respondsToSelector:@selector(setCCT:commit:)]) {
applied = [client setCCT:target commit:YES];
}
}
// At the neutral endpoint, actually turn Night Shift off instead of
// leaving its system toggle active with a zero-strength transform.
BOOL enabled = [client setEnabled:requestedStrength > 0.005f ? YES : NO];
float strength = 0.0f;
if ([client respondsToSelector:@selector(getStrength:)]) {
[client getStrength:&strength];
}
if (outStrength != NULL) { *outStrength = strength; }
return applied && enabled;
}
bool PTTNightShiftApplyStrength(float strength, float transitionSeconds) {
CBBlueLightClient *client = PTTClient();
if (client == nil) { return false; }
float requested = PTTClamp(strength, 0.0f, 1.0f);
float period = PTTClamp(transitionSeconds, 0.0f, 10.0f);
BOOL applied = NO;
if (period > 0.01f &&
[client respondsToSelector:@selector(setStrength:withPeriod:commit:)]) {
applied = [client setStrength:requested withPeriod:period commit:YES];
} else if ([client respondsToSelector:@selector(setStrength:commit:)]) {
applied = [client setStrength:requested commit:YES];
}
BOOL enabled = [client setEnabled:requested > 0.005f ? YES : NO];
return applied && enabled;
}
#pragma mark - Read-only built-in display brightness probe
typedef int (*PTTDisplayServicesGetBrightnessFunction)(CGDirectDisplayID, float *);
static PTTDisplayServicesGetBrightnessFunction PTTBrightnessGetter(void) {
static PTTDisplayServicesGetBrightnessFunction function = NULL;
static dispatch_once_t once;
dispatch_once(&once, ^{
void *handle = dlopen(
"/System/Library/PrivateFrameworks/DisplayServices.framework/DisplayServices",
RTLD_LAZY | RTLD_LOCAL
);
if (handle != NULL) {
function = (PTTDisplayServicesGetBrightnessFunction)dlsym(
handle,
"DisplayServicesGetBrightness"
);
}
});
return function;
}
bool PTTBuiltInDisplayGetBrightness(float *outBrightness) {
if (outBrightness == NULL) { return false; }
PTTDisplayServicesGetBrightnessFunction getBrightness = PTTBrightnessGetter();
if (getBrightness == NULL) { return false; }
CGDirectDisplayID displays[16] = {0};
uint32_t count = 0;
if (CGGetActiveDisplayList(16, displays, &count) != kCGErrorSuccess) {
return false;
}
bool found = false;
float lowest = 1.0f;
for (uint32_t index = 0; index < count; index++) {
CGDirectDisplayID display = displays[index];
if (!CGDisplayIsBuiltin(display)) { continue; }
float brightness = 1.0f;
if (getBrightness(display, &brightness) == 0 &&
isfinite(brightness) && brightness >= 0.0f && brightness <= 1.5f) {
lowest = found ? fminf(lowest, brightness) : brightness;
found = true;
}
}
if (found) { *outBrightness = lowest; }
return found;
}
bool PTTBuiltInDisplayIsAsleep(bool *outAsleep) {
if (outAsleep == NULL) { return false; }
// The online list deliberately includes sleeping displays, unlike the
// active list used by the brightness probe above.
CGDirectDisplayID displays[16] = {0};
uint32_t count = 0;
if (CGGetOnlineDisplayList(16, displays, &count) != kCGErrorSuccess) {
return false;
}
bool found = false;
bool asleep = false;
for (uint32_t index = 0; index < count; index++) {
CGDirectDisplayID display = displays[index];
if (!CGDisplayIsBuiltin(display)) { continue; }
found = true;
asleep = asleep || CGDisplayIsAsleep(display);
}
if (found) { *outAsleep = asleep; }
return found;
}
bool PTTNightShiftRestore(PTTNightShiftState state) {
CBBlueLightClient *client = PTTClient();
if (client == nil) { return false; }
BOOL valueRestored = YES;
if ([client respondsToSelector:@selector(setStrength:withPeriod:commit:)]) {
valueRestored = [client setStrength:PTTClamp(state.strength, 0.0f, 1.0f)
withPeriod:0.35f
commit:YES];
} else if (state.cct >= 1000.0f &&
[client respondsToSelector:@selector(setCCT:withPeriod:commit:)]) {
valueRestored = [client setCCT:state.cct withPeriod:0.35f commit:YES];
}
if ([client respondsToSelector:@selector(setMode:)]) {
[client setMode:state.mode];
}
BOOL enabledRestored = [client setEnabled:state.enabled ? YES : NO];
if ([client respondsToSelector:@selector(setActive:)]) {
[client setActive:state.active ? YES : NO];
}
return valueRestored && enabledRestored;
}
#pragma mark - Read-only macOS camera AWB runtime probe
typedef struct {
float redGain;
float greenGain;
float blueGain;
} PTTWhiteBalanceGains;
typedef struct {
float temperature;
float tint;
} PTTTemperatureAndTint;
bool PTTCameraReadWhiteBalance(AVCaptureDevice *device, float *outKelvin, float *outTint) {
if (device == nil || outKelvin == NULL || outTint == NULL) { return false; }
SEL gainsSelector = NSSelectorFromString(@"deviceWhiteBalanceGains");
if (![device respondsToSelector:gainsSelector]) { return false; }
IMP gainsImplementation = [device methodForSelector:gainsSelector];
if (gainsImplementation == NULL) { return false; }
PTTWhiteBalanceGains (*readGains)(id, SEL) =
(PTTWhiteBalanceGains (*)(id, SEL))gainsImplementation;
PTTWhiteBalanceGains gains = readGains(device, gainsSelector);
if (!isfinite(gains.redGain) || !isfinite(gains.greenGain) || !isfinite(gains.blueGain) ||
gains.redGain <= 0.05f || gains.greenGain <= 0.05f || gains.blueGain <= 0.05f ||
gains.redGain > 32.0f || gains.greenGain > 32.0f || gains.blueGain > 32.0f) {
return false;
}
SEL conversionSelector = NSSelectorFromString(
@"temperatureAndTintValuesForDeviceWhiteBalanceGains:"
);
if ([device respondsToSelector:conversionSelector]) {
IMP conversionImplementation = [device methodForSelector:conversionSelector];
if (conversionImplementation != NULL) {
PTTTemperatureAndTint (*convert)(id, SEL, PTTWhiteBalanceGains) =
(PTTTemperatureAndTint (*)(id, SEL, PTTWhiteBalanceGains))conversionImplementation;
PTTTemperatureAndTint value = convert(device, conversionSelector, gains);
if (isfinite(value.temperature) && value.temperature >= 1500.0f &&
value.temperature <= 15000.0f && isfinite(value.tint)) {
*outKelvin = value.temperature;
*outTint = PTTClamp(value.tint, -300.0f, 300.0f);
return true;
}
}
}
// If the concrete camera exposes gains but not Apple's conversion helper,
// use the correction-gain ratio as a conservative illuminant estimate.
float blueToRed = gains.blueGain / gains.redGain;
float greenToGeometricMean = gains.greenGain / sqrtf(gains.redGain * gains.blueGain);
float kelvin = 6500.0f / powf(blueToRed, 1.05f);
*outKelvin = PTTClamp(kelvin, 1800.0f, 10000.0f);
*outTint = PTTClamp(logf(greenToGeometricMean) * 100.0f, -150.0f, 150.0f);
return true;
}
OBJC
cat > "$BUILD_DIR/main.swift" <<'SWIFT'
import Foundation
import AppKit
@preconcurrency import AVFoundation
import CoreMedia
import CoreVideo
import ServiceManagement
import ApplicationServices
import CoreGraphics
private let appName = "Pseudo True Tone"
private let samplingIntervalOptions: [TimeInterval] = [20.0, 60.0, 120.0, 300.0, 600.0]
private let defaultSamplingInterval: TimeInterval = 120.0
private let minimumVisibleBrightness: Float = 0.001
private let wakeSettlingDelay: TimeInterval = 2.0
private final class AppLog {
static let shared = AppLog()
let url: URL
private let queue = DispatchQueue(label: "local.pseudotruetone.log")
private var enabled = false
private let maximumBytes: UInt64 = 5 * 1024 * 1024
private let retainedBytes: UInt64 = 1024 * 1024
private init() {
let logs = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Library/Logs", isDirectory: true)
try? FileManager.default.createDirectory(at: logs, withIntermediateDirectories: true)
url = logs.appendingPathComponent("PseudoTrueTone.log")
}
func setEnabled(_ value: Bool) {
queue.sync { enabled = value }
}
func write(_ message: String) {
queue.async {
guard self.enabled else { return }
let formatter = ISO8601DateFormatter()
let line = "[\(formatter.string(from: Date()))] \(message)\n"
guard let data = line.data(using: .utf8) else { return }
if !FileManager.default.fileExists(atPath: self.url.path) {
FileManager.default.createFile(atPath: self.url.path, contents: nil)
}
self.truncateIfNeeded()
guard let handle = try? FileHandle(forWritingTo: self.url) else { return }
defer { try? handle.close() }
do {
try handle.seekToEnd()
try handle.write(contentsOf: data)
} catch {
// Logging must never interfere with display control.
}
}
}
private func truncateIfNeeded() {
guard let attributes = try? FileManager.default.attributesOfItem(atPath: url.path),
let size = (attributes[.size] as? NSNumber)?.uint64Value,
size >= maximumBytes,
let input = try? FileHandle(forReadingFrom: url)
else { return }
defer { try? input.close() }
do {
try input.seek(toOffset: size > retainedBytes ? size - retainedBytes : 0)
let tail = try input.readToEnd() ?? Data()
var replacement = Data("--- log truncated; newest entries retained ---\n".utf8)
replacement.append(tail)
try replacement.write(to: url, options: .atomic)
} catch {
// Log maintenance must never interfere with display control.
}
}
}
private struct AmbientReading {
let kelvin: Double
let tint: Double
let spread: Double
let brightness: Double
let neutralFraction: Double
let samples: Int
let source: String
var quality: String {
if spread <= 180.0 && brightness >= 0.07 && brightness <= 0.92 {
return neutralFraction >= 0.015 ? "good" : "good (colorful scene)"
}
if spread <= 500.0 && brightness >= 0.035 {
return "fair"
}
return "rough"
}
var isColorfulScene: Bool {
quality == "good (colorful scene)"
}
var menuQuality: String {
isColorfulScene ? "good" : quality
}
}
private struct SamplerFailure: LocalizedError {
let message: String
var errorDescription: String? { message }
}
private final class CameraSampler: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
private let sessionQueue = DispatchQueue(label: "local.pseudotruetone.camera.session")
private let sampleQueue = DispatchQueue(label: "local.pseudotruetone.camera.frames")
private let captureGateLock = NSLock()
private var session: AVCaptureSession?
private var camera: AVCaptureDevice?
private var output: AVCaptureVideoDataOutput?
private var captureAllowed = true
// Accessed only on sessionQueue.
private var currentID: UUID?
private var completion: ((Result<AmbientReading, Error>) -> Void)?
private var startedAt: TimeInterval = 0
private var lastAcceptedAt: TimeInterval = 0
private var temperatures: [Double] = []
private var tints: [Double] = []
private var brightnesses: [Double] = []
private var neutralFractions: [Double] = []
private var runtimeAWBSamples = 0
private var snapshotSamples = 0
func setCaptureAllowed(_ allowed: Bool) {
captureGateLock.lock()
captureAllowed = allowed
captureGateLock.unlock()
}
private func isCaptureAllowed() -> Bool {
captureGateLock.lock()
defer { captureGateLock.unlock() }
return captureAllowed
}
private func captureMayStart() -> Bool {
guard isCaptureAllowed() else { return false }
var asleep = false
if PTTBuiltInDisplayIsAsleep(&asleep), asleep {
return false
}
return true
}
func takeReading(completion: @escaping (Result<AmbientReading, Error>) -> Void) {
guard captureMayStart() else {
DispatchQueue.main.async {
completion(.failure(SamplerFailure(message: "Sampling paused.")))
}
return
}
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
sessionQueue.async { self.begin(completion: completion) }
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video) { granted in
if granted {
self.sessionQueue.async { self.begin(completion: completion) }
} else {
DispatchQueue.main.async {
completion(.failure(SamplerFailure(message: "Camera access was denied.")))
}
}
}
case .denied:
DispatchQueue.main.async {
completion(.failure(SamplerFailure(
message: "Camera access is off. Enable it in Privacy & Security → Camera."
)))
}
case .restricted:
DispatchQueue.main.async {
completion(.failure(SamplerFailure(message: "Camera access is restricted on this Mac.")))
}
@unknown default:
DispatchQueue.main.async {
completion(.failure(SamplerFailure(message: "Unknown camera authorization state.")))
}
}
}
func cancelCurrentReading(completion: (() -> Void)? = nil) {
sessionQueue.async { [weak self] in
guard let self else {
DispatchQueue.main.async { completion?() }
return
}
if let id = self.currentID {
self.finish(
id: id,
result: .failure(SamplerFailure(message: "Sampling paused."))
)
}
DispatchQueue.main.async { completion?() }
}
}
func cancelCurrentReadingAndWait() {
sessionQueue.sync {
guard let id = currentID else { return }
finish(
id: id,
result: .failure(SamplerFailure(message: "Sampling paused."))
)
}
}
private func begin(completion: @escaping (Result<AmbientReading, Error>) -> Void) {
guard captureMayStart() else {
DispatchQueue.main.async {
completion(.failure(SamplerFailure(message: "Sampling paused.")))
}
return
}
guard currentID == nil else {
DispatchQueue.main.async {
completion(.failure(SamplerFailure(message: "A camera sample is already running.")))
}
return
}
do {
try prepareSessionIfNeeded()
} catch {
DispatchQueue.main.async { completion(.failure(error)) }
return
}
guard let session else {
DispatchQueue.main.async {
completion(.failure(SamplerFailure(message: "Could not create a camera session.")))
}
return
}
let id = UUID()
currentID = id
self.completion = completion
startedAt = ProcessInfo.processInfo.systemUptime
lastAcceptedAt = 0
temperatures.removeAll(keepingCapacity: true)
tints.removeAll(keepingCapacity: true)
brightnesses.removeAll(keepingCapacity: true)
neutralFractions.removeAll(keepingCapacity: true)
runtimeAWBSamples = 0
snapshotSamples = 0
// Recheck immediately before the synchronous AVFoundation start. This
// closes the permission/configuration window where the display could
// have powered down after the menu-layer preflight.
guard captureMayStart() else {
finish(
id: id,
result: .failure(SamplerFailure(message: "Sampling paused."))
)
return
}
session.startRunning()
guard session.isRunning else {
finish(id: id, result: .failure(SamplerFailure(
message: "The camera did not start; it may be unavailable or in use."
)))
return
}
sessionQueue.asyncAfter(deadline: .now() + 5.0) { [weak self] in
guard let self, self.currentID == id else { return }
if self.temperatures.count >= 3 {
self.finishFromCollectedValues(id: id)
} else {
self.finish(id: id, result: .failure(SamplerFailure(
message: "The camera did not produce a stable white-balance reading."
)))
}
}
}
private func prepareSessionIfNeeded() throws {
if session != nil { return }
let discovery = AVCaptureDevice.DiscoverySession(
deviceTypes: [.builtInWideAngleCamera],
mediaType: .video,
position: .unspecified
)
guard let device = discovery.devices.first ?? AVCaptureDevice.default(for: .video) else {
throw SamplerFailure(message: "No video camera was found.")
}
let input = try AVCaptureDeviceInput(device: device)
let videoOutput = AVCaptureVideoDataOutput()
videoOutput.alwaysDiscardsLateVideoFrames = true
videoOutput.videoSettings = [
kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_32BGRA)
]
videoOutput.setSampleBufferDelegate(self, queue: sampleQueue)
let captureSession = AVCaptureSession()
captureSession.beginConfiguration()
if captureSession.canSetSessionPreset(.low) {
captureSession.sessionPreset = .low
}
guard captureSession.canAddInput(input), captureSession.canAddOutput(videoOutput) else {
captureSession.commitConfiguration()
throw SamplerFailure(message: "The built-in camera cannot provide video frames.")
}
captureSession.addInput(input)
captureSession.addOutput(videoOutput)
captureSession.commitConfiguration()
camera = device
output = videoOutput
session = captureSession
AppLog.shared.write("Prepared camera: \(device.localizedName)")
}
func captureOutput(
_ output: AVCaptureOutput,
didOutput sampleBuffer: CMSampleBuffer,
from connection: AVCaptureConnection
) {
guard let device = camera,
let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)
else { return }
let stats = frameStats(pixelBuffer)
var runtimeKelvin: Float = 0
var runtimeTint: Float = 0
let hasRuntimeAWB = PTTCameraReadWhiteBalance(device, &runtimeKelvin, &runtimeTint)
let kelvin = hasRuntimeAWB ? Double(runtimeKelvin) : stats.kelvin
let tint = hasRuntimeAWB ? Double(runtimeTint) : stats.tint
guard kelvin.isFinite, tint.isFinite,
kelvin >= 1_500.0, kelvin <= 15_000.0,
abs(tint) <= 300.0
else { return }
let uptime = ProcessInfo.processInfo.systemUptime
let adjusting = device.isAdjustingWhiteBalance
sessionQueue.async { [weak self] in
self?.ingest(
uptime: uptime,
kelvin: kelvin,
tint: tint,
brightness: stats.brightness,
neutralFraction: stats.neutralFraction,
adjusting: adjusting,
runtimeAWB: hasRuntimeAWB
)
}
}
private func ingest(
uptime: TimeInterval,
kelvin: Double,
tint: Double,
brightness: Double,
neutralFraction: Double,
adjusting: Bool,
runtimeAWB: Bool
) {
guard let id = currentID else { return }
let elapsed = uptime - startedAt
guard elapsed >= (runtimeAWB ? 0.5 : 0.05) else { return }
guard uptime - lastAcceptedAt >= 0.12 else { return }
if runtimeAWB && adjusting && elapsed < 2.2 { return }
lastAcceptedAt = uptime
temperatures.append(kelvin)
tints.append(tint)
brightnesses.append(brightness)
neutralFractions.append(neutralFraction)
if runtimeAWB {
runtimeAWBSamples += 1
} else {
snapshotSamples += 1
}
if temperatures.count >= 10 && elapsed >= 1.5 {
finishFromCollectedValues(id: id)
} else if elapsed >= 3.4 && temperatures.count >= 3 {
finishFromCollectedValues(id: id)
}
}
private func finishFromCollectedValues(id: UUID) {
let temperature = median(temperatures)
let tint = median(tints)
let brightness = median(brightnesses)
let neutral = median(neutralFractions)
let deviations = temperatures.map { abs($0 - temperature) }
let spread = median(deviations)
let source = runtimeAWBSamples >= snapshotSamples ? "camera AWB" : "snapshot estimate"
let reading = AmbientReading(
kelvin: temperature,
tint: tint,
spread: spread,
brightness: brightness,
neutralFraction: neutral,
samples: temperatures.count,
source: source
)
finish(id: id, result: .success(reading))
}
private func finish(id: UUID, result: Result<AmbientReading, Error>) {
guard currentID == id else { return }
if session?.isRunning == true {
session?.stopRunning()
}
let callback = completion
currentID = nil
completion = nil
DispatchQueue.main.async { callback?(result) }
}
private func median(_ values: [Double]) -> Double {
guard !values.isEmpty else { return 0 }
let sorted = values.sorted()
let middle = sorted.count / 2
if sorted.count.isMultiple(of: 2) {
return (sorted[middle - 1] + sorted[middle]) / 2.0
}
return sorted[middle]
}
private func frameStats(_ buffer: CVPixelBuffer) -> (
brightness: Double,
neutralFraction: Double,
kelvin: Double,
tint: Double
) {
guard CVPixelBufferGetPixelFormatType(buffer) == kCVPixelFormatType_32BGRA else {
return (0.5, 0, 6500, 0)
}
CVPixelBufferLockBaseAddress(buffer, .readOnly)
defer { CVPixelBufferUnlockBaseAddress(buffer, .readOnly) }
guard let base = CVPixelBufferGetBaseAddress(buffer) else { return (0.5, 0, 6500, 0) }
let width = CVPixelBufferGetWidth(buffer)
let height = CVPixelBufferGetHeight(buffer)
let bytesPerRow = CVPixelBufferGetBytesPerRow(buffer)
let bytes = base.assumingMemoryBound(to: UInt8.self)
let step = max(1, min(width, height) / 48)
var lumaSum = 0.0
var count = 0
var neutralCount = 0
var weightedRed = 0.0
var weightedGreen = 0.0
var weightedBlue = 0.0
var weightSum = 0.0
for y in Swift.stride(from: 0, to: height, by: step) {
let row = bytes.advanced(by: y * bytesPerRow)
for x in Swift.stride(from: 0, to: width, by: step) {
let p = row.advanced(by: x * 4)
let blue = Int(p[0])
let green = Int(p[1])
let red = Int(p[2])
let maximum = max(red, max(green, blue))
let minimum = min(red, min(green, blue))
let luma = 0.2126 * Double(red) + 0.7152 * Double(green) + 0.0722 * Double(blue)
lumaSum += luma / 255.0
if luma >= 20.0 && luma <= 238.0 && maximum > 0 {
let chroma = Double(maximum - minimum) / Double(maximum)
if chroma <= 0.18 { neutralCount += 1 }
// Favor bright, low-chroma surfaces without requiring a
// perfectly gray card. A warm-lit white surface is still
// accepted because the chroma ceiling is intentionally wide.
if chroma <= 0.52 {
let normalizedLuma = luma / 255.0
let weight = pow(normalizedLuma, 1.4) * pow(1.0 - chroma, 2.0)
weightedRed += linearSRGB(Double(red) / 255.0) * weight
weightedGreen += linearSRGB(Double(green) / 255.0) * weight
weightedBlue += linearSRGB(Double(blue) / 255.0) * weight
weightSum += weight
}
}
count += 1
}
}
guard count > 0, weightSum > 0.0001 else { return (0.5, 0, 6500, 0) }
let red = weightedRed / weightSum
let green = weightedGreen / weightSum
let blue = weightedBlue / weightSum
// Linear sRGB D65 → CIE XYZ, then McCamy's CCT approximation.
let xValue = 0.4124564 * red + 0.3575761 * green + 0.1804375 * blue
let yValue = 0.2126729 * red + 0.7151522 * green + 0.0721750 * blue
let zValue = 0.0193339 * red + 0.1191920 * green + 0.9503041 * blue
let xyzSum = xValue + yValue + zValue
var kelvin = 6500.0
if xyzSum > 0.0001 {
let chromaticityX = xValue / xyzSum
let chromaticityY = yValue / xyzSum
let denominator = 0.1858 - chromaticityY
if abs(denominator) > 0.00001 {
let n = (chromaticityX - 0.3320) / denominator
let estimate = 449.0 * pow(n, 3.0) + 3525.0 * pow(n, 2.0) + 6823.3 * n + 5520.33
if estimate.isFinite {
kelvin = min(max(estimate, 1800.0), 10000.0)
}
}
}
let expectedGreen = sqrt(max(red * blue, 0.000001))
let tint = min(max(log(max(green, 0.000001) / expectedGreen) * 100.0, -150.0), 150.0)
return (
lumaSum / Double(count),
Double(neutralCount) / Double(count),
kelvin,
tint
)
}
private func linearSRGB(_ value: Double) -> Double {
if value <= 0.04045 { return value / 12.92 }
return pow((value + 0.055) / 1.055, 2.4)
}
}
@MainActor private var showedAccessibilityAlert = false
@MainActor
private func relaunchPseudoTrueTone() {
let isApp = Bundle.main.bundlePath.hasSuffix(".app")
let target = isApp ? Bundle.main.bundlePath : (Bundle.main.executablePath ?? Bundle.main.bundlePath)
let process = Process()
process.executableURL = URL(fileURLWithPath: "/bin/sh")
process.arguments = [
"-c",
isApp ? "sleep 0.5; /usr/bin/open \"$1\"" : "sleep 0.5; \"$1\" >/dev/null 2>&1 &",
"sh",
target
]
try? process.run()
NSApp.terminate(nil)
}
@MainActor
private func openAccessibilitySettings() {
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") {
NSWorkspace.shared.open(url)
}
}
@MainActor
private func showAccessibilityAlert() {
guard !showedAccessibilityAlert else { return }
showedAccessibilityAlert = true
NSApp.activate(ignoringOtherApps: true)
let alert = NSAlert()
alert.alertStyle = .warning
alert.messageText = "Accessibility Permission Needed"
alert.informativeText = "Give Accessibility permission in System Settings, then restart Pseudo True Tone. This permission is used only to detect fullscreen windows."
alert.addButton(withTitle: "Open Settings")
alert.addButton(withTitle: "Restart Pseudo True Tone")
alert.addButton(withTitle: "Quit Pseudo True Tone")
switch alert.runModal() {
case .alertFirstButtonReturn:
openAccessibilitySettings()
let followup = NSAlert()
followup.alertStyle = .informational
followup.messageText = "Restart After Permission"
followup.informativeText = "After enabling Accessibility permission, restart Pseudo True Tone."
followup.addButton(withTitle: "Restart Pseudo True Tone")
followup.addButton(withTitle: "Quit Pseudo True Tone")
if followup.runModal() == .alertFirstButtonReturn {
relaunchPseudoTrueTone()
} else {
NSApp.terminate(nil)
}
case .alertSecondButtonReturn:
relaunchPseudoTrueTone()
default:
NSApp.terminate(nil)
}
}
private func axCopy(_ element: AXUIElement, _ attribute: String) -> AnyObject? {
var value: CFTypeRef?
return AXUIElementCopyAttributeValue(element, attribute as CFString, &value) == .success ? value : nil
}
private func axBool(_ element: AXUIElement, _ attribute: String) -> Bool {
(axCopy(element, attribute) as? NSNumber)?.boolValue ?? false
}
private func axValue(_ element: AXUIElement, _ attribute: String, _ type: AXValueType) -> AXValue? {
guard let value = axCopy(element, attribute) else { return nil }
let axValue = value as! AXValue
return AXValueGetType(axValue) == type ? axValue : nil
}
private func axRect(_ element: AXUIElement) -> CGRect? {
guard let position = axValue(element, kAXPositionAttribute, .cgPoint),
let dimensions = axValue(element, kAXSizeAttribute, .cgSize)
else { return nil }
var point = CGPoint.zero
var size = CGSize.zero
guard AXValueGetValue(position, .cgPoint, &point),
AXValueGetValue(dimensions, .cgSize, &size)
else { return nil }
return CGRect(origin: point, size: size)
}
private func focusedOrMainWindow(_ app: AXUIElement) -> AXUIElement? {
if let window = axCopy(app, kAXFocusedWindowAttribute) { return (window as! AXUIElement) }
if let window = axCopy(app, kAXMainWindowAttribute) { return (window as! AXUIElement) }
return nil
}
private func axWindows(_ app: AXUIElement) -> [AXUIElement] {
guard let values = axCopy(app, kAXWindowsAttribute) as? [AnyObject] else { return [] }
return values.map { $0 as! AXUIElement }
}
private func displayRects() -> [CGRect] {
var count: UInt32 = 0
CGGetActiveDisplayList(0, nil, &count)
var identifiers = [CGDirectDisplayID](repeating: 0, count: Int(count))
CGGetActiveDisplayList(count, &identifiers, &count)
return identifiers.prefix(Int(count)).map { CGDisplayBounds($0) }
}
private func fillsDisplay(_ rect: CGRect, _ display: CGRect) -> Bool {
let tolerance: CGFloat = 8
let intersection = rect.intersection(display)
return intersection.width >= display.width - tolerance &&
intersection.height >= display.height - tolerance
}
private func windowLooksFullscreen(_ window: AXUIElement, _ displays: [CGRect]) -> Bool {
if axBool(window, "AXFullScreen") { return true }
guard let rect = axRect(window) else { return false }
return displays.contains { fillsDisplay(rect, $0) }
}
private func cgFullscreenWindowVisible() -> Bool {
guard let app = NSWorkspace.shared.frontmostApplication,
app.bundleIdentifier != "com.apple.finder"
else { return false }
let pid = Int(app.processIdentifier)
let selfPID = Int(ProcessInfo.processInfo.processIdentifier)
let displays = displayRects()
guard pid != selfPID,
let windows = CGWindowListCopyWindowInfo(
[.optionOnScreenOnly, .excludeDesktopElements],
kCGNullWindowID
) as? [[String: Any]]
else { return false }
for window in windows {
guard let owner = (window[kCGWindowOwnerPID as String] as? NSNumber)?.intValue,
owner == pid,
let layer = (window[kCGWindowLayer as String] as? NSNumber)?.intValue,
layer == 0,
let alpha = (window[kCGWindowAlpha as String] as? NSNumber)?.doubleValue,
alpha > 0,
let bounds = window[kCGWindowBounds as String] as? NSDictionary,
let rect = CGRect(dictionaryRepresentation: bounds as CFDictionary)
else { continue }
if displays.contains(where: { fillsDisplay(rect, $0) }) { return true }
}
return false
}
private func hasFullscreenWindowVisible() -> Bool {
guard NSWorkspace.shared.frontmostApplication?.bundleIdentifier != "com.apple.finder" else {
return false
}
let displays = displayRects()
if AXIsProcessTrusted(),
let app = NSWorkspace.shared.frontmostApplication,
app.processIdentifier != ProcessInfo.processInfo.processIdentifier {
let axApp = AXUIElementCreateApplication(app.processIdentifier)
var windows = axWindows(axApp)
if let window = focusedOrMainWindow(axApp) { windows.insert(window, at: 0) }
for window in windows where windowLooksFullscreen(window, displays) { return true }
}
return cgFullscreenWindowVisible()
}
private func fallbackHalfMoonMenuImage() -> NSImage {
let image = NSImage(size: NSSize(width: 18, height: 18))
image.lockFocus()
NSColor.black.setFill()
let center = NSPoint(x: 9, y: 9)
let radius: CGFloat = 7
let halfMoon = NSBezierPath()
halfMoon.move(to: NSPoint(x: center.x, y: center.y + radius))
halfMoon.appendArc(
withCenter: center,
radius: radius,
startAngle: 90,
endAngle: -90,
clockwise: true
)
halfMoon.close()
halfMoon.fill()
image.unlockFocus()
image.isTemplate = true
return image
}
private func axCallback(
_ observer: AXObserver,
_ element: AXUIElement,
_ notification: CFString,
_ refcon: UnsafeMutableRawPointer?
) {
guard let refcon else { return }
let app = Unmanaged<AppDelegate>.fromOpaque(refcon).takeUnretainedValue()
DispatchQueue.main.async { app.axEvent() }
}
private final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
private let defaults = UserDefaults.standard
private let sampler = CameraSampler()
private let savedNightShiftKey = "savedNightShiftState"
private let nightShiftDirtyKey = "nightShiftControlWasActive"
private var statusItem: NSStatusItem!
private var menu: NSMenu!
private var enabledItem: NSMenuItem!
private var samplingPauseItem: NSMenuItem!
private var fullscreenPauseItem: NSMenuItem!
private var loggingItem: NSMenuItem!
private var statusLineItem: NSMenuItem!
private var ambientLineItem: NSMenuItem!
private var sourceLineItem: NSMenuItem!
private var targetLineItem: NSMenuItem!
private var sampledLineItem: NSMenuItem!
private var sampleNowItem: NSMenuItem!
private var launchItem: NSMenuItem!
private var samplingIntervalControl: NSSegmentedControl!
private var samplingIntervalTitleLabel: NSTextField!
private var responseControl: NSSegmentedControl!
private var responseTitleLabel: NSTextField!
private var offsetSlider: NSSlider!
private var offsetSliderValue: NSTextField!
private var outputSlider: NSSlider!
private var outputSliderValue: NSTextField!
private var timer: Timer?
private var adaptiveEnabled = true
private var samplingPaused = false
private var pauseInFullscreen = false
private var fullscreenActive = false
private var loggingEnabled = false
private var samplingInterval = defaultSamplingInterval
private var response = 0.85
private var warmthOffset = 0.0
private var actualOutputStrength = 0.0
private var sampling = false
private var brightnessBlocked = false
private var displaySleeping = false
private var systemSleeping = false
private var displayProbeFailureLogged = false
private var cancellationStatus: String?
private var nightShiftAvailable = false
private var originalState: PTTNightShiftState?
private var smoothedMired: Double?
private var lastAppliedCCT: Double?
private var lastReading: AmbientReading?
private var lastSampleDate: Date?
private var statusText = "Starting…"
private var hadError = false
private var fullscreenDebounce: DispatchWorkItem?
private var wakeGeneration = 0
private var wakeSettling = false
private var axObserver: AXObserver?
private var axApp: AXUIElement?
private var axObservedWindows: [AXUIElement] = []
private var offsetSnapAnimationID = 0
private var offsetSnapAnimating = false
func applicationWillFinishLaunching(_ notification: Notification) {
_ = NSApp.setActivationPolicy(.accessory)
}
func applicationDidFinishLaunching(_ notification: Notification) {
if defaults.object(forKey: "adaptiveEnabled") != nil {
adaptiveEnabled = defaults.bool(forKey: "adaptiveEnabled")
}
if let stored = defaults.object(forKey: "response") as? Double,
[0.70, 0.85, 1.00].contains(where: { abs($0 - stored) < 0.001 }) {
response = stored
}
if let stored = defaults.object(forKey: "samplingInterval") as? Double,
samplingIntervalOptions.contains(where: { abs($0 - stored) < 0.001 }) {
samplingInterval = stored
}
samplingPaused = defaults.bool(forKey: "samplingPaused")
pauseInFullscreen = defaults.bool(forKey: "pauseInFullscreen")
loggingEnabled = defaults.bool(forKey: "loggingEnabled")
if let storedOffset = defaults.object(forKey: "warmthOffset") as? Double {
warmthOffset = min(max(storedOffset, -0.35), 0.35)
}
AppLog.shared.setEnabled(loggingEnabled)
buildMenu()
if let asleep = builtInDisplaySleepState() {
displaySleeping = asleep
sampler.setCaptureAllowed(!asleep)
} else {
sampler.setCaptureAllowed(true)
}
var state = PTTNightShiftState()
nightShiftAvailable = PTTNightShiftGetState(&state)
let recoveryResult = nightShiftAvailable ? recoverInterruptedSessionIfNeeded() : nil
if !nightShiftAvailable {
statusText = "Night Shift private API unavailable"
hadError = true
} else if recoveryResult == false {
adaptiveEnabled = false
defaults.set(false, forKey: "adaptiveEnabled")
statusText = "Could not recover Night Shift; paused"
hadError = true
} else if recoveryResult == true {
if !adaptiveEnabled {
statusText = "Recovered previous Night Shift state; stopped"
} else if samplingPaused {
statusText = "Recovered previous Night Shift state; camera paused"
} else {
statusText = "Recovered previous Night Shift state; waiting…"
}
} else if !adaptiveEnabled {
statusText = "Stopped"
} else if samplingPaused {
statusText = "Camera sampling paused"
} else if adaptiveEnabled {
statusText = "Waiting for first camera sample…"
} else {
statusText = "Paused"
}
if displaySleeping {
statusText = "Display off • camera stopped"
}
updateUI()
scheduleSamplingTimer()
let workspaceCenter = NSWorkspace.shared.notificationCenter
workspaceCenter.addObserver(
self,
selector: #selector(didWake(_:)),
name: NSWorkspace.didWakeNotification,
object: nil
)
workspaceCenter.addObserver(
self,
selector: #selector(willSleep(_:)),
name: NSWorkspace.willSleepNotification,
object: nil
)
workspaceCenter.addObserver(
self,
selector: #selector(screensDidSleep(_:)),
name: NSWorkspace.screensDidSleepNotification,
object: nil
)
workspaceCenter.addObserver(
self,
selector: #selector(screensDidWake(_:)),
name: NSWorkspace.screensDidWakeNotification,
object: nil
)
workspaceCenter.addObserver(
self,
selector: #selector(frontAppChanged(_:)),
name: NSWorkspace.activeSpaceDidChangeNotification,
object: nil
)
workspaceCenter.addObserver(
self,
selector: #selector(frontAppChanged(_:)),
name: NSWorkspace.didActivateApplicationNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(frontAppChanged(_:)),
name: NSApplication.didChangeScreenParametersNotification,
object: nil
)
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.setupAXObserver()
self.refreshFullscreenState(resampleOnResume: false)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { [weak self] in
guard let self else { return }
self.showWelcomeIfNeeded()
self.requestSample(manual: false)
}
AppLog.shared.write("Application started; interval=\(Int(samplingInterval))s response=\(response)")
}
func applicationWillTerminate(_ notification: Notification) {
timer?.invalidate()
fullscreenDebounce?.cancel()
sampler.setCaptureAllowed(false)
sampler.cancelCurrentReadingAndWait()
cleanupAXObserver()
restoreOriginalNightShift(updateStatus: false)
AppLog.shared.write("Application terminated")
}
private func scheduleSamplingTimer() {
timer?.invalidate()
let repeating = Timer(timeInterval: samplingInterval, repeats: true) { [weak self] _ in
self?.requestSample(manual: false)
}
timer = repeating
RunLoop.main.add(repeating, forMode: .common)
}
private func buildMenu() {
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
statusItem.button?.toolTip = appName
menu = NSMenu()
menu.delegate = self
enabledItem = NSMenuItem(title: "Adaptive warmth enabled", action: #selector(toggleEnabled), keyEquivalent: "")
enabledItem.target = self
menu.addItem(enabledItem)
samplingPauseItem = NSMenuItem(
title: "Pause camera sampling",
action: #selector(toggleSamplingPause),
keyEquivalent: ""
)
samplingPauseItem.target = self
menu.addItem(samplingPauseItem)
fullscreenPauseItem = NSMenuItem(
title: "Pause sampling in fullscreen",
action: #selector(toggleFullscreenPause),
keyEquivalent: ""
)
fullscreenPauseItem.target = self
menu.addItem(fullscreenPauseItem)
menu.addItem(.separator())
statusLineItem = informationItem("Status: Starting…")
ambientLineItem = informationItem("Ambient: —")
sourceLineItem = informationItem("Estimator: —")
targetLineItem = informationItem("Display target: —")
sampledLineItem = informationItem("Last sample: —")
menu.addItem(statusLineItem)
menu.addItem(ambientLineItem)
menu.addItem(sourceLineItem)
menu.addItem(targetLineItem)
menu.addItem(sampledLineItem)
menu.addItem(.separator())
let offsetRow = sliderRow(
title: "Warmth offset",
minimum: -0.35,
maximum: 0.35,
value: warmthOffset,
enabled: true,
action: #selector(offsetChanged(_:)),
showsZeroMarker: true
)
offsetSlider = offsetRow.slider
offsetSliderValue = offsetRow.valueLabel
offsetSlider.isContinuous = true
_ = offsetSlider.sendAction(on: [.leftMouseDragged, .leftMouseUp])
menu.addItem(offsetRow.item)
let outputRow = sliderRow(
title: "Actual Night Shift output",
minimum: 0,
maximum: 1,
value: 0,
enabled: true,
action: #selector(outputChanged(_:)),
showsZeroMarker: false
)
outputSlider = outputRow.slider
outputSliderValue = outputRow.valueLabel
outputSlider.isContinuous = true
_ = outputSlider.sendAction(on: [.leftMouseDragged, .leftMouseUp])
menu.addItem(outputRow.item)
menu.addItem(.separator())
sampleNowItem = NSMenuItem(title: "Sample camera now", action: #selector(sampleNow), keyEquivalent: "r")
sampleNowItem.target = self
menu.addItem(sampleNowItem)
menu.addItem(.separator())
let samplingIntervalRow = samplingIntervalSelectorRow()
samplingIntervalControl = samplingIntervalRow.control
samplingIntervalTitleLabel = samplingIntervalRow.titleLabel
menu.addItem(samplingIntervalRow.item)
menu.addItem(.separator())
let responseRow = responseSelectorRow()
responseControl = responseRow.control
responseTitleLabel = responseRow.titleLabel
menu.addItem(responseRow.item)
menu.addItem(.separator())
loggingItem = NSMenuItem(title: "Enable logging", action: #selector(toggleLogging), keyEquivalent: "")
loggingItem.target = self
menu.addItem(loggingItem)
launchItem = NSMenuItem(title: "Launch at login", action: #selector(toggleLaunchAtLogin), keyEquivalent: "")
launchItem.target = self
menu.addItem(launchItem)
menu.addItem(.separator())
let privacy = NSMenuItem(title: "Camera privacy settings…", action: #selector(openCameraSettings), keyEquivalent: "")
privacy.target = self
menu.addItem(privacy)
let log = NSMenuItem(title: "Reveal log", action: #selector(revealLog), keyEquivalent: "")
log.target = self
menu.addItem(log)
let about = NSMenuItem(title: "About Pseudo True Tone…", action: #selector(showAbout), keyEquivalent: "")
about.target = self
menu.addItem(about)
menu.addItem(.separator())
let quit = NSMenuItem(title: "Quit Pseudo True Tone", action: #selector(quit), keyEquivalent: "q")
quit.target = self
menu.addItem(quit)
statusItem.menu = menu
}
private func informationItem(_ title: String) -> NSMenuItem {
let item = NSMenuItem(title: title, action: nil, keyEquivalent: "")
item.isEnabled = false
return item
}
private func sliderRow(
title: String,
minimum: Double,
maximum: Double,
value: Double,
enabled: Bool,
action: Selector?,
showsZeroMarker: Bool
) -> (item: NSMenuItem, slider: NSSlider, valueLabel: NSTextField) {
let item = NSMenuItem()
let view = NSView(frame: NSRect(x: 0, y: 0, width: 390, height: 64))
let titleLabel = NSTextField(labelWithString: title)
let valueLabel = NSTextField(labelWithString: "—")
let slider = NSSlider(value: value, minValue: minimum, maxValue: maximum, target: self, action: action)
titleLabel.font = NSFont.menuFont(ofSize: 0)
valueLabel.font = NSFont.monospacedDigitSystemFont(ofSize: 11, weight: .regular)
valueLabel.textColor = .secondaryLabelColor
valueLabel.alignment = .right
slider.isEnabled = enabled
for control in [titleLabel, valueLabel, slider] {
control.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(control)
}
NSLayoutConstraint.activate([
titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
titleLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 7),
valueLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
valueLabel.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor),
valueLabel.leadingAnchor.constraint(greaterThanOrEqualTo: titleLabel.trailingAnchor, constant: 8),
slider.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
slider.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
slider.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 5)
])
if showsZeroMarker {
let marker = NSView()
marker.translatesAutoresizingMaskIntoConstraints = false
marker.wantsLayer = true
marker.layer?.backgroundColor = NSColor.tertiaryLabelColor.cgColor
view.addSubview(marker)
NSLayoutConstraint.activate([
marker.centerXAnchor.constraint(equalTo: slider.centerXAnchor),
marker.topAnchor.constraint(equalTo: slider.centerYAnchor, constant: 7),
marker.widthAnchor.constraint(equalToConstant: 1),
marker.heightAnchor.constraint(equalToConstant: 6)
])
}
item.view = view
return (item, slider, valueLabel)
}
private func samplingIntervalSegmentIndex() -> Int {
samplingIntervalOptions.enumerated().min {
abs($0.element - samplingInterval) < abs($1.element - samplingInterval)
}?.offset ?? 2
}
private func samplingIntervalDisplayText(_ interval: TimeInterval) -> String {
if interval < 60 {
return "\(Int(interval)) sec"
}
return "\(Int(interval / 60)) min"
}
private func samplingIntervalTitleText() -> String {
let pausedSuffix = samplingPaused ? " (paused)" : ""
return "Sampling interval: \(samplingIntervalDisplayText(samplingInterval))\(pausedSuffix)"
}
private func samplingIntervalSelectorRow() -> (
item: NSMenuItem,
control: NSSegmentedControl,
titleLabel: NSTextField
) {
let item = NSMenuItem()
let view = NSView(frame: NSRect(x: 0, y: 0, width: 390, height: 64))
let titleLabel = NSTextField(labelWithString: samplingIntervalTitleText())
let control = NSSegmentedControl(frame: .zero)
titleLabel.font = NSFont.menuFont(ofSize: 0)
control.segmentCount = samplingIntervalOptions.count
control.trackingMode = .selectOne
control.segmentStyle = .rounded
let titles = ["20s", "1m", "2m", "5m", "10m"]
let toolTips = [
"Every 20 seconds",
"Every 1 minute",
"Every 2 minutes",
"Every 5 minutes",
"Every 10 minutes"
]
let segmentWidth: CGFloat = (390 - 32) / CGFloat(samplingIntervalOptions.count)
for segment in samplingIntervalOptions.indices {
control.setLabel(titles[segment], forSegment: segment)
control.setWidth(segmentWidth, forSegment: segment)
control.setToolTip(toolTips[segment], forSegment: segment)
}
control.selectedSegment = samplingIntervalSegmentIndex()
control.target = self
control.action = #selector(samplingIntervalChanged(_:))
for child in [titleLabel, control] {
child.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(child)
}
NSLayoutConstraint.activate([
titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
titleLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 7),
control.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
control.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
control.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 5),
control.heightAnchor.constraint(equalToConstant: 26)
])
item.view = view
return (item, control, titleLabel)
}
private func responseSegmentIndex() -> Int {
if response >= 0.925 { return 2 }
if response >= 0.775 { return 1 }
return 0
}
private func responseTitleText() -> String {
switch responseSegmentIndex() {
case 0: return "Response: Gentle (70%)"
case 1: return "Response: Balanced (85%)"
default: return "Response: Match ambient (100%)"
}
}
private func responseSelectorRow() -> (
item: NSMenuItem,
control: NSSegmentedControl,
titleLabel: NSTextField
) {
let item = NSMenuItem()
let view = NSView(frame: NSRect(x: 0, y: 0, width: 390, height: 64))
let titleLabel = NSTextField(labelWithString: responseTitleText())
let control = NSSegmentedControl(frame: .zero)
titleLabel.font = NSFont.menuFont(ofSize: 0)
control.segmentCount = 3
control.trackingMode = .selectOne
control.segmentStyle = .rounded
let titles = ["Gentle", "Balanced", "Match ambient"]
let percentages = ["(70%)", "(85%)", "(100%)"]
let segmentWidth: CGFloat = (390 - 32) / 3
for segment in 0..<3 {
control.setLabel(titles[segment], forSegment: segment)
control.setWidth(segmentWidth, forSegment: segment)
control.setToolTip(
"\(titles[segment]) \(percentages[segment])",
forSegment: segment
)
}
control.selectedSegment = responseSegmentIndex()
control.target = self
control.action = #selector(responseChanged(_:))
for child in [titleLabel, control] {
child.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(child)
}
NSLayoutConstraint.activate([
titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
titleLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 7),
control.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
control.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
control.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 5),
control.heightAnchor.constraint(equalToConstant: 26)
])
item.view = view
return (item, control, titleLabel)
}
private func cleanupAXObserver() {
if let observer = axObserver {
CFRunLoopRemoveSource(
CFRunLoopGetMain(),
AXObserverGetRunLoopSource(observer),
.defaultMode
)
}
axObserver = nil
axApp = nil
axObservedWindows = []
}
private func addAX(_ observer: AXObserver, _ element: AXUIElement, _ name: String) {
_ = AXObserverAddNotification(
observer,
element,
name as CFString,
Unmanaged.passUnretained(self).toOpaque()
)
}
private func attachAXWindows() {
guard let observer = axObserver, let app = axApp else { return }
var windows = axWindows(app)
if let window = focusedOrMainWindow(app) { windows.insert(window, at: 0) }
axObservedWindows = windows
for window in windows {
addAX(observer, window, kAXMovedNotification)
addAX(observer, window, kAXResizedNotification)
addAX(observer, window, kAXUIElementDestroyedNotification)
addAX(observer, window, "AXFullScreenChanged")
}
}
private func setupAXObserver() {
cleanupAXObserver()
guard pauseInFullscreen else { return }
let options = [
kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true
] as CFDictionary
guard AXIsProcessTrustedWithOptions(options) else {
Task { await MainActor.run { showAccessibilityAlert() } }
return
}
guard let app = NSWorkspace.shared.frontmostApplication,
app.processIdentifier != ProcessInfo.processInfo.processIdentifier,
app.bundleIdentifier != "com.apple.finder"
else { return }
var observer: AXObserver?
guard AXObserverCreate(app.processIdentifier, axCallback, &observer) == .success,
let observer
else { return }
axObserver = observer
axApp = AXUIElementCreateApplication(app.processIdentifier)
if let axApp {
addAX(observer, axApp, kAXFocusedWindowChangedNotification)
addAX(observer, axApp, kAXMainWindowChangedNotification)
addAX(observer, axApp, kAXWindowCreatedNotification)
}
attachAXWindows()
CFRunLoopAddSource(
CFRunLoopGetMain(),
AXObserverGetRunLoopSource(observer),
.defaultMode
)
}
private func refreshFullscreenState(resampleOnResume: Bool) {
let wasActive = fullscreenActive
fullscreenActive = pauseInFullscreen && hasFullscreenWindowVisible()
if fullscreenActive {
if sampling {
cancellationStatus = "Camera sampling paused for fullscreen; current output kept"
sampler.cancelCurrentReading()
}
if adaptiveEnabled {
statusText = "Camera sampling paused for fullscreen; current output kept"
}
} else if wasActive && resampleOnResume && adaptiveEnabled && !samplingPaused {
statusText = "Fullscreen ended; sampling resumed"
DispatchQueue.main.async { [weak self] in
self?.requestSample(manual: false)
}
}
}
private func applyAfterFullscreenDebounce() {
fullscreenDebounce?.cancel()
let work = DispatchWorkItem { [weak self] in
guard let self else { return }
self.fullscreenDebounce = nil
self.setupAXObserver()
self.attachAXWindows()
self.refreshFullscreenState(resampleOnResume: true)
self.updateUI()
}
fullscreenDebounce = work
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: work)
}
@objc private func frontAppChanged(_ notification: Notification) {
setupAXObserver()
attachAXWindows()
refreshFullscreenState(resampleOnResume: true)
updateUI()
applyAfterFullscreenDebounce()
}
func axEvent() {
attachAXWindows()
refreshFullscreenState(resampleOnResume: true)
updateUI()
applyAfterFullscreenDebounce()
}
func menuWillOpen(_ menu: NSMenu) {
if !captureBlockedByDisplayPower(updateStatus: true) {
_ = captureBlockedByMinimumBrightness(updateStatus: true)
}
refreshFullscreenState(resampleOnResume: false)
syncNightShiftOutput()
updateUI()
}
private func builtInDisplaySleepState() -> Bool? {
var asleep = false
guard PTTBuiltInDisplayIsAsleep(&asleep) else { return nil }
return asleep
}
@discardableResult
private func captureBlockedByDisplayPower(updateStatus: Bool) -> Bool {
let probedState = builtInDisplaySleepState()
if probedState != nil {
displayProbeFailureLogged = false
} else if !displayProbeFailureLogged {
displayProbeFailureLogged = true
AppLog.shared.write(
"Built-in display sleep-state probe unavailable; using workspace notifications"
)
}
if probedState == true {
if !displaySleeping || wakeSettling {
suspendCameraForPowerState(
reason: "Display power-state fallback",
systemSleep: false,
waitForStop: false
)
}
} else if probedState == false,
displaySleeping,
!systemSleeping,
!wakeSettling {
// Recover even if a screen-wake notification was missed.
beginWakeSettling(reason: "Display power-state fallback")
}
let blocked = systemSleeping || displaySleeping || wakeSettling || probedState == true
if blocked && updateStatus {
if systemSleeping {
statusText = "Mac sleeping • camera stopped"
} else if displaySleeping || probedState == true {
statusText = "Display off • camera stopped"
} else {
statusText = "Display wake settling • camera waiting"
}
}
return blocked
}
private func suspendCameraForPowerState(
reason: String,
systemSleep: Bool,
waitForStop: Bool
) {
wakeGeneration += 1
wakeSettling = false
if systemSleep {
systemSleeping = true
}
displaySleeping = true
fullscreenDebounce?.cancel()
cleanupAXObserver()
sampler.setCaptureAllowed(false)
cancellationStatus = sampling
? (systemSleep
? "Mac sleeping • camera sample cancelled"
: "Display off • camera sample cancelled")
: nil
if waitForStop {
sampler.cancelCurrentReadingAndWait()
} else {
sampler.cancelCurrentReading()
}
statusText = systemSleep
? "Mac sleeping • camera stopped"
: "Display off • camera stopped"
AppLog.shared.write("\(reason); camera gate closed and active sample cancelled")
updateUI()
}
private func beginWakeSettling(reason: String) {
guard !systemSleeping else {
statusText = "Mac waking • camera waiting"
updateUI()
return
}
wakeGeneration += 1
let generation = wakeGeneration
displaySleeping = false
wakeSettling = true
sampler.setCaptureAllowed(false)
smoothedMired = nil
lastAppliedCCT = nil
statusText = "Display wake settling • camera waiting"
AppLog.shared.write("\(reason); waiting \(wakeSettlingDelay)s before camera can resume")
updateUI()
DispatchQueue.main.asyncAfter(deadline: .now() + wakeSettlingDelay) { [weak self] in
guard let self,
self.wakeGeneration == generation,
!self.systemSleeping
else { return }
if self.builtInDisplaySleepState() == true {
self.suspendCameraForPowerState(
reason: "Display still asleep after wake-settling delay",
systemSleep: false,
waitForStop: false
)
return
}
self.sampler.cancelCurrentReading { [weak self] in
guard let self,
self.wakeGeneration == generation,
!self.systemSleeping
else { return }
if self.builtInDisplaySleepState() == true {
self.suspendCameraForPowerState(
reason: "Display slept again while wake was settling",
systemSleep: false,
waitForStop: false
)
return
}
self.sampling = false
self.cancellationStatus = nil
self.displaySleeping = false
self.wakeSettling = false
self.sampler.setCaptureAllowed(true)
self.setupAXObserver()
self.refreshFullscreenState(resampleOnResume: false)
if !self.adaptiveEnabled {
self.statusText = "Stopped"
} else if !self.nightShiftAvailable {
self.statusText = "Night Shift private API unavailable"
} else if self.samplingPaused {
self.statusText = "Camera sampling paused"
} else if self.fullscreenActive {
self.statusText = "Camera sampling paused for fullscreen"
} else {
self.statusText = "Display awake • sampling resumed"
self.requestSample(manual: false)
}
self.updateUI()
}
}
}
@discardableResult
private func captureBlockedByMinimumBrightness(updateStatus: Bool) -> Bool {
let wasBlocked = brightnessBlocked
var brightness: Float = 1.0
guard PTTBuiltInDisplayGetBrightness(&brightness) else {
brightnessBlocked = false
if wasBlocked && updateStatus {
statusText = adaptiveEnabled ? "Brightness read unavailable • capture allowed" : "Stopped"
}
return false
}
brightnessBlocked = brightness <= minimumVisibleBrightness
if brightnessBlocked {
if updateStatus {
statusText = "Screen brightness at minimum • camera skipped"
}
if !wasBlocked {
AppLog.shared.write(String(format: "Camera paused at built-in brightness %.4f", brightness))
}
} else if wasBlocked {
AppLog.shared.write(String(format: "Built-in brightness restored to %.4f", brightness))
if updateStatus {
if !adaptiveEnabled {
statusText = "Stopped"
} else if samplingPaused {
statusText = "Camera sampling paused"
} else if fullscreenActive {
statusText = "Camera sampling paused for fullscreen"
} else {
statusText = "Brightness restored • waiting to sample"
}
}
}
return brightnessBlocked
}
private func requestSample(manual: Bool) {
if captureBlockedByDisplayPower(updateStatus: true) {
updateUI()
return
}
if captureBlockedByMinimumBrightness(updateStatus: true) {
updateUI()
return
}
if pauseInFullscreen && hasFullscreenWindowVisible() {
fullscreenActive = true
statusText = "Camera sampling paused for fullscreen"
updateUI()
return
}
if !manual {
guard adaptiveEnabled, !samplingPaused else { return }
}
guard !sampling else { return }
guard nightShiftAvailable || manual else { return }
sampling = true
hadError = false
statusText = "Sampling camera…"
updateUI()
AppLog.shared.write("Camera sample started")
sampler.takeReading { [weak self] result in
guard let self else { return }
self.sampling = false
self.lastSampleDate = Date()
switch result {
case .success(let reading):
self.lastReading = reading
AppLog.shared.write(
String(
format: "AWB %.0fK tint %.1f MAD %.0f brightness %.3f neutral %.3f (%d values)",
reading.kelvin,
reading.tint,
reading.spread,
reading.brightness,
reading.neutralFraction,
reading.samples
) + " source=\(reading.source)"
)
if self.adaptiveEnabled && self.nightShiftAvailable {
self.apply(reading)
} else {
self.statusText = self.adaptiveEnabled ? "Measured; Night Shift API unavailable" : "Measured; paused"
}
case .failure(let error):
if error.localizedDescription == "Sampling paused." {
if !self.captureBlockedByDisplayPower(updateStatus: true) {
self.statusText = self.cancellationStatus ?? (
self.fullscreenActive
? "Camera sampling paused for fullscreen"
: "Camera sampling paused"
)
}
self.cancellationStatus = nil
} else {
self.statusText = error.localizedDescription
self.hadError = true
AppLog.shared.write("Camera sample failed: \(error.localizedDescription)")
}
}
self.updateUI()
}
}
private func apply(
_ reading: AmbientReading,
transitionSeconds: Float = 1.8,
force: Bool = false
) {
var liveState = PTTNightShiftState()
guard PTTNightShiftGetState(&liveState) else {
nightShiftAvailable = false
statusText = "Night Shift private API became unavailable"
hadError = true
return
}
if originalState == nil {
originalState = liveState
persistOriginalNightShift(liveState)
AppLog.shared.write(
String(format: "Saved Night Shift state enabled=%d active=%d strength=%.3f cct=%.0f",
liveState.enabled ? 1 : 0,
liveState.active ? 1 : 0,
liveState.strength,
liveState.cct)
)
}
let minCCT = Double(liveState.minCCT >= 1_000 ? liveState.minCCT : 2_850)
let maxCCT = Double(liveState.maxCCT > liveState.minCCT ? liveState.maxCCT : 6_500)
let ambient = min(max(reading.kelvin, minCCT), maxCCT)
let neutralMired = 1_000_000.0 / maxCCT
let warmMired = 1_000_000.0 / minCCT
let ambientMired = 1_000_000.0 / ambient
let adaptiveMired = neutralMired + (ambientMired - neutralMired) * response
let baseStrength = min(
max((adaptiveMired - neutralMired) / (warmMired - neutralMired), 0),
1
)
let desiredStrength = min(max(baseStrength + warmthOffset, 0), 1)
let desiredMired = neutralMired + desiredStrength * (warmMired - neutralMired)
if let previous = smoothedMired {
let alpha = reading.quality == "rough" ? 0.16 : 0.34
let candidate = previous + alpha * (desiredMired - previous)
let maximumStep = 48.0
smoothedMired = previous + min(max(candidate - previous, -maximumStep), maximumStep)
} else {
smoothedMired = desiredMired
}
guard let mired = smoothedMired else { return }
let target = min(max(1_000_000.0 / mired, minCCT), maxCCT)
if !force, let previous = lastAppliedCCT, abs(previous - target) < 35.0 {
statusText = "Stable • \(reading.menuQuality)"
return
}
var strength: Float = 0
if PTTNightShiftApplyCCT(Float(target), transitionSeconds, &strength) {
lastAppliedCCT = target
actualOutputStrength = Double(strength)
statusText = "Applied • \(reading.menuQuality)"
hadError = false
AppLog.shared.write(String(format: "Applied target %.0fK (Night Shift %.1f%%)", target, Double(strength) * 100.0))
} else {
statusText = "Night Shift rejected the temperature update"
hadError = true
AppLog.shared.write("Night Shift update failed")
}
}
private func persistOriginalNightShift(_ state: PTTNightShiftState) {
let encoded: [String: Any] = [
"active": state.active,
"enabled": state.enabled,
"mode": NSNumber(value: state.mode),
"strength": NSNumber(value: state.strength),
"cct": NSNumber(value: state.cct),
"minCCT": NSNumber(value: state.minCCT),
"maxCCT": NSNumber(value: state.maxCCT),
"midCCT": NSNumber(value: state.midCCT)
]
defaults.set(encoded, forKey: savedNightShiftKey)
defaults.set(true, forKey: nightShiftDirtyKey)
defaults.synchronize()
AppLog.shared.write("Persisted pre-control Night Shift state for crash recovery")
}
// nil: no interrupted session; true: recovered; false: recovery failed.
private func recoverInterruptedSessionIfNeeded() -> Bool? {
guard defaults.bool(forKey: nightShiftDirtyKey),
let encoded = defaults.dictionary(forKey: savedNightShiftKey)
else { return nil }
func number(_ key: String) -> NSNumber? { encoded[key] as? NSNumber }
guard let active = number("active"),
let enabled = number("enabled"),
let mode = number("mode"),
let strength = number("strength"),
let cct = number("cct"),
let minCCT = number("minCCT"),
let maxCCT = number("maxCCT"),
let midCCT = number("midCCT")
else {
AppLog.shared.write("Crash-recovery state was malformed; leaving marker intact")
return false
}
var saved = PTTNightShiftState()
saved.available = true
saved.active = active.boolValue
saved.enabled = enabled.boolValue
saved.mode = mode.int32Value
saved.strength = strength.floatValue
saved.cct = cct.floatValue
saved.minCCT = minCCT.floatValue
saved.maxCCT = maxCCT.floatValue
saved.midCCT = midCCT.floatValue
let restored = PTTNightShiftRestore(saved)
AppLog.shared.write("Crash-recovery Night Shift restore: \(restored)")
if restored { clearPersistedNightShift() }
return restored
}
private func clearPersistedNightShift() {
defaults.removeObject(forKey: savedNightShiftKey)
defaults.removeObject(forKey: nightShiftDirtyKey)
defaults.synchronize()
}
private func restoreOriginalNightShift(updateStatus: Bool = true) {
if let saved = originalState {
let ok = PTTNightShiftRestore(saved)
AppLog.shared.write("Restored original Night Shift state: \(ok)")
if !ok {
hadError = true
if updateStatus {
statusText = "Night Shift restore failed; saved for next launch"
updateUI()
}
return
}
}
clearPersistedNightShift()
originalState = nil
smoothedMired = nil
lastAppliedCCT = nil
if updateStatus {
statusText = adaptiveEnabled ? "Waiting for camera sample…" : "Stopped; original Night Shift restored"
syncNightShiftOutput()
updateUI()
}
}
private func syncNightShiftOutput() {
var state = PTTNightShiftState()
guard PTTNightShiftGetState(&state) else { return }
actualOutputStrength = (state.active || state.enabled) ? Double(state.strength) : 0
}
private func updateUI() {
enabledItem?.state = adaptiveEnabled ? .on : .off
samplingPauseItem?.state = samplingPaused ? .on : .off
fullscreenPauseItem?.state = pauseInFullscreen ? .on : .off
loggingItem?.state = loggingEnabled ? .on : .off
sampleNowItem?.isEnabled = !sampling
&& !systemSleeping
&& !displaySleeping
&& !wakeSettling
&& !brightnessBlocked
&& !(pauseInFullscreen && fullscreenActive)
launchItem?.state = SMAppService.mainApp.status == .enabled ? .on : .off
samplingIntervalControl?.selectedSegment = samplingIntervalSegmentIndex()
samplingIntervalTitleLabel?.stringValue = samplingIntervalTitleText()
responseControl?.selectedSegment = responseSegmentIndex()
responseTitleLabel?.stringValue = responseTitleText()
if !offsetSnapAnimating {
offsetSlider?.doubleValue = warmthOffset
}
offsetSliderValue?.stringValue = abs(warmthOffset) < 0.000_001
? "0%"
: String(format: "%+.0f%%", warmthOffset * 100.0)
outputSlider?.doubleValue = min(max(actualOutputStrength, 0), 1)
outputSliderValue?.stringValue = String(format: "%.0f%%", actualOutputStrength * 100.0)
statusLineItem?.title = "Status: \(statusText)"
if let reading = lastReading {
ambientLineItem?.title = String(
format: "Ambient: %.0f K • tint %+.0f",
reading.kelvin,
reading.tint
) + " • \(reading.menuQuality)"
let estimator = reading.source == "camera AWB"
? "Estimator: Camera AWB"
: "Estimator: Snapshot estimate"
sourceLineItem?.title = estimator + (reading.isColorfulScene ? " • colorful scene" : "")
} else {
ambientLineItem?.title = "Ambient: —"
sourceLineItem?.title = "Estimator: —"
}
if let target = lastAppliedCCT {
targetLineItem?.title = String(format: "Display target: %.0f K", target)
} else {
targetLineItem?.title = "Display target: —"
}
if let date = lastSampleDate {
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .short
sampledLineItem?.title = "Last sample: \(formatter.localizedString(for: date, relativeTo: Date()))"
} else {
sampledLineItem?.title = "Last sample: —"
}
let symbol: String
if hadError {
symbol = "exclamationmark.triangle.fill"
} else if !adaptiveEnabled {
symbol = "stop.circle.fill"
} else if samplingPaused
|| fullscreenActive
|| brightnessBlocked
|| systemSleeping
|| displaySleeping
|| wakeSettling {
symbol = "pause.circle.fill"
} else {
symbol = "moonphase.last.quarter"
}
let menuImage = NSImage(systemSymbolName: symbol, accessibilityDescription: appName)
?? (symbol == "moonphase.last.quarter" ? fallbackHalfMoonMenuImage() : nil)
if let image = menuImage {
image.isTemplate = true
statusItem?.button?.image = image
statusItem?.button?.title = ""
} else {
statusItem?.button?.image = nil
statusItem?.button?.title = "PT"
}
statusItem?.button?.toolTip = "\(appName): \(statusText)"
}
private func showWelcomeIfNeeded() {
guard !defaults.bool(forKey: "didShowWelcome") else { return }
defaults.set(true, forKey: "didShowWelcome")
NSApp.activate(ignoringOtherApps: true)
let alert = NSAlert()
alert.messageText = "Pseudo True Tone is ready"
alert.informativeText = "At the selected interval (2 minutes by default), the app turns on the built-in camera briefly, makes a read-only color-temperature estimate, smooths it, and changes only Night Shift through CoreBrightness. It skips the camera while the built-in display is asleep or while its user-brightness slider is at zero. It never changes camera settings, and no image is saved or transmitted. The green camera indicator will appear for each reading.\n\nPause camera sampling keeps the current output. Stopping adaptive warmth or quitting restores the original Night Shift state. Logging is off by default."
alert.addButton(withTitle: "Continue")
alert.addButton(withTitle: "Quit")
if alert.runModal() == .alertSecondButtonReturn {
NSApp.terminate(nil)
}
}
@objc private func toggleEnabled() {
adaptiveEnabled.toggle()
defaults.set(adaptiveEnabled, forKey: "adaptiveEnabled")
if adaptiveEnabled {
hadError = false
if fullscreenActive {
statusText = "Camera sampling paused for fullscreen"
} else if samplingPaused {
statusText = "Camera sampling paused"
} else {
statusText = "Waiting for camera sample…"
requestSample(manual: false)
}
} else {
cancellationStatus = "Stopped; original Night Shift restored"
sampler.cancelCurrentReading()
restoreOriginalNightShift()
}
updateUI()
}
@objc private func toggleSamplingPause() {
samplingPaused.toggle()
defaults.set(samplingPaused, forKey: "samplingPaused")
if samplingPaused {
cancellationStatus = "Camera sampling paused; current output kept"
sampler.cancelCurrentReading()
statusText = "Camera sampling paused; current output kept"
} else if fullscreenActive {
statusText = "Camera sampling paused for fullscreen"
} else if adaptiveEnabled {
statusText = "Camera sampling resumed"
requestSample(manual: false)
} else {
statusText = "Stopped"
}
updateUI()
}
@objc private func toggleFullscreenPause() {
pauseInFullscreen.toggle()
defaults.set(pauseInFullscreen, forKey: "pauseInFullscreen")
setupAXObserver()
refreshFullscreenState(resampleOnResume: true)
updateUI()
}
@objc private func toggleLogging() {
if loggingEnabled {
AppLog.shared.write("Logging disabled by user")
loggingEnabled = false
AppLog.shared.setEnabled(false)
} else {
loggingEnabled = true
AppLog.shared.setEnabled(true)
AppLog.shared.write("Logging enabled by user")
}
defaults.set(loggingEnabled, forKey: "loggingEnabled")
updateUI()
}
@objc private func sampleNow() {
requestSample(manual: true)
}
private func commitWarmthOffset(_ value: Double) {
warmthOffset = min(max(value, -0.35), 0.35)
defaults.set(warmthOffset, forKey: "warmthOffset")
smoothedMired = nil
lastAppliedCCT = nil
if adaptiveEnabled, let reading = lastReading {
apply(reading, transitionSeconds: 0, force: true)
}
}
private func snapOffsetToZero(from start: Double) {
offsetSnapAnimationID += 1
let animationID = offsetSnapAnimationID
offsetSnapAnimating = true
commitWarmthOffset(0)
updateUI()
let steps = 7
let duration = 0.12
for step in 1...steps {
DispatchQueue.main.asyncAfter(
deadline: .now() + duration * Double(step) / Double(steps)
) { [weak self] in
guard let self, self.offsetSnapAnimationID == animationID else { return }
let progress = Double(step) / Double(steps)
let remaining = pow(1.0 - progress, 3.0)
self.offsetSlider.doubleValue = start * remaining
if step == steps {
self.offsetSnapAnimating = false
self.offsetSlider.doubleValue = 0
self.updateUI()
}
}
}
}
@objc private func offsetChanged(_ sender: NSSlider) {
let rawValue = min(max(sender.doubleValue, -0.35), 0.35)
if NSApp.currentEvent?.type == .leftMouseUp,
abs(rawValue) <= 0.02,
rawValue != 0 {
snapOffsetToZero(from: rawValue)
return
}
offsetSnapAnimationID += 1
offsetSnapAnimating = false
commitWarmthOffset(abs(rawValue) < 0.000_001 ? 0 : rawValue)
updateUI()
}
@objc private func outputChanged(_ sender: NSSlider) {
let requested = min(max(sender.doubleValue, 0), 1)
// Treat this exactly as a manual Night Shift takeover: stop adaptive
// control without restoring the pre-control value, then apply the
// user's requested output and forget our recovery snapshot.
adaptiveEnabled = false
defaults.set(false, forKey: "adaptiveEnabled")
cancellationStatus = "Stopped • manual Night Shift output"
sampler.cancelCurrentReading()
originalState = nil
clearPersistedNightShift()
smoothedMired = nil
lastAppliedCCT = nil
let applied = PTTNightShiftApplyStrength(Float(requested), 0)
actualOutputStrength = requested
hadError = !applied
statusText = applied
? "Stopped • manual Night Shift output"
: "Manual Night Shift adjustment failed"
updateUI()
}
@objc private func samplingIntervalChanged(_ sender: NSSegmentedControl) {
let index = sender.selectedSegment
guard index >= 0, index < samplingIntervalOptions.count else { return }
samplingInterval = samplingIntervalOptions[index]
defaults.set(samplingInterval, forKey: "samplingInterval")
scheduleSamplingTimer()
AppLog.shared.write("Sampling interval changed to \(Int(samplingInterval))s")
updateUI()
}
@objc private func responseChanged(_ sender: NSSegmentedControl) {
let values = [0.70, 0.85, 1.00]
guard sender.selectedSegment >= 0, sender.selectedSegment < values.count else { return }
let value = values[sender.selectedSegment]
response = value
defaults.set(value, forKey: "response")
smoothedMired = nil
lastAppliedCCT = nil
if adaptiveEnabled, let reading = lastReading {
statusText = "Response changed"
apply(reading, transitionSeconds: 0, force: true)
} else {
statusText = adaptiveEnabled ? "Response changed; waiting…" : "Stopped"
}
updateUI()
}
@objc private func toggleLaunchAtLogin() {
do {
if SMAppService.mainApp.status == .enabled {
try SMAppService.mainApp.unregister()
} else {
try SMAppService.mainApp.register()
}
} catch {
NSApp.activate(ignoringOtherApps: true)
let alert = NSAlert(error: error)
alert.messageText = "Could not change Launch at Login"
alert.informativeText = "Move the app to /Applications and try again.\n\n\(error.localizedDescription)"
alert.runModal()
AppLog.shared.write("Launch-at-login error: \(error.localizedDescription)")
}
updateUI()
}
@objc private func openCameraSettings() {
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Camera") {
NSWorkspace.shared.open(url)
}
}
@objc private func revealLog() {
if !FileManager.default.fileExists(atPath: AppLog.shared.url.path) {
FileManager.default.createFile(atPath: AppLog.shared.url.path, contents: nil)
}
NSWorkspace.shared.activateFileViewerSelecting([AppLog.shared.url])
}
@objc private func showAbout() {
NSApp.activate(ignoringOtherApps: true)
let alert = NSAlert()
alert.messageText = "Pseudo True Tone 0.1.12"
alert.informativeText = "Read-only camera estimate → smoothed ambient CCT → private CoreBrightness Night Shift control.\n\nSampling interval: selectable (20s, 1m, 2m, 5m, 10m)\nCamera while built-in display sleeps: blocked\nDisplay-state fallback: public CoreGraphics power-state probe\nCamera at minimum built-in brightness: skipped\nCamera before system sleep: explicitly cancelled\nWake settling delay: 2.0 seconds\nCamera settings changed: none\nImages stored or transmitted: none\nOther display settings changed: none\nLogging: opt-in, capped near 5 MB\n\nThis is an approximate comfort feature, not a calibrated colorimeter. Colored scenes, mixed lighting, darkness, camera auto-white-balance, and the display's own light can bias the estimate. Because the brightness and CoreBrightness calls are private, a future macOS update may break those checks."
alert.addButton(withTitle: "OK")
alert.runModal()
}
@objc private func willSleep(_ notification: Notification) {
suspendCameraForPowerState(
reason: "Workspace will sleep",
systemSleep: true,
waitForStop: true
)
}
@objc private func didWake(_ notification: Notification) {
systemSleeping = false
if builtInDisplaySleepState() == true {
displaySleeping = true
wakeSettling = false
sampler.setCaptureAllowed(false)
statusText = "Display off • camera stopped"
AppLog.shared.write("Workspace woke while built-in display remained asleep")
updateUI()
return
}
displaySleeping = false
beginWakeSettling(reason: "Workspace did wake")
}
@objc private func screensDidSleep(_ notification: Notification) {
suspendCameraForPowerState(
reason: "Workspace screens did sleep",
systemSleep: false,
waitForStop: false
)
}
@objc private func screensDidWake(_ notification: Notification) {
displaySleeping = false
if systemSleeping {
statusText = "Mac waking • camera waiting"
updateUI()
return
}
beginWakeSettling(reason: "Workspace screens did wake")
}
@objc private func quit() {
NSApp.terminate(nil)
}
}
let application = NSApplication.shared
private let applicationDelegate = AppDelegate()
application.delegate = applicationDelegate
_ = application.setActivationPolicy(.accessory)
application.run()
SWIFT
cat > "$STAGED_APP/Contents/Info.plist" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDisplayName</key>
<string>${APP_NAME}</string>
<key>CFBundleExecutable</key>
<string>${APP_NAME}</string>
<key>CFBundleIconFile</key>
<string>AppIcon</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${APP_NAME}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>${VERSION}</string>
<key>CFBundleVersion</key>
<string>12</string>
<key>LSMinimumSystemVersion</key>
<string>13.0</string>
<key>LSUIElement</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>Pseudo True Tone briefly samples the built-in camera at your selected interval to estimate ambient light color temperature. Images are never saved or transmitted.</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>
PLIST
echo "Compiling Objective-C CoreBrightness bridge…"
set +x
FINAL_MSG="Objective-C bridge compilation failed."
set -x
xcrun clang \
-fobjc-arc \
-fblocks \
-fmodules \
-mmacosx-version-min=13.0 \
-c "$BUILD_DIR/PTTBridge.m" \
-o "$BUILD_DIR/PTTBridge.o"
echo "Compiling Swift menu-bar application…"
set +x
FINAL_MSG="Swift application compilation failed."
set -x
xcrun swiftc \
-O \
-whole-module-optimization \
-swift-version 5 \
-target "$(uname -m)-apple-macosx13.0" \
-import-objc-header "$BUILD_DIR/PTTBridge.h" \
"$BUILD_DIR/main.swift" \
"$BUILD_DIR/PTTBridge.o" \
-framework AppKit \
-framework AVFoundation \
-framework CoreMedia \
-framework CoreVideo \
-framework CoreGraphics \
-framework Foundation \
-framework ServiceManagement \
-framework ApplicationServices \
-Xlinker -dead_strip \
-o "$STAGED_APP/Contents/MacOS/$APP_NAME" &
APP_COMPILE_PID=$!
###### BUILD APP ICON IN PARALLEL ######
BASE_ICON="$STAGED_APP/Contents/Resources/moon.png"
ICONSET="$STAGED_APP/Contents/Resources/AppIcon.iconset"
mkdir -p "$ICONSET"
xcrun swift - "$BASE_ICON" <<'SWIFT' &
import AppKit
let output = CommandLine.arguments[1]
let image = NSImage(size: NSSize(width: 1024, height: 1024))
image.lockFocus()
NSColor.clear.setFill()
NSRect(x: 0, y: 0, width: 1024, height: 1024).fill()
let moon = "🌗" as NSString
let attributes: [NSAttributedString.Key: Any] = [
.font: NSFont(name: "Apple Color Emoji", size: 820)
?? NSFont.systemFont(ofSize: 820)
]
let size = moon.size(withAttributes: attributes)
moon.draw(
at: NSPoint(
x: (1024 - size.width) / 2,
y: (1024 - size.height) / 2 - 40
),
withAttributes: attributes
)
image.unlockFocus()
let representation = NSBitmapImageRep(data: image.tiffRepresentation!)!
let png = representation.representation(using: .png, properties: [:])!
try png.write(to: URL(fileURLWithPath: output))
SWIFT
ICON_RENDER_PID=$!
ICON_RENDER_STATUS=0
wait "$ICON_RENDER_PID" || ICON_RENDER_STATUS=1
if [[ "$ICON_RENDER_STATUS" -ne 0 ]]; then
FINAL_MSG="Icon render failed."
wait "$APP_COMPILE_PID" || true
exit 1
fi
SIPS_PIDS=()
for size in 16 32 128 256 512; do
sips -z "$size" "$size" "$BASE_ICON" \
--out "$ICONSET/icon_${size}x${size}.png" >/dev/null &
SIPS_PIDS+=("$!")
doubled=$((size * 2))
sips -z "$doubled" "$doubled" "$BASE_ICON" \
--out "$ICONSET/icon_${size}x${size}@2x.png" >/dev/null &
SIPS_PIDS+=("$!")
done
SIPS_STATUS=0
for pid in "${SIPS_PIDS[@]}"; do
wait "$pid" || SIPS_STATUS=1
done
if [[ "$SIPS_STATUS" -ne 0 ]]; then
FINAL_MSG="Icon resize failed."
wait "$APP_COMPILE_PID" || true
exit 1
fi
set +x
FINAL_MSG="Icon packaging failed."
set -x
iconutil -c icns "$ICONSET" -o "$STAGED_APP/Contents/Resources/AppIcon.icns"
rm -rf "$BASE_ICON" "$ICONSET"
########################################
APP_COMPILE_STATUS=0
wait "$APP_COMPILE_PID" || APP_COMPILE_STATUS=1
if [[ "$APP_COMPILE_STATUS" -ne 0 ]]; then
FINAL_MSG="Swift application compilation failed."
exit 1
fi
chmod 755 "$STAGED_APP/Contents/MacOS/$APP_NAME"
plutil -lint "$STAGED_APP/Contents/Info.plist"
find "$STAGED_APP"
# Publish only after every build and icon step succeeded.
if [[ -e "$APP_PATH" ]]; then
if [[ "$FORCE" -eq 1 ]]; then
rm -rf "$APP_PATH"
else
FINAL_MSG="Error: '$APP_PATH' appeared while building."
exit 1
fi
fi
set +x
FINAL_MSG="Could not publish the completed app."
set -x
mv "$STAGED_APP" "$APP_PATH"
echo
echo "Built: $APP_PATH"
echo "Move it to /Applications for reliable Launch at Login behavior."
echo "On first launch, macOS will ask for Camera permission."
open -R "$APP_PATH"
FINAL_MSG="App created: $APP_PATH"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment