mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Update: Working BetterDisplay Integration
This commit is contained in:
parent
b900804acb
commit
dd726d6401
5 changed files with 547 additions and 0 deletions
|
|
@ -406,6 +406,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
// Setup SystemHUD Manager
|
||||
SystemHUDManager.shared.setup(coordinator: coordinator)
|
||||
|
||||
// Setup BetterDisplay integration
|
||||
BetterDisplayManager.shared.configure(coordinator: coordinator)
|
||||
|
||||
// Setup ScreenRecording Manager
|
||||
if Defaults[.enableScreenRecordingDetection] {
|
||||
ScreenRecordingManager.shared.startMonitoring()
|
||||
|
|
|
|||
|
|
@ -4762,6 +4762,17 @@
|
|||
"comment" : "A label indicating a beta version.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
"shouldTranslate" : false
|
||||
},
|
||||
"BetterDisplay" : {
|
||||
"comment" : "The name of the BetterDisplay integration option.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"BetterDisplay Integration" : {
|
||||
"comment" : "The header text for the \"BetterDisplay Integration\" section in the settings view.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"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",
|
||||
|
|
@ -8469,6 +8480,10 @@
|
|||
"comment" : "Name of a user profile for a designer.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Detected" : {
|
||||
"comment" : "A label indicating that BetterDisplay is detected on the system.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Detection Status" : {
|
||||
"comment" : "The header for a section that displays whether the screen recording detection feature is currently active or inactive.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
|
|
@ -10205,6 +10220,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"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 when scaling beyond 30x20px so the Atoll opens gracefully." : {
|
||||
"localizations" : {
|
||||
"pt-BR" : {
|
||||
|
|
@ -12990,6 +13009,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"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 updates automatically" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
|
|
@ -17324,6 +17347,9 @@
|
|||
"Not charging" : {
|
||||
"comment" : "Text displayed in the status text when the battery is not charging.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Not detected" : {
|
||||
|
||||
},
|
||||
"Not Now" : {
|
||||
"localizations" : {
|
||||
|
|
@ -19125,6 +19151,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Refresh detection" : {
|
||||
"comment" : "A button label that refreshes the detection status of BetterDisplay.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Release name" : {
|
||||
"localizations" : {
|
||||
"ar" : {
|
||||
|
|
@ -25308,6 +25338,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"When enabled, Atoll listens for OSD notifications from BetterDisplay and displays them using your selected HUD style above. This works alongside the existing media key interception — BetterDisplay handles external display controls while Atoll provides the visual feedback." : {
|
||||
"comment" : "A footer description for the \"BetterDisplay Integration\" section in the settings view.",
|
||||
"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.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
|
|
|
|||
|
|
@ -650,6 +650,7 @@ 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")),
|
||||
|
||||
// Media
|
||||
SettingsSearchEntry(tab: .media, title: "Music Source", keywords: ["media source", "controller"], highlightID: SettingsTab.media.highlightID(for: "Music Source")),
|
||||
|
|
@ -1794,6 +1795,9 @@ private struct HUDAndOSDSettingsView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BetterDisplay Integration (shared across all HUD variants)
|
||||
BetterDisplayIntegrationSection()
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
.background(paneBackgroundColor)
|
||||
|
|
@ -1807,6 +1811,74 @@ private struct HUDAndOSDSettingsView: View {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - BetterDisplay Integration Settings Section
|
||||
|
||||
private struct BetterDisplayIntegrationSection: View {
|
||||
@Default(.enableBetterDisplayIntegration) var enableBetterDisplayIntegration
|
||||
@ObservedObject private var betterDisplayManager = BetterDisplayManager.shared
|
||||
|
||||
private func highlightID(_ title: String) -> String {
|
||||
SettingsTab.hudAndOSD.highlightID(for: title)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("BetterDisplay")
|
||||
.font(.system(size: 13, weight: .medium))
|
||||
Text(betterDisplayManager.isDetected ? "Detected" : "Not detected")
|
||||
.font(.caption)
|
||||
.foregroundStyle(betterDisplayManager.isDetected ? .green : .secondary)
|
||||
}
|
||||
|
||||
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.")
|
||||
.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 {
|
||||
Button {
|
||||
betterDisplayManager.refreshDetectionStatus()
|
||||
} label: {
|
||||
Label("Refresh detection", systemImage: "arrow.clockwise")
|
||||
.font(.caption)
|
||||
}
|
||||
.buttonStyle(.link)
|
||||
}
|
||||
} header: {
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: "display.2")
|
||||
Text("BetterDisplay Integration")
|
||||
}
|
||||
} footer: {
|
||||
Text("When enabled, Atoll listens for OSD notifications from BetterDisplay and displays them using your selected HUD style above. This works alongside the existing media key interception — BetterDisplay handles external display controls while Atoll provides the visual feedback.")
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct HUDSelectionCard<Preview: View>: View {
|
||||
let title: String
|
||||
let isSelected: Bool
|
||||
|
|
|
|||
435
DynamicIsland/managers/BetterDisplayManager.swift
Normal file
435
DynamicIsland/managers/BetterDisplayManager.swift
Normal file
|
|
@ -0,0 +1,435 @@
|
|||
/*
|
||||
* Atoll (DynamicIsland)
|
||||
* Copyright (C) 2024-2026 Atoll Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@preconcurrency import Foundation
|
||||
import AppKit
|
||||
import Defaults
|
||||
import Combine
|
||||
|
||||
// MARK: - BetterDisplay OSD Notification Model
|
||||
|
||||
/// Matches the JSON structure dispatched by BetterDisplay's OSD notification system.
|
||||
/// See: https://github.com/waydabber/BetterDisplay/wiki/Integration-features,-CLI#osd-notification-dispatch-integration
|
||||
struct BetterDisplayOSDNotification: Codable {
|
||||
var displayID: Int?
|
||||
var systemIconID: Int?
|
||||
var customSymbol: String?
|
||||
var text: String?
|
||||
var lock: Bool?
|
||||
var controlTarget: String?
|
||||
var value: Double?
|
||||
var maxValue: Double?
|
||||
var symbolFadeAfter: Int?
|
||||
var symbolSizeMultiplier: Double?
|
||||
var textFadeAfter: Int?
|
||||
}
|
||||
|
||||
// MARK: - BetterDisplay Request / Response Models
|
||||
|
||||
/// JSON structure for sending commands to BetterDisplay via distributed notifications.
|
||||
struct BetterDisplayRequestData: Codable {
|
||||
var uuid: String?
|
||||
var commands: [String] = []
|
||||
var parameters: [String: String?] = [:]
|
||||
}
|
||||
|
||||
/// JSON structure for receiving responses from BetterDisplay.
|
||||
struct BetterDisplayResponseData: Codable {
|
||||
var uuid: String?
|
||||
var result: Bool?
|
||||
var payload: String?
|
||||
}
|
||||
|
||||
// MARK: - BetterDisplay Control Target Classification
|
||||
|
||||
enum BetterDisplayControlCategory {
|
||||
case brightness
|
||||
case volume
|
||||
case other
|
||||
}
|
||||
|
||||
private let brightnessControlTargets: Set<String> = [
|
||||
"combinedBrightness",
|
||||
"hardwareBrightness",
|
||||
"softwareBrightness",
|
||||
]
|
||||
|
||||
private let volumeControlTargets: Set<String> = [
|
||||
"volume",
|
||||
"mute",
|
||||
]
|
||||
|
||||
// MARK: - BetterDisplay Manager
|
||||
|
||||
/// Manages integration with the BetterDisplay app (waydabber.BetterDisplay).
|
||||
///
|
||||
/// Responsibilities:
|
||||
/// - Detect whether BetterDisplay is installed
|
||||
/// - Observe OSD notifications from BetterDisplay and route them to Atoll's HUD pipeline
|
||||
/// - Provide request/response primitives for controlling display properties
|
||||
@MainActor
|
||||
final class BetterDisplayManager: ObservableObject {
|
||||
static let shared = BetterDisplayManager()
|
||||
|
||||
/// The bundle identifier of BetterDisplay.
|
||||
nonisolated static let bundleID = "pro.betterdisplay.BetterDisplay"
|
||||
|
||||
// Notification names
|
||||
private static let osdNotificationName = NSNotification.Name("com.betterdisplay.BetterDisplay.osd")
|
||||
private static let requestNotificationName = NSNotification.Name("com.betterdisplay.BetterDisplay.request")
|
||||
private static let responseNotificationName = NSNotification.Name("com.betterdisplay.BetterDisplay.response")
|
||||
|
||||
// MARK: Published state
|
||||
|
||||
/// Whether BetterDisplay is currently detected on this machine.
|
||||
@Published private(set) var isDetected: Bool = false
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private var osdObserver: NSObjectProtocol?
|
||||
private var responseObserver: NSObjectProtocol?
|
||||
private var workspaceObserver: NSObjectProtocol?
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private weak var coordinator: DynamicIslandViewCoordinator?
|
||||
|
||||
private init() {
|
||||
isDetected = Self.checkInstallation()
|
||||
setupWorkspaceObserver()
|
||||
setupSettingsObserver()
|
||||
}
|
||||
|
||||
// MARK: - Public API
|
||||
|
||||
/// Configure with the view coordinator for HUD dispatch.
|
||||
func configure(coordinator: DynamicIslandViewCoordinator) {
|
||||
self.coordinator = coordinator
|
||||
|
||||
// Start listening if integration is enabled and BetterDisplay is detected
|
||||
if Defaults[.enableBetterDisplayIntegration] && isDetected {
|
||||
startListening()
|
||||
}
|
||||
}
|
||||
|
||||
/// Refresh detection status (e.g. after app install/uninstall).
|
||||
func refreshDetectionStatus() {
|
||||
let wasDetected = isDetected
|
||||
isDetected = Self.checkInstallation()
|
||||
if wasDetected && !isDetected {
|
||||
stopListening()
|
||||
} else if !wasDetected && isDetected && Defaults[.enableBetterDisplayIntegration] {
|
||||
startListening()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Detection
|
||||
|
||||
/// Check if BetterDisplay is installed by looking for its bundle ID in running apps
|
||||
/// and falling back to NSWorkspace URL lookup.
|
||||
static func checkInstallation() -> Bool {
|
||||
// Check running apps first (fast path)
|
||||
if NSWorkspace.shared.runningApplications.contains(where: { $0.bundleIdentifier == bundleID }) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Fallback: check if the app is installed via URL scheme or bundle lookup
|
||||
if let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID) {
|
||||
return FileManager.default.fileExists(atPath: url.path)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// MARK: - OSD Listening
|
||||
|
||||
private func startListening() {
|
||||
guard osdObserver == nil else { return }
|
||||
|
||||
osdObserver = DistributedNotificationCenter.default().addObserver(
|
||||
forName: Self.osdNotificationName,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { [weak self] notification in
|
||||
Task { @MainActor in
|
||||
self?.handleOSDNotification(notification)
|
||||
}
|
||||
}
|
||||
|
||||
NSLog("✅ BetterDisplay OSD listener started")
|
||||
}
|
||||
|
||||
private func stopListening() {
|
||||
if let observer = osdObserver {
|
||||
DistributedNotificationCenter.default().removeObserver(observer)
|
||||
osdObserver = nil
|
||||
NSLog("⏹ BetterDisplay OSD listener stopped")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - OSD Handling
|
||||
|
||||
private func handleOSDNotification(_ notification: Notification) {
|
||||
guard Defaults[.enableBetterDisplayIntegration] else { return }
|
||||
|
||||
guard let notificationString = notification.object as? String else {
|
||||
NSLog("⚠️ BetterDisplay OSD: unexpected notification format")
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let osd = try JSONDecoder().decode(
|
||||
BetterDisplayOSDNotification.self,
|
||||
from: Data(notificationString.utf8)
|
||||
)
|
||||
routeOSDToHUD(osd)
|
||||
} catch {
|
||||
NSLog("⚠️ BetterDisplay OSD decode error: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Route a decoded BetterDisplay OSD notification to the active Atoll HUD variant.
|
||||
private func routeOSDToHUD(_ osd: BetterDisplayOSDNotification) {
|
||||
let category = classifyControlTarget(osd.controlTarget, systemIconID: osd.systemIconID)
|
||||
let normalizedValue = normalizeValue(osd.value, maxValue: osd.maxValue)
|
||||
|
||||
switch category {
|
||||
case .brightness:
|
||||
guard Defaults[.enableBrightnessHUD] else { return }
|
||||
dispatchBrightnessHUD(value: normalizedValue)
|
||||
|
||||
case .volume:
|
||||
guard Defaults[.enableVolumeHUD] else { return }
|
||||
let isMuted = osd.controlTarget == "mute" || osd.systemIconID == 4
|
||||
dispatchVolumeHUD(value: normalizedValue, isMuted: isMuted)
|
||||
|
||||
case .other:
|
||||
// For unsupported control targets (contrast, gamma, temperature, etc.),
|
||||
// show a generic brightness-style HUD if the user has brightness HUD enabled
|
||||
guard Defaults[.enableBrightnessHUD] else { return }
|
||||
dispatchBrightnessHUD(value: normalizedValue, customSymbol: osd.customSymbol)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - HUD Dispatch (mirrors SystemChangesObserver logic)
|
||||
|
||||
private func dispatchVolumeHUD(value: CGFloat, isMuted: Bool) {
|
||||
if HUDSuppressionCoordinator.shared.shouldSuppressVolumeHUD { return }
|
||||
|
||||
if Defaults[.enableCircularHUD] {
|
||||
CircularHUDWindowManager.shared.show(type: .volume, value: value)
|
||||
return
|
||||
}
|
||||
if Defaults[.enableVerticalHUD] {
|
||||
VerticalHUDWindowManager.shared.show(type: .volume, value: value, icon: "")
|
||||
return
|
||||
}
|
||||
if Defaults[.enableCustomOSD] && Defaults[.enableOSDVolume] {
|
||||
CustomOSDWindowManager.shared.showVolume(value: value)
|
||||
}
|
||||
if Defaults[.enableSystemHUD] && !Defaults[.enableCustomOSD] && !Defaults[.enableVerticalHUD] && !Defaults[.enableCircularHUD] {
|
||||
coordinator?.toggleSneakPeek(
|
||||
status: true,
|
||||
type: .volume,
|
||||
value: value,
|
||||
icon: ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func dispatchBrightnessHUD(value: CGFloat, customSymbol: String? = nil) {
|
||||
if Defaults[.enableCircularHUD] {
|
||||
CircularHUDWindowManager.shared.show(type: .brightness, value: value)
|
||||
return
|
||||
}
|
||||
if Defaults[.enableVerticalHUD] {
|
||||
VerticalHUDWindowManager.shared.show(type: .brightness, value: value)
|
||||
return
|
||||
}
|
||||
if Defaults[.enableCustomOSD] && Defaults[.enableOSDBrightness] {
|
||||
CustomOSDWindowManager.shared.showBrightness(value: value)
|
||||
}
|
||||
if Defaults[.enableSystemHUD] && !Defaults[.enableCustomOSD] && !Defaults[.enableVerticalHUD] && !Defaults[.enableCircularHUD] {
|
||||
coordinator?.toggleSneakPeek(
|
||||
status: true,
|
||||
type: .brightness,
|
||||
value: value,
|
||||
icon: ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func classifyControlTarget(_ target: String?, systemIconID: Int?) -> BetterDisplayControlCategory {
|
||||
if let target {
|
||||
if brightnessControlTargets.contains(target) { return .brightness }
|
||||
if volumeControlTargets.contains(target) { return .volume }
|
||||
}
|
||||
|
||||
// Fallback to systemIconID
|
||||
switch systemIconID {
|
||||
case 1: return .brightness // brightness icon
|
||||
case 3: return .volume // volume icon
|
||||
case 4: return .volume // mute icon
|
||||
default: 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 }
|
||||
let maxVal = maxValue ?? 1.0
|
||||
guard maxVal > 0 else { return 0 }
|
||||
return CGFloat(Swift.min(Swift.max(value / maxVal, 0), 1))
|
||||
}
|
||||
|
||||
// MARK: - Request API
|
||||
|
||||
/// Send a command to BetterDisplay and optionally receive a response.
|
||||
/// - Parameters:
|
||||
/// - commands: e.g. ["set"], ["get"]
|
||||
/// - parameters: e.g. ["brightness": "0.8"]
|
||||
/// - completion: Called with the response on the main queue, or nil on timeout.
|
||||
func sendRequest(
|
||||
commands: [String],
|
||||
parameters: [String: String?],
|
||||
completion: (@MainActor @Sendable (BetterDisplayResponseData?) -> Void)? = nil
|
||||
) {
|
||||
let uuid = UUID().uuidString
|
||||
let request = BetterDisplayRequestData(uuid: uuid, commands: commands, parameters: parameters)
|
||||
|
||||
// If we need a response, set up a temporary observer
|
||||
if let completion {
|
||||
// Use a class wrapper so the observer closure can safely capture & cancel
|
||||
final class ResponseState: @unchecked Sendable {
|
||||
var observer: NSObjectProtocol?
|
||||
var timeoutItem: DispatchWorkItem?
|
||||
}
|
||||
let state = ResponseState()
|
||||
|
||||
let timeoutItem = DispatchWorkItem {
|
||||
if let obs = state.observer {
|
||||
DistributedNotificationCenter.default().removeObserver(obs)
|
||||
state.observer = nil
|
||||
}
|
||||
Task { @MainActor in
|
||||
completion(nil)
|
||||
}
|
||||
}
|
||||
state.timeoutItem = timeoutItem
|
||||
|
||||
state.observer = DistributedNotificationCenter.default().addObserver(
|
||||
forName: Self.responseNotificationName,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { notification in
|
||||
guard let responseString = notification.object as? String,
|
||||
let response = try? JSONDecoder().decode(
|
||||
BetterDisplayResponseData.self,
|
||||
from: Data(responseString.utf8)
|
||||
),
|
||||
response.uuid == uuid
|
||||
else { return }
|
||||
|
||||
state.timeoutItem?.cancel()
|
||||
if let obs = state.observer {
|
||||
DistributedNotificationCenter.default().removeObserver(obs)
|
||||
state.observer = nil
|
||||
}
|
||||
Task { @MainActor in
|
||||
completion(response)
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0, execute: timeoutItem)
|
||||
}
|
||||
|
||||
// Send the request
|
||||
do {
|
||||
let encoded = try JSONEncoder().encode(request)
|
||||
if let encodedString = String(data: encoded, encoding: .utf8) {
|
||||
DistributedNotificationCenter.default().postNotificationName(
|
||||
Self.requestNotificationName,
|
||||
object: encodedString,
|
||||
userInfo: nil,
|
||||
deliverImmediately: true
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
NSLog("⚠️ BetterDisplay request encode error: \(error.localizedDescription)")
|
||||
completion?(nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Workspace Observer (detect install/uninstall)
|
||||
|
||||
private func setupWorkspaceObserver() {
|
||||
let betterDisplayBundleID = Self.bundleID
|
||||
|
||||
workspaceObserver = NSWorkspace.shared.notificationCenter.addObserver(
|
||||
forName: NSWorkspace.didLaunchApplicationNotification,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { [weak self] notification in
|
||||
guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication,
|
||||
app.bundleIdentifier == betterDisplayBundleID
|
||||
else { return }
|
||||
Task { @MainActor in
|
||||
self?.refreshDetectionStatus()
|
||||
}
|
||||
}
|
||||
|
||||
// Also observe app termination
|
||||
NSWorkspace.shared.notificationCenter.addObserver(
|
||||
forName: NSWorkspace.didTerminateApplicationNotification,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { notification in
|
||||
guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication,
|
||||
app.bundleIdentifier == betterDisplayBundleID
|
||||
else { return }
|
||||
// Don't clear isDetected on termination — app is still installed, just not running.
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Settings Observer
|
||||
|
||||
private func setupSettingsObserver() {
|
||||
Defaults.publisher(.enableBetterDisplayIntegration, options: [])
|
||||
.sink { [weak self] change in
|
||||
Task { @MainActor in
|
||||
guard let self else { return }
|
||||
if change.newValue && self.isDetected {
|
||||
self.startListening()
|
||||
} else {
|
||||
self.stopListening()
|
||||
}
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let osdObserver {
|
||||
DistributedNotificationCenter.default().removeObserver(osdObserver)
|
||||
}
|
||||
if let workspaceObserver {
|
||||
NSWorkspace.shared.notificationCenter.removeObserver(workspaceObserver)
|
||||
}
|
||||
cancellables.removeAll()
|
||||
}
|
||||
}
|
||||
|
|
@ -1010,6 +1010,9 @@ extension Defaults.Keys {
|
|||
static let circularHUDStrokeWidth = Key<CGFloat>("circularHUDStrokeWidth", default: 4)
|
||||
static let circularHUDUseAccentColor = Key<Bool>("circularHUDUseAccentColor", default: true)
|
||||
|
||||
// MARK: BetterDisplay Integration
|
||||
static let enableBetterDisplayIntegration = Key<Bool>("enableBetterDisplayIntegration", default: false)
|
||||
|
||||
static let hasSeenOSDAlphaWarning = Key<Bool>("hasSeenOSDAlphaWarning", default: false)
|
||||
static let enableOSDVolume = Key<Bool>("enableOSDVolume", default: true)
|
||||
static let enableOSDBrightness = Key<Bool>("enableOSDBrightness", default: true)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue