Third-party app integration
Mac apps can send BetterDisplay requests through DistributedNotificationCenter and receive matching responses. BetterDisplay can also broadcast adjustment feedback for another app to display its own on-screen display (OSD), allowing a shared visual style across brightness, volume and other adjustments.
Configure an alternate OSD
MediaMate and DynamicLake are examples of apps with BetterDisplay OSD integration; DynamicLakePro introduced this support in version 3.0. Enable Settings > Application > Integration > Dispatch OSD notifications, then configure BetterDisplay support in the receiving app.
Dispatching feedback does not suppress BetterDisplay's own overlays. Choose the desired feedback presentation in Keyboard settings to avoid two overlays for the same adjustment. The Integration settings article explains the app's duplicate-feedback warning.
The sections below are for developers implementing a receiving app. Notifications carry JSON in the notification's object string, not in userInfo.
Send a request and match its response
Enable CLI access and notification based integration in BetterDisplay. Listen for pro.betterdisplay.BetterDisplay.response before posting a request to pro.betterdisplay.BetterDisplay.request. Use a unique UUID to match each response to its request, particularly when other integrations might be active.
The JSON models are:
struct IntegrationNotificationRequestData: Codable {
var uuid: String?
var commands: [String] = []
var parameters: [String: String?] = [:]
}
struct IntegrationNotificationResponseData: Codable {
var uuid: String?
var result: Bool?
var payload: String?
}
commands contains operation names such as get, set or perform. parameters maps parameter names to string values, or to a valueless parameter. Use the names from the CLI reference, without CLI hyphens. uuid is echoed in the response; it can be omitted, but then correlation is harder. Include the required commands and parameters fields even when they are empty.
result reports operation success or failure, and payload contains any returned text in the format appropriate to that operation. All response fields are optional. Do not assume that every payload is itself JSON, or that an accepted request proves a physical device completed the change.
For example, this sends a request to set all eligible displays to 80% brightness:
import Foundation
let request = IntegrationNotificationRequestData(
uuid: UUID().uuidString,
commands: ["set"],
parameters: ["brightness": "0.8"]
)
let data = try JSONEncoder().encode(request)
if let json = String(data: data, encoding: .utf8) {
DistributedNotificationCenter.default().postNotificationName(
.init("pro.betterdisplay.BetterDisplay.request"),
object: json,
userInfo: nil,
deliverImmediately: true
)
}
Add "name": "Desk Display" to target one named display. An observer can decode responses like this:
final class BetterDisplayResponses: NSObject {
override init() {
super.init()
DistributedNotificationCenter.default().addObserver(
self,
selector: #selector(receive(_:)),
name: .init("pro.betterdisplay.BetterDisplay.response"),
object: nil
)
}
@objc private func receive(_ notification: Notification) {
guard let json = notification.object as? String,
let response = try? JSONDecoder().decode(
IntegrationNotificationResponseData.self,
from: Data(json.utf8)
) else { return }
// Match response.uuid to a pending request, then handle result/payload.
print(response.uuid ?? "", response.result as Any, response.payload as Any)
}
deinit {
DistributedNotificationCenter.default().removeObserver(self)
}
}
Keep the observer alive for the integration's lifetime. Apply a timeout to pending requests: a stopped app, disabled integration or malformed request may produce no response. The betterdisplaycli source provides a complete companion-client example using this transport.
Detect launch, quit and legacy clients
BetterDisplay posts pro.betterdisplay.BetterDisplay.launched after initial app and device configuration, when normal operation is ready. It posts pro.betterdisplay.BetterDisplay.terminated during orderly shutdown. Both require notification integration to be enabled. An unexpected termination does not produce a reliable quit notification, so a client should still handle unanswered requests.
These lifecycle notifications were introduced in 4.2.1. Treat them as signals rather than relying on their object contents; there are no equivalent lifecycle names under the legacy prefix.
Older integrations used com.betterdisplay.BetterDisplay. Current versions accept requests under both prefixes and respond using the request's prefix. OSD notifications are broadcast under both prefixes. New clients should use pro.betterdisplay.BetterDisplay and subscribe to only that OSD name to avoid handling each event twice. The wiki's historical transition was from the old prefix before 4.2.1 to the preferred new prefix after 4.2.2.
Receive OSD feedback
Listen for pro.betterdisplay.BetterDisplay.osd. Decode the object string using this model; all fields are optional:
struct OsdNotification: 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?
}
| Field | How to interpret it |
|---|---|
displayID |
The display on which to present the feedback. |
systemIconID |
1 for brightness, 3 for volume, 4 for mute; 0 represents no icon in the historical protocol, and the current sender can omit the field. |
customSymbol |
An SF Symbol name; when a system icon is also supplied, it describes a secondary symbol. |
text |
Additional feedback text, which may already contain formatted values. |
lock |
Whether to show a lock indicator when supplied. |
controlTarget |
A string identifying the adjustment or command. |
value, maxValue |
The value and its upper bound; do not assume the maximum is always 1 or 100. |
symbolFadeAfter, textFadeAfter |
Optional fade delays in milliseconds. |
symbolSizeMultiplier |
Optional symbol sizing relative to the receiver's normal size. |
A receiver may support a subset of these presentation hints. Handle missing values and unknown controlTarget strings gracefully rather than rejecting an otherwise useful notification. Guard against a missing or zero maximum before calculating a fraction. A simple action can carry an icon without a numeric value.
final class BetterDisplayOSDReceiver: NSObject {
override init() {
super.init()
DistributedNotificationCenter.default().addObserver(
self,
selector: #selector(receive(_:)),
name: .init("pro.betterdisplay.BetterDisplay.osd"),
object: nil
)
}
@objc private func receive(_ notification: Notification) {
guard let json = notification.object as? String,
let osd = try? JSONDecoder().decode(
OsdNotification.self, from: Data(json.utf8)
) else { return }
// Present your overlay using whichever optional fields you support.
print(osd.controlTarget ?? "", osd.value as Any)
}
deinit {
DistributedNotificationCenter.default().removeObserver(self)
}
}
Control target identifiers
Established identifiers include the following families. They are strings, not a closed enum in the notification contract; custom controls and newer app versions can add others.
- Brightness:
combinedBrightness,hardwareBrightness,softwareBrightness. - Audio:
volume,mute. - Hardware image adjustments:
hardwareContrast,redHardwareBlackLevel,greenHardwareBlackLevel,blueHardwareBlackLevel,redHardwareGain,greenHardwareGain,blueHardwareGain. - Software image adjustments:
gain,gamma,rGamma,gGamma,bGamma,temperature,contrast. - Other established identifiers:
blueLight,underscan.
Current adjustment families also include RGB gain, saturation, hue, quantization, inversion and compositor filters. Use controlTarget to refine an icon or label where recognized, with a fallback to the supplied symbol and text for everything else.
Coordinate feedback settings through requests
Use the request/response interface to manage a running app's preferences. Writing its preferences database directly is unreliable for a live handshake because the app and macOS may cache values.
First verify that BetterDisplay is running, then query a lightweight property such as proAvailable. A response proves the request path is working; no response does not distinguish a stopped app from disabled integration. A false value is a license-state result, not a transport failure.
Read and save the user's current osdShowBasic, osdShowCustom and osdIntegrationNotification values before changing them. When your app takes over feedback, enable dispatch and disable whichever BetterDisplay overlays you replace:
betterdisplaycli get -osdShowBasic -osdShowCustom -osdIntegrationNotification
betterdisplaycli set -osdIntegrationNotification=on -osdShowBasic=off -osdShowCustom=off
On shutdown or when integration is disabled in your app, restore the values you saved. Do not blindly turn all settings on or off. The same parameters work in DNC requests and can be read again to check the resulting app state.
Additional information
This section explains licensing and platform requirements for the integration transport.
Features requiring Pro license
The current request transport and OSD dispatch preference have no separate Pro requirement. Individual requested actions keep their own requirements. Older integration guidance required checking for Pro before OSD dispatch; clients should not treat that historical requirement as a current blanket restriction.
Platform compatibility
The notification interface is available on both Apple silicon and Intel Macs and communicates with cooperating local macOS apps. It is not a network notification transport. The receiving app is responsible for its own compatibility, UI and handling of unknown or missing fields.