Update BetterDisplay

This commit is contained in:
Hariharan Mudaliar 2026-03-05 23:57:13 +05:30
commit d8ca8ceb02
No known key found for this signature in database
GPG key ID: 025583EB0BE5640E
3 changed files with 111 additions and 22 deletions

View file

@ -93,23 +93,33 @@ final class BetterDisplayManager: ObservableObject {
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")
private static let launchedNotificationName = NSNotification.Name("pro.betterdisplay.BetterDisplay.launched")
private static let terminatedNotificationName = NSNotification.Name("pro.betterdisplay.BetterDisplay.terminated")
// MARK: Published state
/// Whether BetterDisplay is currently detected on this machine.
/// Whether BetterDisplay is currently detected (installed) on this machine.
@Published private(set) var isDetected: Bool = false
/// Whether BetterDisplay is currently running and ready for communication.
@Published private(set) var isRunning: Bool = false
// MARK: Private
private var osdObserver: NSObjectProtocol?
private var responseObserver: NSObjectProtocol?
private var workspaceObserver: NSObjectProtocol?
private var workspaceTermObserver: NSObjectProtocol?
private var launchedObserver: NSObjectProtocol?
private var terminatedObserver: NSObjectProtocol?
private var cancellables = Set<AnyCancellable>()
private weak var coordinator: DynamicIslandViewCoordinator?
private init() {
isDetected = Self.checkInstallation()
isRunning = Self.checkRunning()
setupWorkspaceObserver()
setupLifecycleObservers()
setupSettingsObserver()
}
@ -119,8 +129,8 @@ final class BetterDisplayManager: ObservableObject {
func configure(coordinator: DynamicIslandViewCoordinator) {
self.coordinator = coordinator
// Start listening if integration is enabled and BetterDisplay is detected
if Defaults[.enableBetterDisplayIntegration] && isDetected {
// Start listening if integration is enabled and BetterDisplay is running
if Defaults[.enableBetterDisplayIntegration] && isRunning {
startListening()
}
}
@ -128,18 +138,19 @@ final class BetterDisplayManager: ObservableObject {
/// Refresh detection status (e.g. after app install/uninstall).
func refreshDetectionStatus() {
let wasDetected = isDetected
let wasRunning = isRunning
isDetected = Self.checkInstallation()
if wasDetected && !isDetected {
isRunning = Self.checkRunning()
if wasRunning && !isRunning {
stopListening()
} else if !wasDetected && isDetected && Defaults[.enableBetterDisplayIntegration] {
} else if !wasRunning && isRunning && 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.
/// Check if BetterDisplay is installed by looking for its bundle ID.
static func checkInstallation() -> Bool {
// Check running apps first (fast path)
if NSWorkspace.shared.runningApplications.contains(where: { $0.bundleIdentifier == bundleID }) {
@ -154,6 +165,11 @@ final class BetterDisplayManager: ObservableObject {
return false
}
/// Check if BetterDisplay is currently running.
static func checkRunning() -> Bool {
NSWorkspace.shared.runningApplications.contains(where: { $0.bundleIdentifier == bundleID })
}
// MARK: - OSD Listening
private func startListening() {
@ -183,7 +199,7 @@ final class BetterDisplayManager: ObservableObject {
// MARK: - OSD Handling
private func handleOSDNotification(_ notification: Notification) {
guard Defaults[.enableBetterDisplayIntegration] else { return }
guard Defaults[.enableBetterDisplayIntegration], isRunning else { return }
guard let notificationString = notification.object as? String else {
NSLog("⚠️ BetterDisplay OSD: unexpected notification format")
@ -345,6 +361,12 @@ final class BetterDisplayManager: ObservableObject {
parameters: [String: String?],
completion: (@MainActor @Sendable (BetterDisplayResponseData?) -> Void)? = nil
) {
guard isRunning else {
NSLog("⚠️ BetterDisplay sendRequest skipped — app is not running")
completion?(nil)
return
}
let uuid = UUID().uuidString
let request = BetterDisplayRequestData(uuid: uuid, commands: commands, parameters: parameters)
@ -429,16 +451,54 @@ final class BetterDisplayManager: ObservableObject {
}
}
// Also observe app termination
NSWorkspace.shared.notificationCenter.addObserver(
// Also observe app termination — handles crashes and force-quits
// (orderly quits are caught by the lifecycle observer below)
workspaceTermObserver = NSWorkspace.shared.notificationCenter.addObserver(
forName: NSWorkspace.didTerminateApplicationNotification,
object: nil,
queue: .main
) { notification in
) { [weak self] 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.
Task { @MainActor in
NSLog("🔴 BetterDisplay terminated (workspace notification)")
self?.isRunning = false
self?.stopListening()
}
}
}
// MARK: - Lifecycle Observers (BetterDisplay launched/terminated notifications)
/// Listen for distributed notifications sent by BetterDisplay itself
/// to know when it becomes ready and when it shuts down cleanly.
private func setupLifecycleObservers() {
launchedObserver = DistributedNotificationCenter.default().addObserver(
forName: Self.launchedNotificationName,
object: nil,
queue: .main
) { [weak self] _ in
Task { @MainActor in
NSLog("🟢 BetterDisplay launched notification received")
self?.isDetected = true
self?.isRunning = true
if Defaults[.enableBetterDisplayIntegration] {
self?.startListening()
}
}
}
terminatedObserver = DistributedNotificationCenter.default().addObserver(
forName: Self.terminatedNotificationName,
object: nil,
queue: .main
) { [weak self] _ in
Task { @MainActor in
NSLog("🔴 BetterDisplay terminated notification received")
self?.isRunning = false
self?.stopListening()
}
}
}
@ -449,7 +509,7 @@ final class BetterDisplayManager: ObservableObject {
.sink { [weak self] change in
Task { @MainActor in
guard let self else { return }
if change.newValue && self.isDetected {
if change.newValue && self.isRunning {
self.startListening()
} else {
self.stopListening()
@ -463,9 +523,18 @@ final class BetterDisplayManager: ObservableObject {
if let osdObserver {
DistributedNotificationCenter.default().removeObserver(osdObserver)
}
if let launchedObserver {
DistributedNotificationCenter.default().removeObserver(launchedObserver)
}
if let terminatedObserver {
DistributedNotificationCenter.default().removeObserver(terminatedObserver)
}
if let workspaceObserver {
NSWorkspace.shared.notificationCenter.removeObserver(workspaceObserver)
}
if let workspaceTermObserver {
NSWorkspace.shared.notificationCenter.removeObserver(workspaceTermObserver)
}
cancellables.removeAll()
}
}