-
-
Notifications
You must be signed in to change notification settings - Fork 17
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 { | ||
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: "")) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Open for suggestions for a better name for this. 🤷♂ There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -88,7 +107,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { | |
self.lastSaneBrightness = newBrightness | ||
} | ||
} | ||
|
||
RunLoop.current.add(syncTimer!, forMode: .common) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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...") | ||
} | ||
|
@@ -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, | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 ( There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.