Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 32 additions & 21 deletions MonitorControl/Model/Display.swift
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,6 @@ class Display: Equatable {
_ = self.setDirectBrightness(self.smoothBrightnessTransient, transient: true)
self.smoothBrightnessRunning = false
}
self.swBrightnessSemaphore.signal()
return true
}

Expand Down Expand Up @@ -191,7 +190,10 @@ class Display: Equatable {
guard !self.isDummy else {
return
}
CGGetDisplayTransferByTable(self.identifier, 256, &self.defaultGammaTableRed, &self.defaultGammaTableGreen, &self.defaultGammaTableBlue, &self.defaultGammaTableSampleCount)
guard CGGetDisplayTransferByTable(self.identifier, 256, &self.defaultGammaTableRed, &self.defaultGammaTableGreen, &self.defaultGammaTableBlue, &self.defaultGammaTableSampleCount) == CGError.success else {
self.defaultGammaTablePeak = 1.0
return
}
let redPeak = self.defaultGammaTableRed.max() ?? 0
let greenPeak = self.defaultGammaTableGreen.max() ?? 0
let bluePeak = self.defaultGammaTableBlue.max() ?? 0
Expand Down Expand Up @@ -222,14 +224,16 @@ class Display: Equatable {
currentValue = self.swBrightnessTransform(value: currentValue)
newValue = self.swBrightnessTransform(value: newValue)
if smooth {
self.swBrightnessSemaphore.signal()
DispatchQueue.global(qos: .userInteractive).async {
for transientValue in stride(from: currentValue, to: newValue, by: 0.005 * (currentValue > newValue ? -1 : 1)) {
guard app.reconfigureID == 0 else {
self.swBrightnessSemaphore.signal()
return
}
if self.isVirtual || self.readPrefAsBool(key: .avoidGamma) {
_ = DisplayManager.shared.setShadeAlpha(value: 1 - transientValue, displayID: DisplayManager.resolveEffectiveDisplayID(self.identifier))
DispatchQueue.main.async {
_ = DisplayManager.shared.setShadeAlpha(value: 1 - transientValue, displayID: DisplayManager.resolveEffectiveDisplayID(self.identifier))
}
} else {
let gammaTableRed = self.defaultGammaTableRed.map { $0 * transientValue }
let gammaTableGreen = self.defaultGammaTableGreen.map { $0 * transientValue }
Expand All @@ -239,6 +243,7 @@ class Display: Equatable {
Thread.sleep(forTimeInterval: 0.001) // Let's make things quick if not performed in the background
}
}
return true
Comment on lines 226 to +246
} else {
if self.isVirtual || self.readPrefAsBool(key: .avoidGamma) {
self.swBrightnessSemaphore.signal()
Expand Down Expand Up @@ -270,6 +275,10 @@ class Display: Equatable {
self.swBrightnessSemaphore.signal()
return self.swBrightnessTransform(value: rawBrightnessValue, reverse: true)
}
guard self.defaultGammaTablePeak != 0 else {
self.swBrightnessSemaphore.signal()
return 1.0
}
var gammaTableRed = [CGGammaValue](repeating: 0, count: 256)
var gammaTableGreen = [CGGammaValue](repeating: 0, count: 256)
var gammaTableBlue = [CGGammaValue](repeating: 0, count: 256)
Expand Down Expand Up @@ -297,24 +306,26 @@ class Display: Equatable {
os_log("Gamma table interference detected, number of events: %{public}@", type: .info, String(DisplayManager.shared.gammaInterferenceCounter))
if DisplayManager.shared.gammaInterferenceCounter >= 3 {
DisplayManager.shared.gammaInterferenceWarningShown = true
let alert = NSAlert()
alert.messageText = NSLocalizedString("Is f.lux or similar running?", comment: "Shown in the alert dialog")
alert.informativeText = NSLocalizedString("An other app seems to change the brightness or colors which causes issues.\n\nTo solve this, you need to quit the other app or disable gamma control for your displays in MonitorControl!", comment: "Shown in the alert dialog")
alert.addButton(withTitle: NSLocalizedString("I'll quit the other app", comment: "Shown in the alert dialog"))
alert.addButton(withTitle: NSLocalizedString("Disable gamma control for my displays", comment: "Shown in the alert dialog"))
alert.alertStyle = NSAlert.Style.critical
if alert.runModal() != .alertFirstButtonReturn {
for otherDisplay in DisplayManager.shared.getOtherDisplays() {
_ = otherDisplay.setSwBrightness(1)
_ = otherDisplay.setDirectBrightness(1)
otherDisplay.savePref(true, key: .avoidGamma)
_ = otherDisplay.setSwBrightness(1)
DisplayManager.shared.gammaInterferenceWarningShown = false
DisplayManager.shared.gammaInterferenceCounter = 0
displaysPrefsVc?.loadDisplayList()
DispatchQueue.main.async {
let alert = NSAlert()
alert.messageText = NSLocalizedString("Is f.lux or similar running?", comment: "Shown in the alert dialog")
alert.informativeText = NSLocalizedString("An other app seems to change the brightness or colors which causes issues.\n\nTo solve this, you need to quit the other app or disable gamma control for your displays in MonitorControl!", comment: "Shown in the alert dialog")
alert.addButton(withTitle: NSLocalizedString("I'll quit the other app", comment: "Shown in the alert dialog"))
alert.addButton(withTitle: NSLocalizedString("Disable gamma control for my displays", comment: "Shown in the alert dialog"))
alert.alertStyle = NSAlert.Style.critical
if alert.runModal() != .alertFirstButtonReturn {
for otherDisplay in DisplayManager.shared.getOtherDisplays() {
_ = otherDisplay.setSwBrightness(1)
_ = otherDisplay.setDirectBrightness(1)
otherDisplay.savePref(true, key: .avoidGamma)
_ = otherDisplay.setSwBrightness(1)
DisplayManager.shared.gammaInterferenceWarningShown = false
DisplayManager.shared.gammaInterferenceCounter = 0
displaysPrefsVc?.loadDisplayList()
}
} else {
os_log("We won't watch for gamma table interference anymore", type: .info)
}
} else {
os_log("We won't watch for gamma table interference anymore", type: .info)
}
}
}
Expand Down
33 changes: 24 additions & 9 deletions MonitorControl/Model/OtherDisplay.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,6 @@ class OtherDisplay: Display {
case .contrast: currentDDCValue = UInt16(Float(DDC_MAX_DETECT_LIMIT) * 0.750)
default: currentDDCValue = UInt16(Float(DDC_MAX_DETECT_LIMIT) * 1.000)
}
if command == .audioSpeakerVolume {
currentDDCValue = UInt16(Float(DDC_MAX_DETECT_LIMIT) * 0.125) // lower default audio value as high volume might rattle the user.
}
os_log("Setting up display %{public}@ for %{public}@", type: .info, String(self.identifier), String(reflecting: command))
if !self.isSw() {
if prefs.integer(forKey: PrefKey.startupAction.rawValue) == StartupAction.read.rawValue, self.pollingCount != 0, !app.safeMode {
Expand Down Expand Up @@ -399,21 +396,33 @@ class OtherDisplay: Display {
guard value != UInt16.max, value != lastValue else {
return
}
self.writeDDCQueue.async(flags: .barrier) {
self.writeDDCLastSavedValue[command] = value
self.savePref(true, key: PrefKey.isTouched, for: command)
}
var controlCodes = self.getRemapControlCodes(command: command)
if controlCodes.count == 0 {
controlCodes.append(command.rawValue)
}
var writeAttempted = false
var allSucceeded = true
for controlCode in controlCodes {
if Arm64DDC.isArm64 {
if self.arm64ddc {
_ = Arm64DDC.write(service: self.arm64avService, command: controlCode, value: value)
writeAttempted = true
if !Arm64DDC.write(service: self.arm64avService, command: controlCode, value: value) {
allSucceeded = false
}
}
} else {
_ = self.ddc?.write(command: controlCode, value: value, errorRecoveryWaitTime: 2000) ?? false
if self.ddc != nil {
writeAttempted = true
if !(self.ddc?.write(command: controlCode, value: value, errorRecoveryWaitTime: 2000) ?? false) {
allSucceeded = false
}
}
}
}
if writeAttempted, allSucceeded {
self.writeDDCQueue.async(flags: .barrier) {
self.writeDDCLastSavedValue[command] = value
self.savePref(true, key: PrefKey.isTouched, for: command)
}
}
Comment on lines +403 to 427
}
Expand Down Expand Up @@ -487,6 +496,9 @@ class OtherDisplay: Display {
let curveMultiplier = self.getCurveMultiplier(self.readPrefAsInt(key: .curveDDC, for: command))
let minDDCValue = Float(self.readPrefAsInt(key: .minDDCOverride, for: command))
let maxDDCValue = Float(self.readPrefAsInt(key: .maxDDC, for: command))
guard maxDDCValue != minDDCValue else {
return UInt16(minDDCValue)
}
let curvedValue = pow(max(min(value, 1), 0), curveMultiplier)
let deNormalizedValue = (maxDDCValue - minDDCValue) * curvedValue + minDDCValue
var intDDCValue = UInt16(min(max(deNormalizedValue.rounded(), minDDCValue), maxDDCValue))
Expand All @@ -500,6 +512,9 @@ class OtherDisplay: Display {
let curveMultiplier = self.getCurveMultiplier(self.readPrefAsInt(key: .curveDDC, for: command))
let minDDCValue = Float(self.readPrefAsInt(key: .minDDCOverride, for: command))
let maxDDCValue = Float(self.readPrefAsInt(key: .maxDDC, for: command))
guard maxDDCValue != minDDCValue else {
return 0
}
let normalizedValue = ((min(max(Float(from), minDDCValue), maxDDCValue) - minDDCValue) / (maxDDCValue - minDDCValue))
let deCurvedValue = pow(normalizedValue, 1.0 / curveMultiplier)
var value = deCurvedValue
Expand Down
69 changes: 48 additions & 21 deletions MonitorControl/Support/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
var accessibilityObserver: NSObjectProtocol!
var statusItemObserver: NSObjectProtocol!
var statusItemVisibilityChangedByUser = true
var configureID: Int = 0 // dispatched configure command ID
var reconfigureID: Int = 0 // dispatched reconfigure command ID
var sleepID: Int = 0 // sleep event ID
var safeMode = false
Expand Down Expand Up @@ -156,22 +157,45 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
os_log("Request for configuration with reconfigreID %{public}@", type: .info, String(dispatchedReconfigureID))
self.reconfigureID = 0
self.configureID += 1
let dispatchedConfigureID = self.configureID
DisplayManager.shared.gammaInterferenceCounter = 0
DisplayManager.shared.configureDisplays()
DisplayManager.shared.addDisplayCounterSuffixes()
DisplayManager.shared.updateArm64AVServices()
if firstrun && prefs.integer(forKey: PrefKey.startupAction.rawValue) != StartupAction.write.rawValue {
DisplayManager.shared.resetSwBrightnessForAllDisplays(prefsOnly: true)
}
DisplayManager.shared.setupOtherDisplays(firstrun: firstrun)
self.updateMenusAndKeys()
if !firstrun || prefs.integer(forKey: PrefKey.startupAction.rawValue) == StartupAction.write.rawValue {
if !prefs.bool(forKey: PrefKey.disableCombinedBrightness.rawValue) {
DisplayManager.shared.restoreSwBrightnessForAllDisplays(async: !prefs.bool(forKey: PrefKey.disableSmoothBrightness.rawValue))
DispatchQueue.global(qos: .userInitiated).async {
guard self.sleepID == 0, self.reconfigureID == 0, dispatchedConfigureID == self.configureID else {
return
}
DisplayManager.shared.setupOtherDisplays(firstrun: firstrun)
guard self.sleepID == 0, self.reconfigureID == 0, dispatchedConfigureID == self.configureID else {
return
}
DispatchQueue.main.async {
guard self.sleepID == 0, self.reconfigureID == 0, dispatchedConfigureID == self.configureID else {
return
}
self.updateMenusAndKeys()
}
guard self.sleepID == 0, self.reconfigureID == 0, dispatchedConfigureID == self.configureID else {
return
}
if !firstrun || prefs.integer(forKey: PrefKey.startupAction.rawValue) == StartupAction.write.rawValue {
if !prefs.bool(forKey: PrefKey.disableCombinedBrightness.rawValue) {
DisplayManager.shared.restoreSwBrightnessForAllDisplays(async: !prefs.bool(forKey: PrefKey.disableSmoothBrightness.rawValue))
}
}
DispatchQueue.main.async {
guard self.sleepID == 0, self.reconfigureID == 0, dispatchedConfigureID == self.configureID else {
return
}
displaysPrefsVc?.loadDisplayList()
self.job(start: true)
}
}
displaysPrefsVc?.loadDisplayList()
self.job(start: true)
}

func updateMenusAndKeys() {
Expand Down Expand Up @@ -318,22 +342,25 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}

func setStartAtLogin(enabled: Bool) {
let identifier = "\(Bundle.main.bundleIdentifier!)Helper" as CFString
SMLoginItemSetEnabled(identifier, enabled)
let identifier = "\(Bundle.main.bundleIdentifier!)Helper"
if #available(macOS 13.0, *) {
let service = SMAppService.loginItem(identifier: identifier)
do {
if enabled {
try service.register()
} else {
try service.unregister()
}
} catch {
os_log("Unable to update login item registration: %{public}@", type: .error, error.localizedDescription)
}
} else {
SMLoginItemSetEnabled(identifier as CFString, enabled)
}
Comment on lines +346 to +359
Comment on lines +345 to +359
}

func getSystemSettings() -> [String: AnyObject]? {
var propertyListFormat = PropertyListSerialization.PropertyListFormat.xml
let plistPath = NSString(string: "~/Library/Preferences/.GlobalPreferences.plist").expandingTildeInPath
guard let plistXML = FileManager.default.contents(atPath: plistPath) else {
return nil
}
do {
return try PropertyListSerialization.propertyList(from: plistXML, options: .mutableContainersAndLeaves, format: &propertyListFormat) as? [String: AnyObject]
} catch {
os_log("Error reading system prefs plist: %{public}@", type: .info, error.localizedDescription)
return nil
}
func getSystemSettings() -> [String: Any]? {
UserDefaults.standard.persistentDomain(forName: UserDefaults.globalDomain)
}

func macOS10() -> Bool {
Expand Down
14 changes: 12 additions & 2 deletions MonitorControl/Support/Arm64DDC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ class Arm64DDC: NSObject {
var send: [UInt8] = [command]
var reply = [UInt8](repeating: 0, count: 11)
if Self.performDDCCommunication(service: service, send: &send, reply: &reply, writeSleepTime: writeSleepTime, numOfWriteCycles: numOfWriteCycles, readSleepTime: readSleepTime, numOfRetryAttemps: numOfRetryAttemps, retrySleepTime: retrySleepTime) {
guard reply.count > 9, reply[0] == ARM64_DDC_7BIT_ADDRESS << 1, reply[2] == 0x02, reply[4] == command else {
return nil
}
let max = UInt16(reply[6]) * 256 + UInt16(reply[7])
let current = UInt16(reply[8]) * 256 + UInt16(reply[9])
values = (current, max)
Expand All @@ -97,7 +100,8 @@ class Arm64DDC: NSObject {
}
var packet: [UInt8] = [UInt8(0x80 | (send.count + 1)), UInt8(send.count)] + send + [0] // Note: the last byte is the place of the checksum, see next line!
packet[packet.count - 1] = self.checksum(chk: send.count == 1 ? ARM64_DDC_7BIT_ADDRESS << 1 : ARM64_DDC_7BIT_ADDRESS << 1 ^ dataAddress, data: &packet, start: 0, end: packet.count - 2)
for _ in 1 ... (numOfRetryAttemps ?? 4) + 1 {
let attempts = min(Int(numOfRetryAttemps ?? 4) + 1, 30)
for _ in 0 ..< attempts {
for _ in 1 ... max((numOfWriteCycles ?? 2) + 0, 1) {
usleep(writeSleepTime ?? 10000)
success = IOAVServiceWriteI2C(service, UInt32(ARM64_DDC_7BIT_ADDRESS), UInt32(dataAddress), &packet, UInt32(packet.count)) == 0
Expand Down Expand Up @@ -232,7 +236,13 @@ class Arm64DDC: NSObject {
static func getIoregServicesForMatching() -> [IOregService] {
var serviceLocation = 0
var ioregServicesForMatching: [IOregService] = []
let ioregRoot: io_registry_entry_t = IORegistryGetRootEntry(kIOMasterPortDefault)
let masterPort: mach_port_t
if #available(macOS 12.0, *) {
masterPort = kIOMainPortDefault
} else {
masterPort = kIOMasterPortDefault
}
let ioregRoot: io_registry_entry_t = IORegistryGetRootEntry(masterPort)
Comment on lines +239 to +245
defer {
IOObjectRelease(ioregRoot)
}
Expand Down
24 changes: 5 additions & 19 deletions MonitorControl/Support/DisplayManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ class DisplayManager {
}

var shades: [CGDirectDisplayID: NSWindow] = [:]
var shadeGrave: [NSWindow] = []

func isDisqualifiedFromShade(_ displayID: CGDirectDisplayID) -> Bool {
if CGDisplayIsInHWMirrorSet(displayID) != 0 || CGDisplayIsInMirrorSet(displayID) != 0 {
Expand Down Expand Up @@ -117,7 +116,6 @@ class DisplayManager {
func destroyShade(displayID: CGDirectDisplayID) -> Bool {
if let shade = shades[displayID] {
os_log("Destroying shade for display %{public}@", type: .info, String(displayID))
self.shadeGrave.append(shade)
self.shades.removeValue(forKey: displayID)
shade.close()
return true
Expand Down Expand Up @@ -241,21 +239,6 @@ class DisplayManager {
self.displays.compactMap { $0 as? OtherDisplay }
}

func sortDisplays() {
// Opsiyonel: sıralamadan önce log al
let before = self.displays.map(\.name)
os_log("Displays before sorting: %{public}@", before)

// In‑place sıralama
self.displays.sort { lhs, rhs in
lhs.name.localizedStandardCompare(rhs.name) == .orderedAscending
}

// Opsiyonel: sıralamadan sonra log al
let after = self.displays.map(\.name)
os_log("Displays after sorting: %{public}@", after)
}

func sortDisplaysByFriendlyName() -> [Display] {
self.displays.sorted { lhs, rhs in
let lhsTitle = lhs.readPrefAsString(key: .friendlyName).isEmpty
Expand Down Expand Up @@ -337,11 +320,14 @@ class DisplayManager {
if Arm64DDC.isArm64 {
os_log("arm64 AVService update requested", type: .info)
var displayIDs: [CGDirectDisplayID] = []
for otherDisplay in self.getOtherDisplays() {
let otherDisplays = self.getOtherDisplays()
for otherDisplay in otherDisplays {
otherDisplay.arm64avService = nil
otherDisplay.arm64ddc = false
displayIDs.append(otherDisplay.identifier)
}
for serviceMatch in Arm64DDC.getServiceMatches(displayIDs: displayIDs) {
for otherDisplay in self.getOtherDisplays() where otherDisplay.identifier == serviceMatch.displayID && serviceMatch.service != nil {
for otherDisplay in otherDisplays where otherDisplay.identifier == serviceMatch.displayID && serviceMatch.service != nil {
otherDisplay.arm64avService = serviceMatch.service
os_log("Display service match successful for display %{public}@", type: .info, String(serviceMatch.displayID))
if serviceMatch.discouraged {
Expand Down
3 changes: 2 additions & 1 deletion MonitorControl/Support/MenuHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,12 @@ class MenuHandler: NSMenu, NSMenuDelegate {
displays = DisplayManager.shared.sortDisplaysByFriendlyName()
let relevant = prefs.integer(forKey: PrefKey.multiSliders.rawValue) == MultiSliders.relevant.rawValue
let combine = prefs.integer(forKey: PrefKey.multiSliders.rawValue) == MultiSliders.combine.rawValue
let currentDisplayID = currentDisplay.map { DisplayManager.resolveEffectiveDisplayID($0.identifier) }
let numOfDisplays = displays.filter { !$0.isDummy }.count
if numOfDisplays != 0 {
let asSubMenu: Bool = (displays.count > 3 && !relevant && !combine && app.macOS10()) ? true : false
var iterator = 0
for display in displays where (!relevant || DisplayManager.resolveEffectiveDisplayID(display.identifier) == DisplayManager.resolveEffectiveDisplayID(currentDisplay!.identifier)) && !display.isDummy {
for display in displays where (!relevant || currentDisplayID.map { DisplayManager.resolveEffectiveDisplayID(display.identifier) == $0 } ?? false) && !display.isDummy {
iterator += 1
if !relevant, !combine, iterator != 1, app.macOS10() {
self.insertItem(NSMenuItem.separator(), at: 0)
Expand Down
Loading