diff --git a/DynamicIsland/DynamicIslandApp.swift b/DynamicIsland/DynamicIslandApp.swift index c5f22a5..ae8e11d 100644 --- a/DynamicIsland/DynamicIslandApp.swift +++ b/DynamicIsland/DynamicIslandApp.swift @@ -400,6 +400,19 @@ class AppDelegate: NSObject, NSApplicationDelegate { Defaults.Keys.migrateMusicAuxControls() Defaults.Keys.migrateMusicControlSlots() 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) idleAnimationManager.initializeDefaultAnimations() diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index 0da0743..ad59e17 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -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." : { "extractionState" : "stale", "localizations" : { @@ -4919,16 +4923,6 @@ "comment" : "A label indicating a beta version.", "isCommentAutoGenerated" : true, "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" : { "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." : { }, @@ -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" : { "comment" : "Toggle to enable or disable third-party extensions.", "isCommentAutoGenerated" : true @@ -10595,12 +10589,8 @@ } } }, - "Enable to route BetterDisplay's brightness and volume changes through Atoll's HUD instead of the system OSD." : { - "comment" : "A description of the benefit of enabling BetterDisplay integration.", - "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.", + "Enable to route BetterDisplay or Lunar display adjustments through Atoll's active HUD style." : { + "comment" : "A description of the feature that allows third-party display integrations to be enabled.", "isCommentAutoGenerated" : true }, "Enable when scaling beyond 30x20px so the Atoll opens gracefully." : { @@ -13484,14 +13474,6 @@ }, "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" : { "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.", "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" : { "comment" : "Mac battery gauge accessibility label", "localizations" : { @@ -19413,6 +19383,10 @@ } } }, + "Provider" : { + "comment" : "A label for selecting a third-party display integration provider.", + "isCommentAutoGenerated" : true + }, "Quick Actions" : { "localizations" : { "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" : { "localizations" : { "fr" : { @@ -26251,13 +26229,6 @@ }, "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." : { "comment" : "A help text for the setting that allows the focus indicator to collapse instead of staying visible.", diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 8bd68cd..a560bca 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -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: "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: "BetterDisplay integration", keywords: ["betterdisplay", "external", "display", "brightness", "integration", "third party"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "BetterDisplay 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 app integration", keywords: ["ddc", "third party", "external", "display", "betterdisplay", "lunar"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Third-party DDC app 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 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(.enableBrightnessHUD) var enableBrightnessHUD @Default(.enableKeyboardBacklightHUD) var enableKeyboardBacklightHUD - @Default(.enableBetterDisplayIntegration) var enableBetterDisplayIntegration - @Default(.enableLunarIntegration) var enableLunarIntegration + @Default(.enableThirdPartyDDCIntegration) var enableThirdPartyDDCIntegration @Default(.verticalHUDShowValue) var verticalHUDShowValue @Default(.verticalHUDInteractive) var verticalHUDInteractive @Default(.verticalHUDHeight) var verticalHUDHeight @@ -1733,7 +1732,7 @@ private struct HUDAndOSDSettingsView: View { } case .vertical: Form { - if !accessibilityPermission.isAuthorized && !enableBetterDisplayIntegration && !enableLunarIntegration { + if !accessibilityPermission.isAuthorized && !enableThirdPartyDDCIntegration { Section { SettingsPermissionCallout( 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 { Toggle("Volume HUD", isOn: $enableVolumeHUD) Toggle("Brightness HUD", isOn: $enableBrightnessHUD) Toggle("Keyboard Backlight HUD", isOn: $enableKeyboardBacklightHUD) - .disabled(enableBetterDisplayIntegration || enableLunarIntegration) - .help((enableBetterDisplayIntegration || enableLunarIntegration) ? "Disabled while external display integration is active — brightness keys are handled by the external app." : "") + .disabled(enableThirdPartyDDCIntegration) + .help(enableThirdPartyDDCIntegration ? "Disabled while external display integration is active — brightness keys are handled by the external app." : "") } header: { Text("Controls") } footer: { @@ -1850,7 +1849,7 @@ private struct HUDAndOSDSettingsView: View { case .circular: Form { - if !accessibilityPermission.isAuthorized && !enableBetterDisplayIntegration && !enableLunarIntegration { + if !accessibilityPermission.isAuthorized && !enableThirdPartyDDCIntegration { Section { SettingsPermissionCallout( 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 { Toggle("Volume HUD", isOn: $enableVolumeHUD) Toggle("Brightness HUD", isOn: $enableBrightnessHUD) Toggle("Keyboard Backlight HUD", isOn: $enableKeyboardBacklightHUD) - .disabled(enableBetterDisplayIntegration || enableLunarIntegration) - .help((enableBetterDisplayIntegration || enableLunarIntegration) ? "Disabled while external display integration is active — brightness keys are handled by the external app." : "") + .disabled(enableThirdPartyDDCIntegration) + .help(enableThirdPartyDDCIntegration ? "Disabled while external display integration is active — brightness keys are handled by the external app." : "") } header: { Text("Controls") } footer: { @@ -1930,169 +1929,117 @@ private struct HUDAndOSDSettingsView: View { // MARK: - External Display Integrations Settings Section private struct ExternalDisplayIntegrationsSection: View { - @Default(.enableBetterDisplayIntegration) var enableBetterDisplayIntegration - @Default(.enableLunarIntegration) var enableLunarIntegration + @Default(.enableThirdPartyDDCIntegration) var enableThirdPartyDDCIntegration + @Default(.thirdPartyDDCProvider) var thirdPartyDDCProvider @ObservedObject private var betterDisplayManager = BetterDisplayManager.shared @ObservedObject private var lunarManager = LunarManager.shared - @State private var isBetterDisplayExpanded = false - @State private var isLunarExpanded = false - private func highlightID(_ title: String) -> String { SettingsTab.hudAndOSD.highlightID(for: title) } - // MARK: BetterDisplay helpers - - private var bdStatusText: String { - if betterDisplayManager.isRunning { return "Running" } - if betterDisplayManager.isDetected { return "Not running" } - return "Not detected" + private var providerStatusText: String { + switch thirdPartyDDCProvider { + case .betterDisplay: + if betterDisplayManager.isRunning { return "Running" } + if betterDisplayManager.isDetected { return "Not running" } + return "Not detected" + case .lunar: + if lunarManager.isConnected { return "Connected" } + if lunarManager.isRunning { return "Running" } + if lunarManager.isDetected { return "Not running" } + return "Not detected" + } } - private var bdStatusColor: Color { - if betterDisplayManager.isRunning { return .green } - if betterDisplayManager.isDetected { return .orange } - return .secondary + 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.isRunning { return .orange } + if lunarManager.isDetected { return .orange } + return .secondary + } } - // MARK: Lunar helpers - - private var lunarStatusText: String { - if lunarManager.isConnected { return "Connected" } - if lunarManager.isRunning { return "Running" } - if lunarManager.isDetected { return "Not running" } - return "Not detected" + 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 var lunarStatusColor: Color { - if lunarManager.isConnected { return .green } - if lunarManager.isRunning { return .orange } - if lunarManager.isDetected { return .orange } - return .secondary + private func refreshDetectionStatus() { + switch thirdPartyDDCProvider { + case .betterDisplay: + betterDisplayManager.refreshDetectionStatus() + case .lunar: + lunarManager.refreshDetectionStatus() + } } var body: some View { Form { - // MARK: - BetterDisplay - 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 { - VStack(alignment: .leading, spacing: 2) { - Text(bdStatusText) - .font(.caption) - .foregroundStyle(bdStatusColor) - } + Text("Status") Spacer() - Toggle("", isOn: $enableBetterDisplayIntegration) - .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.") + Text(providerStatusText) .font(.caption) - .foregroundStyle(.secondary) - } 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) + .foregroundStyle(providerStatusColor) } - if betterDisplayManager.isDetected { - Button { - betterDisplayManager.refreshDetectionStatus() - } label: { - Label("Refresh detection", systemImage: "arrow.clockwise") - .font(.caption) - } - .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) + Text(providerStatusDescription) .font(.caption) - } - } + .foregroundStyle(.secondary) - // 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 { - Text("Enable to route Lunar's DDC brightness, contrast, and volume changes through Atoll's HUD.") - .font(.caption) - .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)) + Button { + refreshDetectionStatus() + } label: { + Label("Refresh detection", systemImage: "arrow.clockwise") + .font(.caption) } + .buttonStyle(.link) + } else { + Text("Enable to route BetterDisplay or Lunar display adjustments through Atoll's active HUD style.") + .font(.caption) + .foregroundStyle(.secondary) } } footer: { - if isLunarExpanded { - Text("When enabled, Atoll connects to Lunar's TCP socket and displays DDC brightness, contrast, and volume events using your selected HUD style above.") + if enableThirdPartyDDCIntegration { + Text("Atoll listens to the selected provider and renders its brightness/volume HUD updates using your currently active HUD style above.") .foregroundStyle(.secondary) .font(.caption) } @@ -2242,8 +2189,7 @@ struct HUD: View { @Default(.enableVolumeHUD) var enableVolumeHUD @Default(.enableBrightnessHUD) var enableBrightnessHUD @Default(.enableKeyboardBacklightHUD) var enableKeyboardBacklightHUD - @Default(.enableBetterDisplayIntegration) var enableBetterDisplayIntegration - @Default(.enableLunarIntegration) var enableLunarIntegration + @Default(.enableThirdPartyDDCIntegration) var enableThirdPartyDDCIntegration @Default(.systemHUDSensitivity) var systemHUDSensitivity @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared @ObservedObject private var accessibilityPermission = AccessibilityPermissionStore.shared @@ -2262,7 +2208,7 @@ struct HUD: View { var body: some View { Form { - if !hasAccessibilityPermission && !enableBetterDisplayIntegration && !enableLunarIntegration { + if !hasAccessibilityPermission && !enableThirdPartyDDCIntegration { Section { SettingsPermissionCallout( 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 { Toggle("Volume HUD", isOn: $enableVolumeHUD) Toggle("Brightness HUD", isOn: $enableBrightnessHUD) Toggle("Keyboard Backlight HUD", isOn: $enableKeyboardBacklightHUD) - .disabled(enableBetterDisplayIntegration || enableLunarIntegration) - .help((enableBetterDisplayIntegration || enableLunarIntegration) ? "Disabled while external display integration is active \u{2014} brightness keys are handled by the external app." : "") + .disabled(enableThirdPartyDDCIntegration) + .help(enableThirdPartyDDCIntegration ? "Disabled while external display integration is active \u{2014} brightness keys are handled by the external app." : "") } header: { Text("Controls") } footer: { @@ -7003,8 +6949,7 @@ struct CustomOSDSettings: View { @Default(.enableOSDVolume) var enableOSDVolume @Default(.enableOSDBrightness) var enableOSDBrightness @Default(.enableOSDKeyboardBacklight) var enableOSDKeyboardBacklight - @Default(.enableBetterDisplayIntegration) var enableBetterDisplayIntegration - @Default(.enableLunarIntegration) var enableLunarIntegration + @Default(.enableThirdPartyDDCIntegration) var enableThirdPartyDDCIntegration @Default(.osdMaterial) var osdMaterial @Default(.osdLiquidGlassCustomizationMode) var osdLiquidGlassCustomizationMode @Default(.osdLiquidGlassVariant) var osdLiquidGlassVariant @@ -7047,7 +6992,7 @@ struct CustomOSDSettings: View { var body: some View { Form { - if !hasAccessibilityPermission && !enableBetterDisplayIntegration && !enableLunarIntegration { + if !hasAccessibilityPermission && !enableThirdPartyDDCIntegration { Section { SettingsPermissionCallout( 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 { Toggle("Volume OSD", isOn: $enableOSDVolume) .settingsHighlight(id: highlightID("Volume OSD")) @@ -7067,8 +7012,8 @@ struct CustomOSDSettings: View { .settingsHighlight(id: highlightID("Brightness OSD")) Toggle("Keyboard Backlight OSD", isOn: $enableOSDKeyboardBacklight) .settingsHighlight(id: highlightID("Keyboard Backlight OSD")) - .disabled(enableBetterDisplayIntegration || enableLunarIntegration) - .help((enableBetterDisplayIntegration || enableLunarIntegration) ? "Disabled while external display integration is active \u{2014} brightness keys are handled by the external app." : "") + .disabled(enableThirdPartyDDCIntegration) + .help(enableThirdPartyDDCIntegration ? "Disabled while external display integration is active \u{2014} brightness keys are handled by the external app." : "") } header: { Text("Controls") } footer: { diff --git a/DynamicIsland/managers/BetterDisplayManager.swift b/DynamicIsland/managers/BetterDisplayManager.swift index 88d9834..32423ab 100644 --- a/DynamicIsland/managers/BetterDisplayManager.swift +++ b/DynamicIsland/managers/BetterDisplayManager.swift @@ -115,6 +115,10 @@ final class BetterDisplayManager: ObservableObject { private var cancellables = Set() 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) diff --git a/DynamicIsland/managers/LunarManager.swift b/DynamicIsland/managers/LunarManager.swift index 5bd09ab..34e113c 100644 --- a/DynamicIsland/managers/LunarManager.swift +++ b/DynamicIsland/managers/LunarManager.swift @@ -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) diff --git a/DynamicIsland/managers/SystemHUDManager.swift b/DynamicIsland/managers/SystemHUDManager.swift index 34192cf..88be419 100644 --- a/DynamicIsland/managers/SystemHUDManager.swift +++ b/DynamicIsland/managers/SystemHUDManager.swift @@ -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 } diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 459e537..7e0d09a 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -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 { case always case nowPlayingOnly @@ -1029,10 +1045,13 @@ extension Defaults.Keys { static let circularHUDStrokeWidth = Key("circularHUDStrokeWidth", default: 4) static let circularHUDUseAccentColor = Key("circularHUDUseAccentColor", default: true) - // MARK: BetterDisplay Integration - static let enableBetterDisplayIntegration = Key("enableBetterDisplayIntegration", default: false) + // MARK: Third-Party DDC Integration + static let enableThirdPartyDDCIntegration = Key("enableThirdPartyDDCIntegration", default: false) + static let thirdPartyDDCProvider = Key("thirdPartyDDCProvider", default: .betterDisplay) + static let didMigrateThirdPartyDDCIntegration = Key("didMigrateThirdPartyDDCIntegration", default: false) - // MARK: Lunar Integration + // Legacy keys retained for migration/backward compatibility + static let enableBetterDisplayIntegration = Key("enableBetterDisplayIntegration", default: false) static let enableLunarIntegration = Key("enableLunarIntegration", default: false) static let hasSeenOSDAlphaWarning = Key("hasSeenOSDAlphaWarning", default: false) @@ -1157,6 +1176,29 @@ extension Defaults.Keys { 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() { guard Defaults[.musicAuxLeftControl] == Defaults[.musicAuxRightControl] else { return }