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

@ -4795,6 +4795,9 @@
"BetterDisplay Integration" : {
"comment" : "The header text for the \"BetterDisplay Integration\" section in the settings view.",
"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." : {
@ -8517,10 +8520,6 @@
"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,
@ -17410,9 +17409,6 @@
"Not charging" : {
"comment" : "Text displayed in the status text when the battery is not charging.",
"isCommentAutoGenerated" : true
},
"Not detected" : {
},
"Not Now" : {
"localizations" : {

View file

@ -1934,6 +1934,26 @@ private struct BetterDisplayIntegrationSection: View {
SettingsTab.hudAndOSD.highlightID(for: title)
}
private var statusText: String {
if betterDisplayManager.isRunning {
return "Running"
} else if betterDisplayManager.isDetected {
return "Not running"
} else {
return "Not detected"
}
}
private var statusColor: Color {
if betterDisplayManager.isRunning {
return .green
} else if betterDisplayManager.isDetected {
return .orange
} else {
return .secondary
}
}
var body: some View {
Form {
Section {
@ -1941,9 +1961,9 @@ private struct BetterDisplayIntegrationSection: View {
VStack(alignment: .leading, spacing: 2) {
Text("BetterDisplay")
.font(.system(size: 13, weight: .medium))
Text(betterDisplayManager.isDetected ? "Detected" : "Not detected")
Text(statusText)
.font(.caption)
.foregroundStyle(betterDisplayManager.isDetected ? .green : .secondary)
.foregroundStyle(statusColor)
}
Spacer()
@ -1959,6 +1979,10 @@ private struct BetterDisplayIntegrationSection: View {
Text("Install [BetterDisplay](https://betterdisplay.pro) to control external display brightness and volume through Atoll's HUD.")
.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)

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()
}
}