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

@ -400,6 +400,19 @@ class AppDelegate: NSObject, NSApplicationDelegate {
Defaults.Keys.migrateMusicAuxControls() Defaults.Keys.migrateMusicAuxControls()
Defaults.Keys.migrateMusicControlSlots() Defaults.Keys.migrateMusicControlSlots()
Defaults.Keys.migrateCapsLockTintMode() Defaults.Keys.migrateCapsLockTintMode()
Defaults.Keys.migrateThirdPartyDDCIntegration()
Defaults.publisher(.enableThirdPartyDDCIntegration, options: [])
.sink { _ in
Defaults.Keys.syncLegacyThirdPartyDDCKeys()
}
.store(in: &cancellables)
Defaults.publisher(.thirdPartyDDCProvider, options: [])
.sink { _ in
Defaults.Keys.syncLegacyThirdPartyDDCKeys()
}
.store(in: &cancellables)
// Initialize idle animations (load bundled + built-in face) // Initialize idle animations (load bundled + built-in face)
idleAnimationManager.initializeDefaultAnimations() idleAnimationManager.initializeDefaultAnimations()

View file

@ -4111,6 +4111,10 @@
} }
} }
}, },
"Atoll listens to the selected provider and renders its brightness/volume HUD updates using your currently active HUD style above." : {
"comment" : "A footer text that explains the functionality of the \"Enable third-party DDC app integration\" toggle.",
"isCommentAutoGenerated" : true
},
"Atoll needs Full Disk Access to read the system notification database. If Atoll does not appear in the list, use 'Reveal App in Finder' and drag it into the settings window." : { "Atoll needs Full Disk Access to read the system notification database. If Atoll does not appear in the list, use 'Reveal App in Finder' and drag it into the settings window." : {
"extractionState" : "stale", "extractionState" : "stale",
"localizations" : { "localizations" : {
@ -4919,16 +4923,6 @@
"comment" : "A label indicating a beta version.", "comment" : "A label indicating a beta version.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
"shouldTranslate" : false "shouldTranslate" : false
},
"BetterDisplay" : {
"comment" : "The name of the BetterDisplay integration option.",
"isCommentAutoGenerated" : true
},
"BetterDisplay is installed but not currently running. Launch BetterDisplay to enable integration." : {
},
"BetterDisplay OSD events will be routed through Atoll's active HUD style. Make sure BetterDisplay's OSD integration is enabled in its Settings › Application › Integration." : {
}, },
"Blue dashed box shows actual size in Atoll" : { "Blue dashed box shows actual size in Atoll" : {
"extractionState" : "stale", "extractionState" : "stale",
@ -7295,10 +7289,6 @@
} }
} }
}, },
"Connected to Lunar's DDC socket. Brightness, contrast, and volume adjustments will be shown through Atoll's HUD." : {
"comment" : "A description of what happens when the user enables the Lunar integration in the settings.",
"isCommentAutoGenerated" : true
},
"Connects directly to the Apple Music app." : { "Connects directly to the Apple Music app." : {
}, },
@ -10575,6 +10565,10 @@
} }
} }
}, },
"Enable third-party DDC app integration" : {
"comment" : "A toggle switch that enables or disables integration with a third-party Dynamic Display Control (DDC) app.",
"isCommentAutoGenerated" : true
},
"Enable third-party extensions" : { "Enable third-party extensions" : {
"comment" : "Toggle to enable or disable third-party extensions.", "comment" : "Toggle to enable or disable third-party extensions.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@ -10595,12 +10589,8 @@
} }
} }
}, },
"Enable to route BetterDisplay's brightness and volume changes through Atoll's HUD instead of the system OSD." : { "Enable to route BetterDisplay or Lunar display adjustments through Atoll's active HUD style." : {
"comment" : "A description of the benefit of enabling BetterDisplay integration.", "comment" : "A description of the feature that allows third-party display integrations to be enabled.",
"isCommentAutoGenerated" : true
},
"Enable to route Lunar's DDC brightness, contrast, and volume changes through Atoll's HUD." : {
"comment" : "A description of the functionality of enabling the integration with Lunar.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Enable when scaling beyond 30x20px so the Atoll opens gracefully." : { "Enable when scaling beyond 30x20px so the Atoll opens gracefully." : {
@ -13484,14 +13474,6 @@
}, },
"Input" : { "Input" : {
},
"Install [BetterDisplay](https://betterdisplay.pro) to control external display brightness and volume through Atoll's HUD." : {
"comment" : "A description of the benefits of installing BetterDisplay.",
"isCommentAutoGenerated" : true
},
"Install [Lunar](https://lunar.fyi) to control external display brightness, contrast, and volume through Atoll's HUD via DDC." : {
"comment" : "A description of how to install Lunar on a device.",
"isCommentAutoGenerated" : true
}, },
"Install updates automatically" : { "Install updates automatically" : {
"extractionState" : "stale", "extractionState" : "stale",
@ -14524,18 +14506,6 @@
"comment" : "Text displayed in a status label at the top of the screen indicating whether low power mode is on or off.", "comment" : "Text displayed in a status label at the top of the screen indicating whether low power mode is on or off.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Lunar" : {
"comment" : "The name of the Lunar display integration option.",
"isCommentAutoGenerated" : true
},
"Lunar is installed but not currently running. Launch Lunar to enable integration." : {
"comment" : "A message displayed in the \"Lunar\" section of the settings, explaining that Lunar is installed but not currently running and that launching it will enable integration.",
"isCommentAutoGenerated" : true
},
"Lunar is running but the socket connection is not yet established. It will connect automatically." : {
"comment" : "A description of what will happen when the user enables the Lunar integration in the settings.",
"isCommentAutoGenerated" : true
},
"Mac battery at %d percent" : { "Mac battery at %d percent" : {
"comment" : "Mac battery gauge accessibility label", "comment" : "Mac battery gauge accessibility label",
"localizations" : { "localizations" : {
@ -19413,6 +19383,10 @@
} }
} }
}, },
"Provider" : {
"comment" : "A label for selecting a third-party display integration provider.",
"isCommentAutoGenerated" : true
},
"Quick Actions" : { "Quick Actions" : {
"localizations" : { "localizations" : {
"fr" : { "fr" : {
@ -23456,6 +23430,10 @@
} }
} }
}, },
"Status" : {
"comment" : "A label displayed next to the status indicator in the \"Third-party DDC provider\" section of the External Display Integrations settings.",
"isCommentAutoGenerated" : true
},
"Status & Actions" : { "Status & Actions" : {
"localizations" : { "localizations" : {
"fr" : { "fr" : {
@ -26251,13 +26229,6 @@
}, },
"When disabled, the notch music player stays visible with placeholder metadata even when playback is inactive." : { "When disabled, the notch music player stays visible with placeholder metadata even when playback is inactive." : {
},
"When enabled, Atoll connects to Lunar's TCP socket and displays DDC brightness, contrast, and volume events using your selected HUD style above." : {
},
"When enabled, Atoll listens for OSD notifications from BetterDisplay and displays them using your selected HUD style above." : {
"comment" : "A footer text for the \"BetterDisplay\" section in the \"External Display Integrations\" settings.",
"isCommentAutoGenerated" : true
}, },
"When enabled, Focus appears briefly (on/off) and then collapses instead of staying visible." : { "When enabled, Focus appears briefly (on/off) and then collapses instead of staying visible." : {
"comment" : "A help text for the setting that allows the focus indicator to collapse instead of staying visible.", "comment" : "A help text for the setting that allows the focus indicator to collapse instead of staying visible.",

View file

@ -739,8 +739,8 @@ struct SettingsView: View {
SettingsSearchEntry(tab: .hudAndOSD, title: "Keyboard Backlight OSD", keywords: ["keyboard", "backlight", "osd"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Keyboard Backlight OSD")), SettingsSearchEntry(tab: .hudAndOSD, title: "Keyboard Backlight OSD", keywords: ["keyboard", "backlight", "osd"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Keyboard Backlight OSD")),
SettingsSearchEntry(tab: .hudAndOSD, title: "Material", keywords: ["material", "frosted", "liquid", "glass", "solid", "osd"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Material")), SettingsSearchEntry(tab: .hudAndOSD, title: "Material", keywords: ["material", "frosted", "liquid", "glass", "solid", "osd"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Material")),
SettingsSearchEntry(tab: .hudAndOSD, title: "Icon & Progress Color", keywords: ["color", "icon", "white", "black", "gray", "osd"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Icon & Progress Color")), SettingsSearchEntry(tab: .hudAndOSD, title: "Icon & Progress Color", keywords: ["color", "icon", "white", "black", "gray", "osd"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Icon & Progress Color")),
SettingsSearchEntry(tab: .hudAndOSD, title: "BetterDisplay integration", keywords: ["betterdisplay", "external", "display", "brightness", "integration", "third party"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "BetterDisplay integration")), SettingsSearchEntry(tab: .hudAndOSD, title: "Third-party DDC app integration", keywords: ["ddc", "third party", "external", "display", "betterdisplay", "lunar"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Third-party DDC app integration")),
SettingsSearchEntry(tab: .hudAndOSD, title: "Lunar integration", keywords: ["lunar", "ddc", "external", "display", "brightness", "contrast", "integration", "third party"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Lunar integration")), SettingsSearchEntry(tab: .hudAndOSD, title: "Third-party DDC provider", keywords: ["provider", "betterdisplay", "lunar", "integration", "refresh detection"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Third-party DDC provider")),
// Media // Media
SettingsSearchEntry(tab: .media, title: "Music Source", keywords: ["media source", "controller"], highlightID: SettingsTab.media.highlightID(for: "Music Source")), SettingsSearchEntry(tab: .media, title: "Music Source", keywords: ["media source", "controller"], highlightID: SettingsTab.media.highlightID(for: "Music Source")),
@ -1496,8 +1496,7 @@ private struct HUDAndOSDSettingsView: View {
@Default(.enableVolumeHUD) var enableVolumeHUD @Default(.enableVolumeHUD) var enableVolumeHUD
@Default(.enableBrightnessHUD) var enableBrightnessHUD @Default(.enableBrightnessHUD) var enableBrightnessHUD
@Default(.enableKeyboardBacklightHUD) var enableKeyboardBacklightHUD @Default(.enableKeyboardBacklightHUD) var enableKeyboardBacklightHUD
@Default(.enableBetterDisplayIntegration) var enableBetterDisplayIntegration @Default(.enableThirdPartyDDCIntegration) var enableThirdPartyDDCIntegration
@Default(.enableLunarIntegration) var enableLunarIntegration
@Default(.verticalHUDShowValue) var verticalHUDShowValue @Default(.verticalHUDShowValue) var verticalHUDShowValue
@Default(.verticalHUDInteractive) var verticalHUDInteractive @Default(.verticalHUDInteractive) var verticalHUDInteractive
@Default(.verticalHUDHeight) var verticalHUDHeight @Default(.verticalHUDHeight) var verticalHUDHeight
@ -1733,7 +1732,7 @@ private struct HUDAndOSDSettingsView: View {
} }
case .vertical: case .vertical:
Form { Form {
if !accessibilityPermission.isAuthorized && !enableBetterDisplayIntegration && !enableLunarIntegration { if !accessibilityPermission.isAuthorized && !enableThirdPartyDDCIntegration {
Section { Section {
SettingsPermissionCallout( SettingsPermissionCallout(
message: "Accessibility permission is needed to intercept system controls for the Vertical HUD.", message: "Accessibility permission is needed to intercept system controls for the Vertical HUD.",
@ -1749,13 +1748,13 @@ private struct HUDAndOSDSettingsView: View {
} }
} }
if accessibilityPermission.isAuthorized || enableBetterDisplayIntegration || enableLunarIntegration { if accessibilityPermission.isAuthorized || enableThirdPartyDDCIntegration {
Section { Section {
Toggle("Volume HUD", isOn: $enableVolumeHUD) Toggle("Volume HUD", isOn: $enableVolumeHUD)
Toggle("Brightness HUD", isOn: $enableBrightnessHUD) Toggle("Brightness HUD", isOn: $enableBrightnessHUD)
Toggle("Keyboard Backlight HUD", isOn: $enableKeyboardBacklightHUD) Toggle("Keyboard Backlight HUD", isOn: $enableKeyboardBacklightHUD)
.disabled(enableBetterDisplayIntegration || enableLunarIntegration) .disabled(enableThirdPartyDDCIntegration)
.help((enableBetterDisplayIntegration || enableLunarIntegration) ? "Disabled while external display integration is active — brightness keys are handled by the external app." : "") .help(enableThirdPartyDDCIntegration ? "Disabled while external display integration is active — brightness keys are handled by the external app." : "")
} header: { } header: {
Text("Controls") Text("Controls")
} footer: { } footer: {
@ -1850,7 +1849,7 @@ private struct HUDAndOSDSettingsView: View {
case .circular: case .circular:
Form { Form {
if !accessibilityPermission.isAuthorized && !enableBetterDisplayIntegration && !enableLunarIntegration { if !accessibilityPermission.isAuthorized && !enableThirdPartyDDCIntegration {
Section { Section {
SettingsPermissionCallout( SettingsPermissionCallout(
message: "Accessibility permission is needed to intercept system controls for the Circular HUD.", message: "Accessibility permission is needed to intercept system controls for the Circular HUD.",
@ -1866,13 +1865,13 @@ private struct HUDAndOSDSettingsView: View {
} }
} }
if accessibilityPermission.isAuthorized || enableBetterDisplayIntegration || enableLunarIntegration { if accessibilityPermission.isAuthorized || enableThirdPartyDDCIntegration {
Section { Section {
Toggle("Volume HUD", isOn: $enableVolumeHUD) Toggle("Volume HUD", isOn: $enableVolumeHUD)
Toggle("Brightness HUD", isOn: $enableBrightnessHUD) Toggle("Brightness HUD", isOn: $enableBrightnessHUD)
Toggle("Keyboard Backlight HUD", isOn: $enableKeyboardBacklightHUD) Toggle("Keyboard Backlight HUD", isOn: $enableKeyboardBacklightHUD)
.disabled(enableBetterDisplayIntegration || enableLunarIntegration) .disabled(enableThirdPartyDDCIntegration)
.help((enableBetterDisplayIntegration || enableLunarIntegration) ? "Disabled while external display integration is active — brightness keys are handled by the external app." : "") .help(enableThirdPartyDDCIntegration ? "Disabled while external display integration is active — brightness keys are handled by the external app." : "")
} header: { } header: {
Text("Controls") Text("Controls")
} footer: { } footer: {
@ -1930,169 +1929,117 @@ private struct HUDAndOSDSettingsView: View {
// MARK: - External Display Integrations Settings Section // MARK: - External Display Integrations Settings Section
private struct ExternalDisplayIntegrationsSection: View { private struct ExternalDisplayIntegrationsSection: View {
@Default(.enableBetterDisplayIntegration) var enableBetterDisplayIntegration @Default(.enableThirdPartyDDCIntegration) var enableThirdPartyDDCIntegration
@Default(.enableLunarIntegration) var enableLunarIntegration @Default(.thirdPartyDDCProvider) var thirdPartyDDCProvider
@ObservedObject private var betterDisplayManager = BetterDisplayManager.shared @ObservedObject private var betterDisplayManager = BetterDisplayManager.shared
@ObservedObject private var lunarManager = LunarManager.shared @ObservedObject private var lunarManager = LunarManager.shared
@State private var isBetterDisplayExpanded = false
@State private var isLunarExpanded = false
private func highlightID(_ title: String) -> String { private func highlightID(_ title: String) -> String {
SettingsTab.hudAndOSD.highlightID(for: title) SettingsTab.hudAndOSD.highlightID(for: title)
} }
// MARK: BetterDisplay helpers private var providerStatusText: String {
switch thirdPartyDDCProvider {
private var bdStatusText: String { case .betterDisplay:
if betterDisplayManager.isRunning { return "Running" } if betterDisplayManager.isRunning { return "Running" }
if betterDisplayManager.isDetected { return "Not running" } if betterDisplayManager.isDetected { return "Not running" }
return "Not detected" return "Not detected"
} case .lunar:
private var bdStatusColor: Color {
if betterDisplayManager.isRunning { return .green }
if betterDisplayManager.isDetected { return .orange }
return .secondary
}
// MARK: Lunar helpers
private var lunarStatusText: String {
if lunarManager.isConnected { return "Connected" } if lunarManager.isConnected { return "Connected" }
if lunarManager.isRunning { return "Running" } if lunarManager.isRunning { return "Running" }
if lunarManager.isDetected { return "Not running" } if lunarManager.isDetected { return "Not running" }
return "Not detected" return "Not detected"
} }
}
private var lunarStatusColor: Color { private var providerStatusColor: Color {
switch thirdPartyDDCProvider {
case .betterDisplay:
if betterDisplayManager.isRunning { return .green }
if betterDisplayManager.isDetected { return .orange }
return .secondary
case .lunar:
if lunarManager.isConnected { return .green } if lunarManager.isConnected { return .green }
if lunarManager.isRunning { return .orange } if lunarManager.isRunning { return .orange }
if lunarManager.isDetected { return .orange } if lunarManager.isDetected { return .orange }
return .secondary return .secondary
} }
}
private var providerStatusDescription: String {
switch thirdPartyDDCProvider {
case .betterDisplay:
if !betterDisplayManager.isDetected {
return "Install [BetterDisplay](https://betterdisplay.pro) to control external display brightness and volume through Atoll's HUD."
}
if !betterDisplayManager.isRunning {
return "BetterDisplay is installed but not currently running. Launch BetterDisplay to enable integration."
}
return "BetterDisplay OSD events will be routed through Atoll's active HUD style. Make sure BetterDisplay's OSD integration is enabled in Settings › Application › Integration."
case .lunar:
if !lunarManager.isDetected {
return "Install [Lunar](https://lunar.fyi) to control external display brightness, contrast, and volume through Atoll's HUD via DDC."
}
if !lunarManager.isRunning {
return "Lunar is installed but not currently running. Launch Lunar to enable integration."
}
if lunarManager.isConnected {
return "Connected to Lunar's DDC socket. Brightness, contrast, and volume adjustments will be shown through Atoll's HUD."
}
return "Lunar is running but the socket connection is not yet established. It will connect automatically."
}
}
private func refreshDetectionStatus() {
switch thirdPartyDDCProvider {
case .betterDisplay:
betterDisplayManager.refreshDetectionStatus()
case .lunar:
lunarManager.refreshDetectionStatus()
}
}
var body: some View { var body: some View {
Form { Form {
// MARK: - BetterDisplay
Section { Section {
DisclosureGroup(isExpanded: $isBetterDisplayExpanded) { Toggle("Enable third-party DDC app integration", isOn: $enableThirdPartyDDCIntegration)
.settingsHighlight(id: highlightID("Third-party DDC app integration"))
if enableThirdPartyDDCIntegration {
Picker("Provider", selection: $thirdPartyDDCProvider) {
ForEach(ThirdPartyDDCProvider.allCases) { provider in
Text(provider.displayName).tag(provider)
}
}
.settingsHighlight(id: highlightID("Third-party DDC provider"))
HStack { HStack {
VStack(alignment: .leading, spacing: 2) { Text("Status")
Text(bdStatusText)
.font(.caption)
.foregroundStyle(bdStatusColor)
}
Spacer() Spacer()
Toggle("", isOn: $enableBetterDisplayIntegration) Text(providerStatusText)
.toggleStyle(.switch)
.labelsHidden()
.disabled(!betterDisplayManager.isDetected)
}
.settingsHighlight(id: highlightID("BetterDisplay integration"))
if !betterDisplayManager.isDetected {
Text("Install [BetterDisplay](https://betterdisplay.pro) to control external display brightness and volume through Atoll's HUD.")
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(providerStatusColor)
} else if !betterDisplayManager.isRunning {
Text("BetterDisplay is installed but not currently running. Launch BetterDisplay to enable integration.")
.font(.caption)
.foregroundStyle(.secondary)
} else if enableBetterDisplayIntegration {
Text("BetterDisplay OSD events will be routed through Atoll's active HUD style. Make sure BetterDisplay's OSD integration is enabled in its Settings › Application › Integration.")
.font(.caption)
.foregroundStyle(.secondary)
} else {
Text("Enable to route BetterDisplay's brightness and volume changes through Atoll's HUD instead of the system OSD.")
.font(.caption)
.foregroundStyle(.secondary)
} }
if betterDisplayManager.isDetected { Text(providerStatusDescription)
.font(.caption)
.foregroundStyle(.secondary)
Button { Button {
betterDisplayManager.refreshDetectionStatus() refreshDetectionStatus()
} label: { } label: {
Label("Refresh detection", systemImage: "arrow.clockwise") Label("Refresh detection", systemImage: "arrow.clockwise")
.font(.caption) .font(.caption)
} }
.buttonStyle(.link) .buttonStyle(.link)
}
} label: {
HStack(spacing: 6) {
Image(systemName: "display.2")
Text("BetterDisplay")
.font(.system(size: 13, weight: .medium))
}
}
} footer: {
if isBetterDisplayExpanded {
Text("When enabled, Atoll listens for OSD notifications from BetterDisplay and displays them using your selected HUD style above.")
.foregroundStyle(.secondary)
.font(.caption)
}
}
// MARK: - Lunar
Section {
DisclosureGroup(isExpanded: $isLunarExpanded) {
HStack {
VStack(alignment: .leading, spacing: 2) {
Text(lunarStatusText)
.font(.caption)
.foregroundStyle(lunarStatusColor)
}
Spacer()
Toggle("", isOn: $enableLunarIntegration)
.toggleStyle(.switch)
.labelsHidden()
.disabled(!lunarManager.isDetected)
}
.settingsHighlight(id: highlightID("Lunar integration"))
if !lunarManager.isDetected {
Text("Install [Lunar](https://lunar.fyi) to control external display brightness, contrast, and volume through Atoll's HUD via DDC.")
.font(.caption)
.foregroundStyle(.secondary)
} else if !lunarManager.isRunning {
Text("Lunar is installed but not currently running. Launch Lunar to enable integration.")
.font(.caption)
.foregroundStyle(.secondary)
} else if lunarManager.isConnected && enableLunarIntegration {
Text("Connected to Lunar's DDC socket. Brightness, contrast, and volume adjustments will be shown through Atoll's HUD.")
.font(.caption)
.foregroundStyle(.secondary)
} else if enableLunarIntegration {
Text("Lunar is running but the socket connection is not yet established. It will connect automatically.")
.font(.caption)
.foregroundStyle(.secondary)
} else { } else {
Text("Enable to route Lunar's DDC brightness, contrast, and volume changes through Atoll's HUD.") Text("Enable to route BetterDisplay or Lunar display adjustments through Atoll's active HUD style.")
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
if lunarManager.isDetected {
Button {
lunarManager.refreshDetectionStatus()
} label: {
Label("Refresh detection", systemImage: "arrow.clockwise")
.font(.caption)
}
.buttonStyle(.link)
}
} label: {
HStack(spacing: 6) {
Image(systemName: "moon.fill")
Text("Lunar")
.font(.system(size: 13, weight: .medium))
}
}
} footer: { } footer: {
if isLunarExpanded { if enableThirdPartyDDCIntegration {
Text("When enabled, Atoll connects to Lunar's TCP socket and displays DDC brightness, contrast, and volume events using your selected HUD style above.") Text("Atoll listens to the selected provider and renders its brightness/volume HUD updates using your currently active HUD style above.")
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.font(.caption) .font(.caption)
} }
@ -2242,8 +2189,7 @@ struct HUD: View {
@Default(.enableVolumeHUD) var enableVolumeHUD @Default(.enableVolumeHUD) var enableVolumeHUD
@Default(.enableBrightnessHUD) var enableBrightnessHUD @Default(.enableBrightnessHUD) var enableBrightnessHUD
@Default(.enableKeyboardBacklightHUD) var enableKeyboardBacklightHUD @Default(.enableKeyboardBacklightHUD) var enableKeyboardBacklightHUD
@Default(.enableBetterDisplayIntegration) var enableBetterDisplayIntegration @Default(.enableThirdPartyDDCIntegration) var enableThirdPartyDDCIntegration
@Default(.enableLunarIntegration) var enableLunarIntegration
@Default(.systemHUDSensitivity) var systemHUDSensitivity @Default(.systemHUDSensitivity) var systemHUDSensitivity
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
@ObservedObject private var accessibilityPermission = AccessibilityPermissionStore.shared @ObservedObject private var accessibilityPermission = AccessibilityPermissionStore.shared
@ -2262,7 +2208,7 @@ struct HUD: View {
var body: some View { var body: some View {
Form { Form {
if !hasAccessibilityPermission && !enableBetterDisplayIntegration && !enableLunarIntegration { if !hasAccessibilityPermission && !enableThirdPartyDDCIntegration {
Section { Section {
SettingsPermissionCallout( SettingsPermissionCallout(
message: "Accessibility permission lets Dynamic Island replace the native volume, brightness, and keyboard HUDs.", message: "Accessibility permission lets Dynamic Island replace the native volume, brightness, and keyboard HUDs.",
@ -2276,13 +2222,13 @@ struct HUD: View {
if enableSystemHUD && !Defaults[.enableCustomOSD] && (hasAccessibilityPermission || enableBetterDisplayIntegration || enableLunarIntegration) { if enableSystemHUD && !Defaults[.enableCustomOSD] && (hasAccessibilityPermission || enableThirdPartyDDCIntegration) {
Section { Section {
Toggle("Volume HUD", isOn: $enableVolumeHUD) Toggle("Volume HUD", isOn: $enableVolumeHUD)
Toggle("Brightness HUD", isOn: $enableBrightnessHUD) Toggle("Brightness HUD", isOn: $enableBrightnessHUD)
Toggle("Keyboard Backlight HUD", isOn: $enableKeyboardBacklightHUD) Toggle("Keyboard Backlight HUD", isOn: $enableKeyboardBacklightHUD)
.disabled(enableBetterDisplayIntegration || enableLunarIntegration) .disabled(enableThirdPartyDDCIntegration)
.help((enableBetterDisplayIntegration || enableLunarIntegration) ? "Disabled while external display integration is active \u{2014} brightness keys are handled by the external app." : "") .help(enableThirdPartyDDCIntegration ? "Disabled while external display integration is active \u{2014} brightness keys are handled by the external app." : "")
} header: { } header: {
Text("Controls") Text("Controls")
} footer: { } footer: {
@ -7003,8 +6949,7 @@ struct CustomOSDSettings: View {
@Default(.enableOSDVolume) var enableOSDVolume @Default(.enableOSDVolume) var enableOSDVolume
@Default(.enableOSDBrightness) var enableOSDBrightness @Default(.enableOSDBrightness) var enableOSDBrightness
@Default(.enableOSDKeyboardBacklight) var enableOSDKeyboardBacklight @Default(.enableOSDKeyboardBacklight) var enableOSDKeyboardBacklight
@Default(.enableBetterDisplayIntegration) var enableBetterDisplayIntegration @Default(.enableThirdPartyDDCIntegration) var enableThirdPartyDDCIntegration
@Default(.enableLunarIntegration) var enableLunarIntegration
@Default(.osdMaterial) var osdMaterial @Default(.osdMaterial) var osdMaterial
@Default(.osdLiquidGlassCustomizationMode) var osdLiquidGlassCustomizationMode @Default(.osdLiquidGlassCustomizationMode) var osdLiquidGlassCustomizationMode
@Default(.osdLiquidGlassVariant) var osdLiquidGlassVariant @Default(.osdLiquidGlassVariant) var osdLiquidGlassVariant
@ -7047,7 +6992,7 @@ struct CustomOSDSettings: View {
var body: some View { var body: some View {
Form { Form {
if !hasAccessibilityPermission && !enableBetterDisplayIntegration && !enableLunarIntegration { if !hasAccessibilityPermission && !enableThirdPartyDDCIntegration {
Section { Section {
SettingsPermissionCallout( SettingsPermissionCallout(
message: "Accessibility permission is needed to intercept system controls for the Custom OSD.", message: "Accessibility permission is needed to intercept system controls for the Custom OSD.",
@ -7059,7 +7004,7 @@ struct CustomOSDSettings: View {
} }
} }
if hasAccessibilityPermission || enableBetterDisplayIntegration || enableLunarIntegration { if hasAccessibilityPermission || enableThirdPartyDDCIntegration {
Section { Section {
Toggle("Volume OSD", isOn: $enableOSDVolume) Toggle("Volume OSD", isOn: $enableOSDVolume)
.settingsHighlight(id: highlightID("Volume OSD")) .settingsHighlight(id: highlightID("Volume OSD"))
@ -7067,8 +7012,8 @@ struct CustomOSDSettings: View {
.settingsHighlight(id: highlightID("Brightness OSD")) .settingsHighlight(id: highlightID("Brightness OSD"))
Toggle("Keyboard Backlight OSD", isOn: $enableOSDKeyboardBacklight) Toggle("Keyboard Backlight OSD", isOn: $enableOSDKeyboardBacklight)
.settingsHighlight(id: highlightID("Keyboard Backlight OSD")) .settingsHighlight(id: highlightID("Keyboard Backlight OSD"))
.disabled(enableBetterDisplayIntegration || enableLunarIntegration) .disabled(enableThirdPartyDDCIntegration)
.help((enableBetterDisplayIntegration || enableLunarIntegration) ? "Disabled while external display integration is active \u{2014} brightness keys are handled by the external app." : "") .help(enableThirdPartyDDCIntegration ? "Disabled while external display integration is active \u{2014} brightness keys are handled by the external app." : "")
} header: { } header: {
Text("Controls") Text("Controls")
} footer: { } footer: {

View file

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

View file

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

View file

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

View file

@ -316,6 +316,22 @@ enum ColorPickerDisplayMode: String, CaseIterable, Codable, Defaults.Serializabl
} }
} }
enum ThirdPartyDDCProvider: String, CaseIterable, Codable, Defaults.Serializable, Identifiable {
case betterDisplay
case lunar
var id: String { rawValue }
var displayName: String {
switch self {
case .betterDisplay:
return "BetterDisplay"
case .lunar:
return "Lunar"
}
}
}
enum HideNotchOption: String, Defaults.Serializable { enum HideNotchOption: String, Defaults.Serializable {
case always case always
case nowPlayingOnly case nowPlayingOnly
@ -1029,10 +1045,13 @@ extension Defaults.Keys {
static let circularHUDStrokeWidth = Key<CGFloat>("circularHUDStrokeWidth", default: 4) static let circularHUDStrokeWidth = Key<CGFloat>("circularHUDStrokeWidth", default: 4)
static let circularHUDUseAccentColor = Key<Bool>("circularHUDUseAccentColor", default: true) static let circularHUDUseAccentColor = Key<Bool>("circularHUDUseAccentColor", default: true)
// MARK: BetterDisplay Integration // MARK: Third-Party DDC Integration
static let enableBetterDisplayIntegration = Key<Bool>("enableBetterDisplayIntegration", default: false) static let enableThirdPartyDDCIntegration = Key<Bool>("enableThirdPartyDDCIntegration", default: false)
static let thirdPartyDDCProvider = Key<ThirdPartyDDCProvider>("thirdPartyDDCProvider", default: .betterDisplay)
static let didMigrateThirdPartyDDCIntegration = Key<Bool>("didMigrateThirdPartyDDCIntegration", default: false)
// MARK: Lunar Integration // Legacy keys retained for migration/backward compatibility
static let enableBetterDisplayIntegration = Key<Bool>("enableBetterDisplayIntegration", default: false)
static let enableLunarIntegration = Key<Bool>("enableLunarIntegration", default: false) static let enableLunarIntegration = Key<Bool>("enableLunarIntegration", default: false)
static let hasSeenOSDAlphaWarning = Key<Bool>("hasSeenOSDAlphaWarning", default: false) static let hasSeenOSDAlphaWarning = Key<Bool>("hasSeenOSDAlphaWarning", default: false)
@ -1157,6 +1176,29 @@ extension Defaults.Keys {
Defaults[.didMigrateMusicControlSlots] = true Defaults[.didMigrateMusicControlSlots] = true
} }
static func migrateThirdPartyDDCIntegration() {
if Defaults[.didMigrateThirdPartyDDCIntegration] == false {
let legacyBetterDisplayEnabled = Defaults[.enableBetterDisplayIntegration]
let legacyLunarEnabled = Defaults[.enableLunarIntegration]
if legacyBetterDisplayEnabled || legacyLunarEnabled {
Defaults[.enableThirdPartyDDCIntegration] = true
Defaults[.thirdPartyDDCProvider] = (legacyLunarEnabled && !legacyBetterDisplayEnabled) ? .lunar : .betterDisplay
}
Defaults[.didMigrateThirdPartyDDCIntegration] = true
}
syncLegacyThirdPartyDDCKeys()
}
static func syncLegacyThirdPartyDDCKeys() {
let isIntegrationEnabled = Defaults[.enableThirdPartyDDCIntegration]
let selectedProvider = Defaults[.thirdPartyDDCProvider]
Defaults[.enableBetterDisplayIntegration] = isIntegrationEnabled && selectedProvider == .betterDisplay
Defaults[.enableLunarIntegration] = isIntegrationEnabled && selectedProvider == .lunar
}
private static func normalizeMusicAuxControls() { private static func normalizeMusicAuxControls() {
guard Defaults[.musicAuxLeftControl] == Defaults[.musicAuxRightControl] else { return } guard Defaults[.musicAuxLeftControl] == Defaults[.musicAuxRightControl] else { return }