Skip to content

Add a brightness offset option #5

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 21, 2019
Merged
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
146 changes: 100 additions & 46 deletions Brightness Sync/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,79 +4,98 @@ import os
class AppDelegate: NSObject, NSApplicationDelegate {
let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
let statusIndicator = NSMenuItem(title: "Starting", action: nil, keyEquivalent: "")


let updateInterval = 0.1
var syncTimer: Timer?

var lastBrightness: Double?


let brightnessOffsetKey = "BSBrightnessOffset"
var brightnessOffset: Double {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps having a different default value than 0 would be more appropriate. I'm using about -20% / - 25% to match up my MBP and the LG. On the other hand, this means the display is automatically deprived of it's max brightness, which somebody might not like. So 0 might be the safer option.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll go with 0. This should also be the correct setting for users still on Mojave.

get {
UserDefaults.standard.double(forKey: brightnessOffsetKey)
}
set (newValue) {
UserDefaults.standard.set(newValue, forKey: brightnessOffsetKey)
}
}

// CoreDisplay_Display_GetUserBrightness reports 1.0 for builtin display just before applicationDidChangeScreenParameters when closing lid.
// This is a workaround to restore the last sane value after syncing stops.
var lastSaneBrightness: Double?
var lastSaneBrightnessDelayTimer: Timer?

static let maxDisplays: UInt32 = 8

func applicationDidFinishLaunching(_ aNotification: Notification) {
if let button = statusItem.button {
button.image = #imageLiteral(resourceName: "StatusBarButtonImage")
}

let menu = NSMenu()
menu.addItem(statusIndicator)
menu.addItem(NSMenuItem.separator())


menu.addItem(NSMenuItem(title: "Brightness Offset", action: nil, keyEquivalent: ""))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open for suggestions for a better name for this. 🤷‍♂

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, can't think of any. I also wish I had a better name for the app than “Brightness Sync”...

let menuItem = NSMenuItem()
menuItem.view = sliderView
menu.addItem(menuItem)

menu.addItem(NSMenuItem.separator())

let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String
menu.addItem(NSMenuItem(title: "v\(appVersion)", action: nil, keyEquivalent: ""))
menu.addItem(NSMenuItem(title: "Check For Updates", action: #selector(checkForUpdates), keyEquivalent: ""))
menu.addItem(NSMenuItem.separator())

let copyDiagnosticsButton = NSMenuItem(title: "Copy Diagnostics", action: #selector(copyDiagnosticsToPasteboard), keyEquivalent: "c")
copyDiagnosticsButton.isHidden = true
copyDiagnosticsButton.allowsKeyEquivalentWhenHidden = true
menu.addItem(copyDiagnosticsButton)

menu.addItem(NSMenuItem(title: "Quit", action: #selector(NSApplication.terminate), keyEquivalent: ""))
statusItem.menu = menu

refresh()
}

func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}

func applicationDidChangeScreenParameters(_ notification: Notification) {
refresh()
}

func refresh() {
os_log("Starting display refresh...")

let allDisplays = AppDelegate.getAllDisplays()
let lgDisplayIdentifiers = AppDelegate.getConnectedUltraFineDisplayIdentifiers()

let builtin = allDisplays.first { CGDisplayIsBuiltin($0) == 1 }
let syncTo = allDisplays.filter { lgDisplayIdentifiers.contains(DisplayIdentifier(vendorNumber: CGDisplayVendorNumber($0), modelNumber: CGDisplayModelNumber($0))) }

syncTimer?.invalidate()

if let syncFrom = builtin, !syncTo.isEmpty {
syncTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { (_) -> Void in
let newBrightness = CoreDisplay_Display_GetUserBrightness(syncFrom)

syncTimer = Timer(timeInterval: updateInterval, repeats: true) { (_) in
let sourceBrightness = CoreDisplay_Display_GetUserBrightness(syncFrom)
let newBrightness = (sourceBrightness + self.brightnessOffset).clamped(to: 0.0...1.0)

if let oldBrightness = self.lastBrightness, abs(oldBrightness - newBrightness) < 0.01 {
return
}

for display in syncTo {
CoreDisplay_Display_SetUserBrightness(display, newBrightness)
}

self.lastBrightness = newBrightness

if newBrightness == 1, self.lastSaneBrightness != 1 {
let timerAlreadyRunning = self.lastSaneBrightnessDelayTimer?.isValid ?? false

if !timerAlreadyRunning {
self.lastSaneBrightnessDelayTimer = Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { (_) -> Void in
self.lastSaneBrightness = newBrightness
Expand All @@ -88,7 +107,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
self.lastSaneBrightness = newBrightness
}
}

RunLoop.current.add(syncTimer!, forMode: .common)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enables brightness adjustments while interacting with the slider.


statusIndicator.title = "Activated"
os_log("Activated...")
}
Expand All @@ -100,24 +120,24 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
lastSaneBrightness = nil
}

statusIndicator.title = "Deactivated"
os_log("Deactivated...")
}
}

static func getAllDisplays() -> [CGDirectDisplayID] {
var onlineDisplays = [CGDirectDisplayID](repeating: 0, count: Int(maxDisplays))
var displayCount: UInt32 = 0

CGGetOnlineDisplayList(maxDisplays, &onlineDisplays, &displayCount)

return Array(onlineDisplays[0..<Int(displayCount)])
}

static func getConnectedUltraFineDisplayIdentifiers() -> Set<DisplayIdentifier> {
var ultraFineDisplays = Set<DisplayIdentifier>()

for displayInfo in getDisplayInfoDictionaries() {
if
let displayNames = displayInfo[kDisplayProductName] as? NSDictionary,
Expand All @@ -139,61 +159,95 @@ class AppDelegate: NSObject, NSApplicationDelegate {
os_log("Display without en_US name found.")
}
}

return ultraFineDisplays
}

static func getDisplayInfoDictionaries() -> [NSDictionary] {
var diplayInfoDictionaries = [NSDictionary]()

var iterator: io_iterator_t = 0
guard IOServiceGetMatchingServices(kIOMasterPortDefault, IOServiceMatching("IODisplayConnect"), &iterator) == 0 else {
return diplayInfoDictionaries
}

var display = IOIteratorNext(iterator)

while display != 0 {
if let displayInfo = IODisplayCreateInfoDictionary(display, 0)?.takeRetainedValue() as NSDictionary? {
diplayInfoDictionaries.append(displayInfo)
}

IOObjectRelease(display)

display = IOIteratorNext(iterator)
}

IOObjectRelease(iterator)

return diplayInfoDictionaries
}

@objc func copyDiagnosticsToPasteboard() {
let CGDisplays = AppDelegate.getAllDisplays()
let IODisplays = AppDelegate.getDisplayInfoDictionaries()

let diagnostics = """
CGDisplayList:
\(CGDisplays.map {
["VendorNumber": CGDisplayVendorNumber($0),
"ModelNumber": CGDisplayModelNumber($0),
"SerialNumber": CGDisplaySerialNumber($0)]
})

IODisplayList:
\(IODisplays)
"""

NSPasteboard.general.clearContents()
NSPasteboard.general.setString(diagnostics, forType: .string)
}

@objc func checkForUpdates() {
NSWorkspace.shared.open(URL(string: "https://github.com/OCJvanDijk/Brightness-Sync/releases")!)
}


@objc func brightnessOffsetUpdated(slider: NSSlider) {
brightnessOffset = slider.doubleValue
syncTimer?.fire()
}

struct DisplayIdentifier: Hashable {
let vendorNumber: UInt32
let modelNumber: UInt32
}

private lazy var sliderView: NSView = {
let container = NSView(frame: NSRect(origin: CGPoint.zero, size: CGSize(width: 160, height: 28)))

let updateSel = #selector(brightnessOffsetUpdated(slider:))
let statusSlider = NSSlider(value: brightnessOffset, minValue: -0.5, maxValue: 0.5, target: self, action: updateSel)
container.addSubview(statusSlider)
statusSlider.translatesAutoresizingMaskIntoConstraints = false

statusSlider.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 20).isActive = true
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a better way to match up the slider to the text, than using a magic number (20) here?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I searched for it, but can't find anything. I made a screenshot of the system menu Volume slider and counted the pixels, so I'm going to just copy those measurements 😬

statusSlider.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive = true
statusSlider.topAnchor.constraint(equalTo: container.topAnchor).isActive = true
statusSlider.bottomAnchor.constraint(equalTo: container.bottomAnchor).isActive = true


return container
}()
}

extension Comparable {
func clamped(to limits: ClosedRange<Self>) -> Self {
return min(max(self, limits.lowerBound), limits.upperBound)
}
}

extension Strideable where Stride: SignedInteger {
func clamped(to limits: CountableClosedRange<Self>) -> Self {
return min(max(self, limits.lowerBound), limits.upperBound)
}
}