Update ddc control toggle

This commit is contained in:
Hariharan Mudaliar 2026-03-15 12:14:32 +05:30
commit 8abd21de31
No known key found for this signature in database
GPG key ID: 025583EB0BE5640E
7 changed files with 305 additions and 261 deletions

View file

@ -115,6 +115,10 @@ final class BetterDisplayManager: ObservableObject {
private var cancellables = Set<AnyCancellable>()
private weak var coordinator: DynamicIslandViewCoordinator?
private var isBetterDisplayIntegrationEnabled: Bool {
Defaults[.enableThirdPartyDDCIntegration] && Defaults[.thirdPartyDDCProvider] == .betterDisplay
}
private init() {
isDetected = Self.checkInstallation()
isRunning = Self.checkRunning()
@ -128,11 +132,7 @@ final class BetterDisplayManager: ObservableObject {
/// Configure with the view coordinator for HUD dispatch.
func configure(coordinator: DynamicIslandViewCoordinator) {
self.coordinator = coordinator
// Start listening if integration is enabled and BetterDisplay is running
if Defaults[.enableBetterDisplayIntegration] && isRunning {
startListening()
}
refreshListeningState()
}
/// Refresh detection status (e.g. after app install/uninstall).
@ -141,11 +141,13 @@ final class BetterDisplayManager: ObservableObject {
let wasRunning = isRunning
isDetected = Self.checkInstallation()
isRunning = Self.checkRunning()
if wasRunning && !isRunning {
stopListening()
} else if !wasRunning && isRunning && Defaults[.enableBetterDisplayIntegration] {
startListening()
if wasDetected != isDetected {
NSLog("📺 BetterDisplay detection changed: detected=\(isDetected)")
}
if wasRunning != isRunning {
NSLog("📺 BetterDisplay running state changed: running=\(isRunning)")
}
refreshListeningState()
}
// MARK: - Detection
@ -199,7 +201,7 @@ final class BetterDisplayManager: ObservableObject {
// MARK: - OSD Handling
private func handleOSDNotification(_ notification: Notification) {
guard Defaults[.enableBetterDisplayIntegration], isRunning else { return }
guard isBetterDisplayIntegrationEnabled, isRunning else { return }
guard let notificationString = notification.object as? String else {
NSLog("⚠️ BetterDisplay OSD: unexpected notification format")
@ -213,6 +215,16 @@ final class BetterDisplayManager: ObservableObject {
BetterDisplayOSDNotification.self,
from: Data(notificationString.utf8)
)
let displayIDText = osd.displayID.map { String($0) } ?? "nil"
let targetText = osd.controlTarget ?? "nil"
let iconIDText = osd.systemIconID.map { String($0) } ?? "nil"
let valueText = osd.value.map { String($0) } ?? "nil"
let maxValueText = osd.maxValue.map { String($0) } ?? "nil"
let symbolText = osd.customSymbol ?? "nil"
let textValue = osd.text ?? "nil"
NSLog(
"📺 BetterDisplay decoded payload: displayID=\(displayIDText) target=\(targetText) iconID=\(iconIDText) value=\(valueText) maxValue=\(maxValueText) customSymbol=\(symbolText) text=\(textValue)"
)
routeOSDToHUD(osd)
} catch {
NSLog("⚠️ BetterDisplay OSD decode error: \(error.localizedDescription)")
@ -225,8 +237,18 @@ final class BetterDisplayManager: ObservableObject {
let normalizedValue = normalizeValue(osd.value, maxValue: osd.maxValue)
let targetScreen = resolveScreen(for: osd.displayID)
let isExternalDisplay = isExternal(displayID: osd.displayID, resolvedScreen: targetScreen)
let inferredMute = osd.controlTarget == "mute" || osd.systemIconID == 4
let hasVolumeData = category == .volume && osd.value != nil
let targetText = osd.controlTarget ?? "nil"
let displayIDText = osd.displayID.map { String($0) } ?? "nil"
let resolvedScreenText = targetScreen?.localizedName ?? "nil"
let valueText = osd.value.map { String($0) } ?? "nil"
let maxValueText = osd.maxValue.map { String($0) } ?? "nil"
let normalizedText = String(format: "%.3f", normalizedValue)
NSLog("📺 BetterDisplay OSD: target=\(osd.controlTarget ?? "nil") displayID=\(osd.displayID.map(String.init) ?? "nil") resolvedScreen=\(targetScreen?.localizedName ?? "nil") isExternal=\(isExternalDisplay) value=\(osd.value ?? -1)")
NSLog(
"📺 BetterDisplay routed payload: category=\(categoryName(category)) target=\(targetText) displayID=\(displayIDText) resolvedScreen=\(resolvedScreenText) isExternal=\(isExternalDisplay) rawValue=\(valueText) maxValue=\(maxValueText) normalized=\(normalizedText) hasVolumeData=\(hasVolumeData) inferredMute=\(inferredMute)"
)
switch category {
case .brightness:
@ -365,6 +387,17 @@ final class BetterDisplayManager: ObservableObject {
}
}
private func categoryName(_ category: BetterDisplayControlCategory) -> String {
switch category {
case .brightness:
return "brightness"
case .volume:
return "volume"
case .other:
return "other"
}
}
/// Normalize a BetterDisplay value (0...maxValue) to 0...1 range.
private func normalizeValue(_ value: Double?, maxValue: Double?) -> CGFloat {
guard let value else { return 0 }
@ -488,7 +521,7 @@ final class BetterDisplayManager: ObservableObject {
Task { @MainActor in
NSLog("🔴 BetterDisplay terminated (workspace notification)")
self?.isRunning = false
self?.stopListening()
self?.refreshListeningState()
}
}
}
@ -507,9 +540,7 @@ final class BetterDisplayManager: ObservableObject {
NSLog("🟢 BetterDisplay launched notification received")
self?.isDetected = true
self?.isRunning = true
if Defaults[.enableBetterDisplayIntegration] {
self?.startListening()
}
self?.refreshListeningState()
}
}
@ -521,23 +552,34 @@ final class BetterDisplayManager: ObservableObject {
Task { @MainActor in
NSLog("🔴 BetterDisplay terminated notification received")
self?.isRunning = false
self?.stopListening()
self?.refreshListeningState()
}
}
}
// MARK: - Settings Observer
private func refreshListeningState() {
if isBetterDisplayIntegrationEnabled && isRunning {
startListening()
} else {
stopListening()
}
}
private func setupSettingsObserver() {
Defaults.publisher(.enableBetterDisplayIntegration, options: [])
.sink { [weak self] change in
Defaults.publisher(.enableThirdPartyDDCIntegration, options: [])
.sink { [weak self] _ in
Task { @MainActor in
guard let self else { return }
if change.newValue && self.isRunning {
self.startListening()
} else {
self.stopListening()
}
self?.refreshListeningState()
}
}
.store(in: &cancellables)
Defaults.publisher(.thirdPartyDDCProvider, options: [])
.sink { [weak self] _ in
Task { @MainActor in
self?.refreshListeningState()
}
}
.store(in: &cancellables)

View file

@ -87,6 +87,10 @@ final class LunarManager: ObservableObject {
private static let maxReconnectAttempts = 5
private static let baseReconnectDelay: UInt64 = 2_000_000_000 // 2 seconds
private var isLunarIntegrationEnabled: Bool {
Defaults[.enableThirdPartyDDCIntegration] && Defaults[.thirdPartyDDCProvider] == .lunar
}
private init() {
isDetected = Self.checkInstallation()
isRunning = Self.checkRunning()
@ -99,10 +103,7 @@ final class LunarManager: ObservableObject {
/// Configure with the view coordinator for HUD dispatch.
func configure(coordinator: DynamicIslandViewCoordinator) {
self.coordinator = coordinator
if Defaults[.enableLunarIntegration] && isRunning {
connectToLunar()
}
refreshConnectionState()
}
/// Called when Atoll is about to quit so Lunar's native OSD is restored.
@ -115,11 +116,10 @@ final class LunarManager: ObservableObject {
let wasRunning = isRunning
isDetected = Self.checkInstallation()
isRunning = Self.checkRunning()
if wasRunning && !isRunning {
disconnect()
} else if !wasRunning && isRunning && Defaults[.enableLunarIntegration] {
connectToLunar()
if wasRunning != isRunning {
NSLog("🌙 Lunar running state changed: running=\(isRunning)")
}
refreshConnectionState()
}
// MARK: - Detection
@ -267,11 +267,11 @@ final class LunarManager: ObservableObject {
}
private func handleReceivedData(_ data: Data) {
guard Defaults[.enableLunarIntegration], isRunning else { return }
guard isLunarIntegrationEnabled, isRunning else { return }
do {
let displayData = try JSONDecoder().decode(LunarDisplayData.self, from: data)
NSLog("🌙 Lunar: display=\(displayData.display) brightness=\(displayData.brightness ?? -1) contrast=\(displayData.contrast ?? -1) volume=\(displayData.volume ?? -1) mute=\(displayData.mute.map(String.init) ?? "nil")")
logDecodedPayload(displayData, source: "socket")
routeToHUD(displayData)
} catch {
// Lunar may send multi-line JSON or partial data; try splitting by newlines
@ -280,7 +280,7 @@ final class LunarManager: ObservableObject {
for line in lines {
guard let lineData = line.data(using: .utf8) else { continue }
if let displayData = try? JSONDecoder().decode(LunarDisplayData.self, from: lineData) {
NSLog("🌙 Lunar: display=\(displayData.display) brightness=\(displayData.brightness ?? -1) contrast=\(displayData.contrast ?? -1) volume=\(displayData.volume ?? -1)")
logDecodedPayload(displayData, source: "socket-line")
routeToHUD(displayData)
}
}
@ -292,6 +292,16 @@ final class LunarManager: ObservableObject {
private func routeToHUD(_ data: LunarDisplayData) {
let targetScreen = resolveScreen(for: data.display)
let isExternal = isExternalDisplay(data.display, resolvedScreen: targetScreen)
let hasVolumeData = data.volume != nil || data.mute != nil
let resolvedScreenText = targetScreen?.localizedName ?? "nil"
let brightnessText = data.brightness.map { String($0) } ?? "nil"
let contrastText = data.contrast.map { String($0) } ?? "nil"
let volumeText = data.volume.map { String($0) } ?? "nil"
let muteText = data.mute.map { String($0) } ?? "nil"
NSLog(
"🌙 Lunar routed payload: display=\(data.display) resolvedScreen=\(resolvedScreenText) isExternal=\(isExternal) brightness=\(brightnessText) contrast=\(contrastText) volume=\(volumeText) mute=\(muteText) hasVolumeData=\(hasVolumeData)"
)
// Nits-only events are informational (no user-facing HUD needed).
let hasActionableChange = data.brightness != nil || data.contrast != nil || data.volume != nil || data.mute != nil
@ -418,10 +428,21 @@ final class LunarManager: ObservableObject {
return num.uint32Value
}
private func logDecodedPayload(_ data: LunarDisplayData, source: String) {
let hasVolumeData = data.volume != nil || data.mute != nil
let brightnessText = data.brightness.map { String($0) } ?? "nil"
let contrastText = data.contrast.map { String($0) } ?? "nil"
let volumeText = data.volume.map { String($0) } ?? "nil"
let muteText = data.mute.map { String($0) } ?? "nil"
NSLog(
"🌙 Lunar decoded payload (\(source)): display=\(data.display) brightness=\(brightnessText) contrast=\(contrastText) volume=\(volumeText) mute=\(muteText) hasVolumeData=\(hasVolumeData)"
)
}
// MARK: - Reconnection
private func scheduleReconnect() {
guard Defaults[.enableLunarIntegration], isRunning else { return }
guard isLunarIntegrationEnabled, isRunning else { return }
guard reconnectAttempts < Self.maxReconnectAttempts else {
NSLog("🌙 Lunar: max reconnect attempts reached, giving up")
return
@ -435,7 +456,7 @@ final class LunarManager: ObservableObject {
reconnectTask = Task { @MainActor in
try? await Task.sleep(nanoseconds: delay)
guard !Task.isCancelled, self.isRunning, Defaults[.enableLunarIntegration] else { return }
guard !Task.isCancelled, self.isRunning, self.isLunarIntegrationEnabled else { return }
NSLog("🌙 Lunar: reconnect attempt \(attempt + 1)/\(Self.maxReconnectAttempts)")
self.connectToLunar()
}
@ -459,7 +480,7 @@ final class LunarManager: ObservableObject {
self?.isDetected = true
self?.isRunning = true
self?.reconnectAttempts = 0
if Defaults[.enableLunarIntegration] {
if self?.isLunarIntegrationEnabled == true {
// Give Lunar a moment to start its socket server
try? await Task.sleep(nanoseconds: 1_500_000_000)
self?.connectToLunar()
@ -485,16 +506,27 @@ final class LunarManager: ObservableObject {
// MARK: - Settings Observer
private func refreshConnectionState() {
if isLunarIntegrationEnabled && isRunning {
connectToLunar()
} else {
disconnect()
}
}
private func setupSettingsObserver() {
Defaults.publisher(.enableLunarIntegration, options: [])
.sink { [weak self] change in
Defaults.publisher(.enableThirdPartyDDCIntegration, options: [])
.sink { [weak self] _ in
Task { @MainActor in
guard let self else { return }
if change.newValue && self.isRunning {
self.connectToLunar()
} else {
self.disconnect()
}
self?.refreshConnectionState()
}
}
.store(in: &cancellables)
Defaults.publisher(.thirdPartyDDCProvider, options: [])
.sink { [weak self] _ in
Task { @MainActor in
self?.refreshConnectionState()
}
}
.store(in: &cancellables)

View file

@ -181,16 +181,15 @@ class SystemHUDManager {
)
}.store(in: &cancellables)
// When BetterDisplay integration toggled, restart observer to update Cmd+Brightness key interception
Defaults.publisher(.enableBetterDisplayIntegration, options: []).sink { [weak self] _ in
// Restart observer when third-party DDC integration state changes.
Defaults.publisher(.enableThirdPartyDDCIntegration, options: []).sink { [weak self] _ in
guard let self = self, self.isSetupComplete else { return }
Task { @MainActor in
await self.startSystemObserver()
}
}.store(in: &cancellables)
// When Lunar integration toggled, restart observer to update brightness key interception
Defaults.publisher(.enableLunarIntegration, options: []).sink { [weak self] _ in
Defaults.publisher(.thirdPartyDDCProvider, options: []).sink { [weak self] _ in
guard let self = self, self.isSetupComplete else { return }
Task { @MainActor in
await self.startSystemObserver()
@ -248,9 +247,9 @@ class SystemHUDManager {
keyboardBacklightEnabled = Defaults[.enableKeyboardBacklightHUD]
}
// When BetterDisplay or Lunar integration is on, stop intercepting brightness and
// When third-party DDC integration is on, stop intercepting brightness and
// Cmd+Brightness keys so the external app receives them and sends DDC/OSD events.
if Defaults[.enableBetterDisplayIntegration] || Defaults[.enableLunarIntegration] {
if Defaults[.enableThirdPartyDDCIntegration] {
brightnessEnabled = false
keyboardBacklightEnabled = false
}
@ -281,7 +280,7 @@ class SystemHUDManager {
// Force disable system HUD to ensure no duplicates
SystemOSDManager.disableSystemHUD()
print("System observer started (HUD: \(Defaults[.enableSystemHUD]), OSD: \(Defaults[.enableCustomOSD]), Vertical: \(Defaults[.enableVerticalHUD]), BD: \(Defaults[.enableBetterDisplayIntegration]))")
print("System observer started (HUD: \(Defaults[.enableSystemHUD]), OSD: \(Defaults[.enableCustomOSD]), Vertical: \(Defaults[.enableVerticalHUD]), ThirdPartyDDC: \(Defaults[.enableThirdPartyDDCIntegration]), Provider: \(Defaults[.thirdPartyDDCProvider].displayName))")
isSystemOperationInProgress = false
}