├── .gitignore ├── Brightness Sync ├── Assets.xcassets │ ├── Contents.json │ ├── AppIcon.appiconset │ │ ├── Brightness Sync.png │ │ └── Contents.json │ ├── MenuBarIcon.imageset │ │ ├── Brightness Sync.png │ │ └── Contents.json │ └── MenuBarIconOld.imageset │ │ ├── Brightness Sync Old.png │ │ └── Contents.json ├── main.swift ├── Brightness Sync.entitlements ├── Brightness Sync-Bridging-Header.h ├── SlidersView.swift ├── Info.plist └── AppDelegate.swift ├── Brightness Sync.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcshareddata │ └── xcschemes │ │ └── Brightness Sync.xcscheme └── project.pbxproj ├── BrightnessSyncLauncher ├── BrightnessSyncLauncher.entitlements ├── main.swift └── Info.plist ├── .github └── FUNDING.yml ├── LICENSE.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | xcuserdata/ -------------------------------------------------------------------------------- /Brightness Sync/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Brightness Sync/main.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | let app = NSApplication.shared 4 | let delegate = AppDelegate() 5 | app.delegate = delegate 6 | 7 | app.run() 8 | -------------------------------------------------------------------------------- /Brightness Sync/Assets.xcassets/AppIcon.appiconset/Brightness Sync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OCJvanDijk/Brightness-Sync/HEAD/Brightness Sync/Assets.xcassets/AppIcon.appiconset/Brightness Sync.png -------------------------------------------------------------------------------- /Brightness Sync/Assets.xcassets/MenuBarIcon.imageset/Brightness Sync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OCJvanDijk/Brightness-Sync/HEAD/Brightness Sync/Assets.xcassets/MenuBarIcon.imageset/Brightness Sync.png -------------------------------------------------------------------------------- /Brightness Sync/Assets.xcassets/MenuBarIconOld.imageset/Brightness Sync Old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OCJvanDijk/Brightness-Sync/HEAD/Brightness Sync/Assets.xcassets/MenuBarIconOld.imageset/Brightness Sync Old.png -------------------------------------------------------------------------------- /Brightness Sync/Brightness Sync.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Brightness Sync.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /BrightnessSyncLauncher/BrightnessSyncLauncher.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Brightness Sync.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /BrightnessSyncLauncher/main.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | let launcherURL = Bundle.main.bundleURL 4 | let appURL = launcherURL.appendingPathComponent("../../../..").standardized 5 | 6 | if NSRunningApplication.runningApplications(withBundleIdentifier: "dev.vandijk.Brightness-Sync").isEmpty { 7 | NSWorkspace.shared.launchApplication(appURL.path) 8 | } 9 | -------------------------------------------------------------------------------- /Brightness Sync/Assets.xcassets/MenuBarIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "Brightness Sync.png", 9 | "idiom" : "mac", 10 | "scale" : "2x" 11 | } 12 | ], 13 | "info" : { 14 | "author" : "xcode", 15 | "version" : 1 16 | }, 17 | "properties" : { 18 | "template-rendering-intent" : "template" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Brightness Sync/Assets.xcassets/MenuBarIconOld.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "Brightness Sync Old.png", 9 | "idiom" : "mac", 10 | "scale" : "2x" 11 | } 12 | ], 13 | "info" : { 14 | "author" : "xcode", 15 | "version" : 1 16 | }, 17 | "properties" : { 18 | "template-rendering-intent" : "template" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: ocjvandijk 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /Brightness Sync/Brightness Sync-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #import 6 | 7 | double CoreDisplay_Display_GetUserBrightness(CGDirectDisplayID display); 8 | double CoreDisplay_Display_GetDynamicLinearBrightness(CGDirectDisplayID display); 9 | double CoreDisplay_Display_GetLinearBrightness(CGDirectDisplayID display); 10 | void CoreDisplay_Display_SetUserBrightness(CGDirectDisplayID display, double brightness); 11 | void CoreDisplay_Display_SetDynamicLinearBrightness(CGDirectDisplayID display, double brightness); 12 | void CoreDisplay_Display_SetLinearBrightness(CGDirectDisplayID display, double brightness); 13 | 14 | CFDictionaryRef CoreDisplay_DisplayCreateInfoDictionary(CGDirectDisplayID); 15 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Onne van Dijk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Brightness Sync/SlidersView.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import SwiftUI 3 | 4 | struct SlidersView: View { 5 | let monitorPublisher: AnyPublisher<[CFUUID], Never> 6 | @State private var monitors = [CFUUID]() 7 | 8 | var body: some View { 9 | SlidersViewInner(monitors: monitors) 10 | .onReceive(monitorPublisher) { 11 | self.monitors = $0 12 | } 13 | } 14 | } 15 | 16 | private struct SlidersViewInner: View { 17 | let monitors: [CFUUID] 18 | 19 | var body: some View { 20 | VStack { 21 | ForEach(monitors, id: \.self) { 22 | SliderView(monitor: $0) 23 | } 24 | } 25 | .padding(.leading, 22) 26 | .padding(.trailing, 12) 27 | } 28 | } 29 | 30 | private struct SliderView: View { 31 | let monitor: CFUUID 32 | @EnvironmentObject private var monitorOffsets: MonitorOffsets 33 | 34 | var body: some View { 35 | Slider(value: $monitorOffsets[monitor], in: -0.4...0.4) 36 | } 37 | } 38 | 39 | struct SliderView_Previews: PreviewProvider { 40 | static var previews: some View { 41 | SlidersViewInner(monitors: [CFUUIDCreate(nil), CFUUIDCreate(nil), CFUUIDCreate(nil)]) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Brightness Sync/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "filename" : "Brightness Sync.png", 50 | "idiom" : "mac", 51 | "scale" : "2x", 52 | "size" : "512x512" 53 | } 54 | ], 55 | "info" : { 56 | "author" : "xcode", 57 | "version" : 1 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Brightness Sync/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | LSApplicationCategoryType 24 | public.app-category.utilities 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | LSUIElement 28 | 29 | NSHumanReadableCopyright 30 | Copyright © 2019 Onne van Dijk. 31 | NSPrincipalClass 32 | NSApplication 33 | 34 | 35 | -------------------------------------------------------------------------------- /BrightnessSyncLauncher/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSBackgroundOnly 24 | 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | NSHumanReadableCopyright 28 | Copyright © 2020 Onne van Dijk. All rights reserved. 29 | NSPrincipalClass 30 | NSApplication 31 | NSSupportsAutomaticTermination 32 | 33 | NSSupportsSuddenTermination 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /Brightness Sync.xcodeproj/xcshareddata/xcschemes/Brightness Sync.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Brightness Sync 2 | 3 | ___Apple Silicon users:__ Download the [beta](https://github.com/OCJvanDijk/Brightness-Sync/releases/download/v2.4.0-beta.1/Brightness.Sync.app.zip)._ 4 | 5 | __Download:__ [here](https://github.com/OCJvanDijk/Brightness-Sync/releases/latest/download/Brightness.Sync.app.zip) (macOS Catalina required, app is signed and notarized) 6 | Alternatively, you can use [Homebrew Cask](https://github.com/Homebrew/homebrew-cask): `$ brew install --cask brightness-sync` 7 | 8 | _The app doesn't automatically check for updates. If you want to stay up-to-date, I recommend selecting "Watch->Custom->Releases" at the top of this page if you have a GitHub account, or using the Homebrew Cask installation method._ 9 | 10 | ## Compatibility 11 | This app is only compatible with the LG UltraFine monitors developed for Mac in cooperation with Apple, specifically: 12 | 13 | __22MD4KA, 24MD4KL, 27MD5KA and 27MD5KL__ 14 | 15 | These monitors work differently than other monitors and this app was developed because existing solutions didn't support them. 16 | If you have a different (UltraFine) monitor, please check out another solution like [Lunar](https://github.com/alin23/Lunar). 17 | 18 | ## About 19 | This is a small menu bar app to mitigate the problem of the first-generation LG UltraFine not being able to automatically adjust brightness. 20 | It will poll the brightness of your built-in display and synchronize it to your LG monitors. 21 | This way you can use the ambient light sensor of your MBP or iMac for all your (UltraFine) displays! 22 | This also means manually adjusting the brightness of all your displays at once is easier, a single swipe on your Touch Bar or press on your keyboard will do. 23 | 24 | The difference between this app and some existing apps is that this app uses a private framework of macOS to control the backlight of the LG UltraFine the same way the secondary slider of your Touch Bar will do. 25 | Other apps might virtually darken the display with the backlight staying the same, this will greatly reduce contrast. 26 | 27 | Because this app relies on your Mac’s ambient light sensor, unfortunately it won't help with automatic brightness if your MacBook is in clamshell mode or you have connected your UltraFine to for example a Mac Mini. 28 | 29 | You can use the offset slider to control your UltraFine's brightness in relation to your built-in display (e.g. the difference between them). You might want to do that if your monitors are at different angles, you perceive a difference between them that you want to correct, or for various other reasons. 30 | If you are interested in controlling and finetuning this offset throughout the day, disable the offset lock in the menu. Now you can use various other ways to easily control the offset. For more info see the release notes for [v2.2.0](https://github.com/OCJvanDijk/Brightness-Sync/releases/tag/v2.2.0). Otherwise, just keep the lock enabled. 31 | 32 | I only have one 27-inch LG UltraFine display, so I can do only limited testing. Let me know if you have issues. 33 | 34 | Requires macOS 10.15. If you're on 10.14, you can download v1 from the releases page. 35 | 36 | ## Energy impact 37 | The app polls the brightness pretty aggressively (every 0.1 seconds), which results in a small energy impact of around 0.3-0.5 according to Activity Monitor. 38 | _However_ it will automatically stop the polling when no UltraFine displays are connected and because those monitors are also a power source, this effectively means it will never run when on battery power. 39 | 40 | If you want to change the default update interval, it is exposed as an internal user preference `BSUpdateInterval`. It can be set via the terminal: 41 | `$ defaults write dev.vandijk.Brightness-Sync BSUpdateInterval 0.5` where 0.5 is the desired delay in seconds. The app must be restarted for the change to take effect. 42 | 43 | ## 2nd Gen UltraFine support 44 | This app was designed with 1st generation UltraFines in mind that don't support auto brightness. I started work on supporting mixed setups with both 1st gen and 2nd gen displays in v2.3.0. If no built-in display is detected, it will use a 2nd gen display as the "source" and sync its brightness to all other connected displays. The app will currently override the auto brightness of all 2nd gen displays that aren't used as the source, because some people have reported the auto brightness of the 2nd gen to not be so reliable and are using this app to sync the brightness of the built-in display to the 2nd gen. You should probably turn off the built-in auto brightness of the 2nd gen in the Settings if you do this. Other people might want to only sync to 1st gen and let the 2nd gen handle its own auto brightness. I'm considering making this an option for those people. 45 | -------------------------------------------------------------------------------- /Brightness Sync/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import Combine 3 | import os 4 | import ServiceManagement 5 | import SwiftUI 6 | 7 | class AppDelegate: NSObject, NSApplicationDelegate { 8 | // MARK: - Menu / App 9 | 10 | let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength) 11 | let statusIndicator = NSMenuItem(title: "Starting", action: nil, keyEquivalent: "") 12 | 13 | let pauseButton = NSMenuItem(title: "Pause", action: #selector(togglePause), keyEquivalent: "") 14 | 15 | let monitorOffsets = MonitorOffsets() 16 | 17 | func applicationDidFinishLaunching(_ aNotification: Notification) { 18 | if let button = statusItem.button { 19 | if #available(macOS 11.0, *) { 20 | button.image = NSImage(named: "MenuBarIcon") 21 | } else { 22 | button.image = NSImage(named: "MenuBarIconOld") 23 | } 24 | } 25 | 26 | let menu = NSMenu() 27 | menu.addItem(statusIndicator) 28 | menu.addItem(pauseButton) 29 | menu.addItem(NSMenuItem.separator()) 30 | 31 | menu.addItem(NSMenuItem(title: "Brightness Offset:", action: nil, keyEquivalent: "")) 32 | let slidersItem = NSMenuItem() 33 | let slidersView = NSHostingView(rootView: SlidersView(monitorPublisher: displaysPublisher.map { $0.targets }.eraseToAnyPublisher()).environmentObject(monitorOffsets)) 34 | slidersView.translatesAutoresizingMaskIntoConstraints = false 35 | slidersView.widthAnchor.constraint(equalToConstant: 250).isActive = true 36 | slidersView.layoutSubtreeIfNeeded() 37 | slidersItem.view = slidersView 38 | menu.addItem(slidersItem) 39 | menu.addItem(NSMenuItem(title: "Reset", action: #selector(brightnessOffsetReset), keyEquivalent: "")) 40 | lockOffsetMenuItem.state = lockOffset ? .on : .off 41 | menu.addItem(lockOffsetMenuItem) 42 | menu.addItem(NSMenuItem.separator()) 43 | 44 | let launchAtLoginEnabled = (SMJobCopyDictionary(kSMDomainUserLaunchd, Self.launcherId)?.takeRetainedValue() as NSDictionary?)?["OnDemand"] as? Bool ?? false 45 | launchAtLoginMenuItem.state = launchAtLoginEnabled ? .on : .off 46 | menu.addItem(launchAtLoginMenuItem) 47 | menu.addItem(NSMenuItem.separator()) 48 | 49 | let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String 50 | menu.addItem(NSMenuItem(title: "v\(appVersion)", action: nil, keyEquivalent: "")) 51 | menu.addItem(NSMenuItem(title: "Check For Updates", action: #selector(checkForUpdates), keyEquivalent: "")) 52 | menu.addItem(NSMenuItem.separator()) 53 | 54 | let copyDiagnosticsButton = NSMenuItem(title: "Copy Diagnostics", action: #selector(copyDiagnosticsToPasteboard), keyEquivalent: "c") 55 | copyDiagnosticsButton.isHidden = true 56 | copyDiagnosticsButton.allowsKeyEquivalentWhenHidden = true 57 | menu.addItem(copyDiagnosticsButton) 58 | 59 | menu.addItem(NSMenuItem(title: "Quit", action: #selector(NSApplication.terminate), keyEquivalent: "")) 60 | statusItem.menu = menu 61 | 62 | setup() 63 | setupDisplayMonitor() 64 | } 65 | 66 | func applicationWillTerminate(_ aNotification: Notification) { 67 | // Insert code here to tear down your application 68 | } 69 | 70 | let pausedPublisher = CurrentValueSubject(false) 71 | @objc func togglePause() { 72 | pausedPublisher.send(!pausedPublisher.value) 73 | pauseButton.title = pausedPublisher.value ? "Resume" : "Pause" 74 | } 75 | 76 | @objc func brightnessOffsetReset() { 77 | UserDefaults.standard 78 | .dictionaryRepresentation() 79 | .keys 80 | .filter { $0.starts(with: "BSBrightnessOffset_") } 81 | .forEach { key in 82 | UserDefaults.standard.removeObject(forKey: key) 83 | } 84 | } 85 | 86 | var lockOffset: Bool { 87 | get { 88 | UserDefaults.standard.object(forKey: "BSLockBrightness") as? Bool ?? true 89 | } 90 | set { 91 | UserDefaults.standard.set(newValue, forKey: "BSLockBrightness") 92 | } 93 | } 94 | 95 | let lockOffsetMenuItem = NSMenuItem(title: "Lock", action: #selector(toggleLockOffset), keyEquivalent: "") 96 | @objc func toggleLockOffset() { 97 | lockOffset.toggle() 98 | lockOffsetMenuItem.state = lockOffset ? .on : .off 99 | } 100 | 101 | let launchAtLoginMenuItem = NSMenuItem(title: "Launch At Login", action: #selector(toggleLaunchAtLoginEnabled), keyEquivalent: "") 102 | static let launcherId = "dev.vandijk.BrightnessSyncLauncher" as CFString 103 | @objc func toggleLaunchAtLoginEnabled() { 104 | let enable = launchAtLoginMenuItem.state == .off 105 | let success = SMLoginItemSetEnabled(Self.launcherId, enable) 106 | if success { 107 | launchAtLoginMenuItem.state = enable ? .on : .off 108 | } 109 | } 110 | 111 | @objc func checkForUpdates() { 112 | NSWorkspace.shared.open(URL(string: "https://github.com/OCJvanDijk/Brightness-Sync/releases")!) 113 | } 114 | 115 | @objc func copyDiagnosticsToPasteboard() { 116 | let displayInfoDict = Dictionary(uniqueKeysWithValues: Self.getOnlineDisplays().map { ($0, CoreDisplay_DisplayCreateInfoDictionary($0)?.takeRetainedValue()) }) 117 | 118 | NSPasteboard.general.clearContents() 119 | NSPasteboard.general.setString(String(describing: displayInfoDict), forType: .string) 120 | } 121 | 122 | // MARK: - Brightness Sync 123 | 124 | enum Status: Equatable { 125 | case deactivated 126 | case paused 127 | case running(sourceBrightness: Double, targets: [Target]) 128 | } 129 | 130 | struct Target: Equatable { 131 | let id: CFUUID 132 | let brightness: Double 133 | let offset: Double 134 | } 135 | 136 | var cancelBag = Set() 137 | 138 | func setup() { 139 | UserDefaults.standard.register(defaults: ["BSUpdateInterval": 0.1]) 140 | let updateInterval = UserDefaults.standard.double(forKey: "BSUpdateInterval") 141 | if updateInterval != 0.1 { 142 | os_log("Using custom polling interval: %fs", updateInterval) 143 | } 144 | 145 | let statusPublisher = displaysPublisher 146 | .combineLatest(pausedPublisher) 147 | .map { [monitorOffsets] displays, paused -> AnyPublisher in 148 | // We don't want the timer running unless necessary to save energy 149 | if paused { 150 | os_log("Paused...") 151 | return Just(.paused).eraseToAnyPublisher() 152 | } else if let source = displays.source, !displays.targets.isEmpty { 153 | os_log("Activated...") 154 | return Timer.publish(every: updateInterval, on: .current, in: .common) 155 | .autoconnect() 156 | .map { _ in 157 | .running( 158 | sourceBrightness: CoreDisplay_Display_GetLinearBrightness(CGDisplayGetDisplayIDFromUUID(source)), 159 | targets: displays.targets.map { 160 | .init( 161 | id: $0, 162 | brightness: CoreDisplay_Display_GetLinearBrightness(CGDisplayGetDisplayIDFromUUID($0)), 163 | offset: monitorOffsets[$0] 164 | ) 165 | } 166 | ) 167 | } 168 | .eraseToAnyPublisher() 169 | } else { 170 | os_log("Deactivated...") 171 | return Just(.deactivated).eraseToAnyPublisher() 172 | } 173 | } 174 | .switchToLatest() 175 | .removeDuplicates() 176 | .multicast(subject: PassthroughSubject()) 177 | 178 | // There is a quirk in CoreDisplay, that causes it to read incorrect values just before you close the lid and enter clamshell mode or disconnect a monitor. 179 | // This causes different kinds of problems, so we roll back to two seconds ago. 180 | // This is probably desirable anyway because even without the quirk closing the lid will briefly affect brightness readings. 181 | let pastStatusPublisher = statusPublisher 182 | .delay(for: .seconds(2), scheduler: RunLoop.current) 183 | .prepend(.deactivated) 184 | let rollbackInjector = statusPublisher 185 | .withLatestFrom(pastStatusPublisher) 186 | .compactMap { brightnessStatus, brightnessStatusTwoSecondsAgo -> [Target]? in 187 | if brightnessStatus == .deactivated, case .running(_, let targets) = brightnessStatusTwoSecondsAgo { 188 | return targets 189 | } else { 190 | return nil 191 | } 192 | } 193 | 194 | statusPublisher 195 | .scan([]) { [weak self] previouslySynced, newStatus -> [Target] in 196 | guard case .running(let sourceBrightness, let targets) = newStatus else { return [] } 197 | 198 | return targets.map { target in 199 | var offset = target.offset 200 | let lockOffset = self?.lockOffset ?? true 201 | if !lockOffset, let expectedBrightness = previouslySynced.first(where: { $0.id == target.id })?.brightness, abs(target.brightness - expectedBrightness) > 0.0001 { 202 | let currentTargetUserBrightness = estimatedLinearToUserBrightness(target.brightness) 203 | let expectedTargetUserBrightness = estimatedLinearToUserBrightness(expectedBrightness) 204 | let offsetDelta = currentTargetUserBrightness - expectedTargetUserBrightness 205 | offset += offsetDelta 206 | } 207 | 208 | // Brightness offset set by the user is naturally a "User" brightness. 209 | // Ideally we would map this to "Linear" brightness exactly like CoreDisplay does, but I've been unable to reverse engineer the formula. 210 | // (Probably something to do with CoreDisplay_Display_GetDynamicSliderParameters, CoreDisplay_Display_GetLuminanceCorrectionFactor etc) 211 | // Instead I've curve fitted an exponential function to observed user->linear values of my own MBP's screen which I hope is a reasonable approximation for offset values. 212 | // This approximation is only applied to the offset so if offset is 0 we keep the exact Linear brightness as reported by CoreDisplay. 213 | let adjustedEstimatedLinearBrightness = estimatedUserToLinearBrightness(estimatedLinearToUserBrightness(sourceBrightness) + offset).clamped(to: 0.0...1.0) 214 | return .init(id: target.id, brightness: adjustedEstimatedLinearBrightness, offset: offset) 215 | } 216 | } 217 | .merge(with: rollbackInjector) 218 | .sink { [monitorOffsets] targets in 219 | for target in targets { 220 | CoreDisplay_Display_SetLinearBrightness(CGDisplayGetDisplayIDFromUUID(target.id), target.brightness) 221 | monitorOffsets[target.id] = target.offset 222 | } 223 | } 224 | .store(in: &cancelBag) 225 | 226 | statusPublisher 227 | .map { 228 | switch $0 { 229 | case .deactivated: 230 | return "Deactivated" 231 | case .paused: 232 | return "Paused" 233 | case .running: 234 | return "Activated" 235 | } 236 | } 237 | .removeDuplicates() 238 | .assign(to: \.title, on: statusIndicator) 239 | .store(in: &cancelBag) 240 | 241 | statusPublisher.connect().store(in: &cancelBag) 242 | } 243 | 244 | // MARK: - Displays 245 | 246 | let displaysPublisher: PassthroughSubject<(source: CFUUID?, targets: [CFUUID]), Never> = .init() 247 | 248 | var displayReconfigurationCounter = 0 249 | 250 | func setupDisplayMonitor() { 251 | // We use reconfiguration callback to track if display reconfiguration is done, which is the only reliable way I've found to know if all displays are "brightness writable" to prevent offset shift. 252 | // All calls with beginConfigurationFlag are balanced with another call when configuration is done, so we keep track with a counter. 253 | CGDisplayRegisterReconfigurationCallback({ id, flags, _ in 254 | guard let `self` = NSApplication.shared.delegate as? AppDelegate else { return } 255 | 256 | if flags.contains(.beginConfigurationFlag) { 257 | if self.displayReconfigurationCounter == 0 { 258 | os_log("Display reconfiguration started...") 259 | self.displaysPublisher.send((nil, [])) // Deactivate during reconfiguration 260 | } 261 | self.displayReconfigurationCounter += 1 262 | } 263 | else { 264 | self.displayReconfigurationCounter -= 1 265 | if self.displayReconfigurationCounter == 0 { 266 | os_log("Display reconfiguration ended...") 267 | self.refreshDisplays() 268 | } 269 | } 270 | }, nil) 271 | 272 | refreshDisplays() 273 | } 274 | 275 | func applicationDidChangeScreenParameters(_ notification: Notification) { 276 | if displayReconfigurationCounter == 0 { 277 | refreshDisplays() 278 | } 279 | } 280 | 281 | func refreshDisplays() { 282 | os_log("Starting display refresh...") 283 | 284 | let onlineDisplays = Self.getOnlineDisplays() 285 | os_log("Displays: %{public}@", onlineDisplays) 286 | 287 | let isOnConsole = (CGSessionCopyCurrentDictionary() as NSDictionary?)?[kCGSessionOnConsoleKey] as? Bool ?? false 288 | 289 | if isOnConsole { 290 | let lgVendorNumber: UInt32 = 7789 291 | // let ultraFine4k1stGenModelNumber: UInt32 = 23312 292 | // let ultraFine5k1stGenModelNumber: UInt32 = 23313 293 | let ultraFine4k2ndGenModelNumber: UInt32 = 23419 294 | let ultraFine5k2ndGenModelNumber: UInt32 = 23412 295 | 296 | func is2ndGenUltraFine(_ display: CGDirectDisplayID) -> Bool { 297 | switch (CGDisplayVendorNumber(display), CGDisplayModelNumber(display)) { 298 | case (lgVendorNumber, ultraFine4k2ndGenModelNumber), (lgVendorNumber, ultraFine5k2ndGenModelNumber): 299 | return true 300 | default: 301 | return false 302 | } 303 | } 304 | 305 | let builtin = onlineDisplays 306 | .filter { CGDisplayIsBuiltin($0) == 1 } 307 | .compactMap { CGDisplayCreateUUIDFromDisplayID($0)?.takeRetainedValue() } 308 | .first 309 | 310 | let source = builtin ?? onlineDisplays 311 | .filter { is2ndGenUltraFine($0) } 312 | .compactMap { CGDisplayCreateUUIDFromDisplayID($0)?.takeRetainedValue() } 313 | .first 314 | 315 | let targets = onlineDisplays 316 | .filter { 317 | if let displayInfo = CoreDisplay_DisplayCreateInfoDictionary($0)?.takeRetainedValue() as NSDictionary? { 318 | if 319 | let displayNames = displayInfo[kDisplayProductName] as? NSDictionary, 320 | let displayName = displayNames["en_US"] as? NSString 321 | { 322 | if 323 | displayName.contains("LG UltraFine") 324 | { 325 | os_log("Found compatible display: %{public}@", displayName) 326 | return true 327 | } 328 | else { 329 | os_log("Found incompatible display: %{public}@", displayName) 330 | return false 331 | } 332 | } 333 | else { 334 | os_log("Display without en_US name found.") 335 | return false 336 | } 337 | } else { 338 | os_log("Display without retrievable info found.") 339 | return false 340 | } 341 | } 342 | .compactMap { CGDisplayCreateUUIDFromDisplayID($0)?.takeRetainedValue() } 343 | .filter { $0 != source } 344 | 345 | displaysPublisher.send((source, targets)) 346 | } else { 347 | displaysPublisher.send((nil, [])) 348 | os_log("User not active") 349 | } 350 | } 351 | 352 | static let maxDisplays: UInt32 = 8 353 | 354 | static func getOnlineDisplays() -> Set { 355 | var onlineDisplays = [CGDirectDisplayID](repeating: 0, count: Int(maxDisplays)) 356 | var displayCount: UInt32 = 0 357 | 358 | CGGetOnlineDisplayList(maxDisplays, &onlineDisplays, &displayCount) 359 | 360 | return Set(onlineDisplays[0.. Double { 366 | get { 367 | UserDefaults.standard.double(forKey: "BSBrightnessOffset_\(CFUUIDCreateString(nil, monitor)!)") 368 | } 369 | set { 370 | objectWillChange.send() 371 | UserDefaults.standard.set(newValue, forKey: "BSBrightnessOffset_\(CFUUIDCreateString(nil, monitor)!)") 372 | } 373 | } 374 | } 375 | 376 | func estimatedLinearToUserBrightness(_ brightness: Double) -> Double { 377 | log(brightness / 0.0079) / 4.6533 378 | } 379 | 380 | func estimatedUserToLinearBrightness(_ brightness: Double) -> Double { 381 | exp(brightness * 4.6533) * 0.0079 382 | } 383 | 384 | extension Comparable { 385 | func clamped(to limits: ClosedRange) -> Self { 386 | return min(max(self, limits.lowerBound), limits.upperBound) 387 | } 388 | } 389 | 390 | extension Publisher { 391 | func withLatestFrom(_ second: P) 392 | -> Publishers.SwitchToLatest, Publishers.Map>> where P.Output == A, P.Failure == Failure { 393 | second.map { latestValue in 394 | self.map { ownValue in (ownValue, latestValue) } 395 | } 396 | .switchToLatest() 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /Brightness Sync.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 921AF06C24487F1700E8BE48 /* SlidersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 921AF06B24487F1700E8BE48 /* SlidersView.swift */; }; 11 | 922D19982291B21C00DBF67D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 922D19972291B21C00DBF67D /* AppDelegate.swift */; }; 12 | 922D199A2291B21C00DBF67D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 922D19992291B21C00DBF67D /* Assets.xcassets */; }; 13 | 922D19A62291B5C600DBF67D /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 922D19A52291B5C600DBF67D /* main.swift */; }; 14 | 922F72A624339D8300AF6830 /* BrightnessSyncLauncher.app in CopyFiles */ = {isa = PBXBuildFile; fileRef = 922F729224339AD700AF6830 /* BrightnessSyncLauncher.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 15 | 927DF28E2292E6DE00C9C85F /* CoreDisplay.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 922D19AD2291CD4300DBF67D /* CoreDisplay.framework */; }; 16 | 929E03D02433A5C300A272DE /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 929E03CF2433A5C300A272DE /* main.swift */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXCopyFilesBuildPhase section */ 20 | 922F72A524339D5200AF6830 /* CopyFiles */ = { 21 | isa = PBXCopyFilesBuildPhase; 22 | buildActionMask = 2147483647; 23 | dstPath = Contents/Library/LoginItems; 24 | dstSubfolderSpec = 1; 25 | files = ( 26 | 922F72A624339D8300AF6830 /* BrightnessSyncLauncher.app in CopyFiles */, 27 | ); 28 | runOnlyForDeploymentPostprocessing = 0; 29 | }; 30 | /* End PBXCopyFilesBuildPhase section */ 31 | 32 | /* Begin PBXFileReference section */ 33 | 921AF06B24487F1700E8BE48 /* SlidersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlidersView.swift; sourceTree = ""; }; 34 | 922D19942291B21C00DBF67D /* Brightness Sync.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Brightness Sync.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 35 | 922D19972291B21C00DBF67D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 36 | 922D19992291B21C00DBF67D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 37 | 922D199E2291B21C00DBF67D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 38 | 922D19A52291B5C600DBF67D /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 39 | 922D19A92291CC9300DBF67D /* Brightness Sync-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Brightness Sync-Bridging-Header.h"; sourceTree = ""; }; 40 | 922D19AD2291CD4300DBF67D /* CoreDisplay.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreDisplay.framework; path = /System/Library/Frameworks/CoreDisplay.framework; sourceTree = ""; }; 41 | 922F729224339AD700AF6830 /* BrightnessSyncLauncher.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BrightnessSyncLauncher.app; sourceTree = BUILT_PRODUCTS_DIR; }; 42 | 922F72A024339AD800AF6830 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 43 | 922F72A124339AD800AF6830 /* BrightnessSyncLauncher.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = BrightnessSyncLauncher.entitlements; sourceTree = ""; }; 44 | 929E03CF2433A5C300A272DE /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 45 | 92B83E1622A968DF0009570E /* Brightness Sync.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Brightness Sync.entitlements"; sourceTree = ""; }; 46 | /* End PBXFileReference section */ 47 | 48 | /* Begin PBXFrameworksBuildPhase section */ 49 | 922D19912291B21C00DBF67D /* Frameworks */ = { 50 | isa = PBXFrameworksBuildPhase; 51 | buildActionMask = 2147483647; 52 | files = ( 53 | 927DF28E2292E6DE00C9C85F /* CoreDisplay.framework in Frameworks */, 54 | ); 55 | runOnlyForDeploymentPostprocessing = 0; 56 | }; 57 | 922F728F24339AD700AF6830 /* Frameworks */ = { 58 | isa = PBXFrameworksBuildPhase; 59 | buildActionMask = 2147483647; 60 | files = ( 61 | ); 62 | runOnlyForDeploymentPostprocessing = 0; 63 | }; 64 | /* End PBXFrameworksBuildPhase section */ 65 | 66 | /* Begin PBXGroup section */ 67 | 922D198B2291B21C00DBF67D = { 68 | isa = PBXGroup; 69 | children = ( 70 | 922D19962291B21C00DBF67D /* Brightness Sync */, 71 | 922F729324339AD700AF6830 /* BrightnessSyncLauncher */, 72 | 922D19952291B21C00DBF67D /* Products */, 73 | 922D19AC2291CD4300DBF67D /* Frameworks */, 74 | ); 75 | sourceTree = ""; 76 | }; 77 | 922D19952291B21C00DBF67D /* Products */ = { 78 | isa = PBXGroup; 79 | children = ( 80 | 922D19942291B21C00DBF67D /* Brightness Sync.app */, 81 | 922F729224339AD700AF6830 /* BrightnessSyncLauncher.app */, 82 | ); 83 | name = Products; 84 | sourceTree = ""; 85 | }; 86 | 922D19962291B21C00DBF67D /* Brightness Sync */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | 92B83E1622A968DF0009570E /* Brightness Sync.entitlements */, 90 | 922D19972291B21C00DBF67D /* AppDelegate.swift */, 91 | 921AF06B24487F1700E8BE48 /* SlidersView.swift */, 92 | 922D19992291B21C00DBF67D /* Assets.xcassets */, 93 | 922D199E2291B21C00DBF67D /* Info.plist */, 94 | 922D19A52291B5C600DBF67D /* main.swift */, 95 | 922D19A92291CC9300DBF67D /* Brightness Sync-Bridging-Header.h */, 96 | ); 97 | path = "Brightness Sync"; 98 | sourceTree = ""; 99 | }; 100 | 922D19AC2291CD4300DBF67D /* Frameworks */ = { 101 | isa = PBXGroup; 102 | children = ( 103 | 922D19AD2291CD4300DBF67D /* CoreDisplay.framework */, 104 | ); 105 | name = Frameworks; 106 | sourceTree = ""; 107 | }; 108 | 922F729324339AD700AF6830 /* BrightnessSyncLauncher */ = { 109 | isa = PBXGroup; 110 | children = ( 111 | 922F72A124339AD800AF6830 /* BrightnessSyncLauncher.entitlements */, 112 | 922F72A024339AD800AF6830 /* Info.plist */, 113 | 929E03CF2433A5C300A272DE /* main.swift */, 114 | ); 115 | path = BrightnessSyncLauncher; 116 | sourceTree = ""; 117 | }; 118 | /* End PBXGroup section */ 119 | 120 | /* Begin PBXNativeTarget section */ 121 | 922D19932291B21C00DBF67D /* Brightness Sync */ = { 122 | isa = PBXNativeTarget; 123 | buildConfigurationList = 922D19A22291B21C00DBF67D /* Build configuration list for PBXNativeTarget "Brightness Sync" */; 124 | buildPhases = ( 125 | 922D19902291B21C00DBF67D /* Sources */, 126 | 922D19912291B21C00DBF67D /* Frameworks */, 127 | 922D19922291B21C00DBF67D /* Resources */, 128 | 922F72A524339D5200AF6830 /* CopyFiles */, 129 | ); 130 | buildRules = ( 131 | ); 132 | dependencies = ( 133 | ); 134 | name = "Brightness Sync"; 135 | productName = "Brightness Sync"; 136 | productReference = 922D19942291B21C00DBF67D /* Brightness Sync.app */; 137 | productType = "com.apple.product-type.application"; 138 | }; 139 | 922F729124339AD700AF6830 /* BrightnessSyncLauncher */ = { 140 | isa = PBXNativeTarget; 141 | buildConfigurationList = 922F72A224339AD800AF6830 /* Build configuration list for PBXNativeTarget "BrightnessSyncLauncher" */; 142 | buildPhases = ( 143 | 922F728E24339AD700AF6830 /* Sources */, 144 | 922F728F24339AD700AF6830 /* Frameworks */, 145 | 922F729024339AD700AF6830 /* Resources */, 146 | ); 147 | buildRules = ( 148 | ); 149 | dependencies = ( 150 | ); 151 | name = BrightnessSyncLauncher; 152 | productName = BrightnessSyncLauncher; 153 | productReference = 922F729224339AD700AF6830 /* BrightnessSyncLauncher.app */; 154 | productType = "com.apple.product-type.application"; 155 | }; 156 | /* End PBXNativeTarget section */ 157 | 158 | /* Begin PBXProject section */ 159 | 922D198C2291B21C00DBF67D /* Project object */ = { 160 | isa = PBXProject; 161 | attributes = { 162 | LastSwiftUpdateCheck = 1140; 163 | LastUpgradeCheck = 1220; 164 | ORGANIZATIONNAME = "Onne van Dijk"; 165 | TargetAttributes = { 166 | 922D19932291B21C00DBF67D = { 167 | CreatedOnToolsVersion = 10.2.1; 168 | LastSwiftMigration = 1020; 169 | SystemCapabilities = { 170 | com.apple.HardenedRuntime = { 171 | enabled = 1; 172 | }; 173 | com.apple.Sandbox = { 174 | enabled = 0; 175 | }; 176 | }; 177 | }; 178 | 922F729124339AD700AF6830 = { 179 | CreatedOnToolsVersion = 11.4; 180 | }; 181 | }; 182 | }; 183 | buildConfigurationList = 922D198F2291B21C00DBF67D /* Build configuration list for PBXProject "Brightness Sync" */; 184 | compatibilityVersion = "Xcode 9.3"; 185 | developmentRegion = en; 186 | hasScannedForEncodings = 0; 187 | knownRegions = ( 188 | en, 189 | Base, 190 | ); 191 | mainGroup = 922D198B2291B21C00DBF67D; 192 | productRefGroup = 922D19952291B21C00DBF67D /* Products */; 193 | projectDirPath = ""; 194 | projectRoot = ""; 195 | targets = ( 196 | 922D19932291B21C00DBF67D /* Brightness Sync */, 197 | 922F729124339AD700AF6830 /* BrightnessSyncLauncher */, 198 | ); 199 | }; 200 | /* End PBXProject section */ 201 | 202 | /* Begin PBXResourcesBuildPhase section */ 203 | 922D19922291B21C00DBF67D /* Resources */ = { 204 | isa = PBXResourcesBuildPhase; 205 | buildActionMask = 2147483647; 206 | files = ( 207 | 922D199A2291B21C00DBF67D /* Assets.xcassets in Resources */, 208 | ); 209 | runOnlyForDeploymentPostprocessing = 0; 210 | }; 211 | 922F729024339AD700AF6830 /* Resources */ = { 212 | isa = PBXResourcesBuildPhase; 213 | buildActionMask = 2147483647; 214 | files = ( 215 | ); 216 | runOnlyForDeploymentPostprocessing = 0; 217 | }; 218 | /* End PBXResourcesBuildPhase section */ 219 | 220 | /* Begin PBXSourcesBuildPhase section */ 221 | 922D19902291B21C00DBF67D /* Sources */ = { 222 | isa = PBXSourcesBuildPhase; 223 | buildActionMask = 2147483647; 224 | files = ( 225 | 921AF06C24487F1700E8BE48 /* SlidersView.swift in Sources */, 226 | 922D19A62291B5C600DBF67D /* main.swift in Sources */, 227 | 922D19982291B21C00DBF67D /* AppDelegate.swift in Sources */, 228 | ); 229 | runOnlyForDeploymentPostprocessing = 0; 230 | }; 231 | 922F728E24339AD700AF6830 /* Sources */ = { 232 | isa = PBXSourcesBuildPhase; 233 | buildActionMask = 2147483647; 234 | files = ( 235 | 929E03D02433A5C300A272DE /* main.swift in Sources */, 236 | ); 237 | runOnlyForDeploymentPostprocessing = 0; 238 | }; 239 | /* End PBXSourcesBuildPhase section */ 240 | 241 | /* Begin XCBuildConfiguration section */ 242 | 922D19A02291B21C00DBF67D /* Debug */ = { 243 | isa = XCBuildConfiguration; 244 | buildSettings = { 245 | ALWAYS_SEARCH_USER_PATHS = NO; 246 | CLANG_ANALYZER_NONNULL = YES; 247 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 248 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 249 | CLANG_CXX_LIBRARY = "libc++"; 250 | CLANG_ENABLE_MODULES = YES; 251 | CLANG_ENABLE_OBJC_ARC = YES; 252 | CLANG_ENABLE_OBJC_WEAK = YES; 253 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 254 | CLANG_WARN_BOOL_CONVERSION = YES; 255 | CLANG_WARN_COMMA = YES; 256 | CLANG_WARN_CONSTANT_CONVERSION = YES; 257 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 258 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 259 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 260 | CLANG_WARN_EMPTY_BODY = YES; 261 | CLANG_WARN_ENUM_CONVERSION = YES; 262 | CLANG_WARN_INFINITE_RECURSION = YES; 263 | CLANG_WARN_INT_CONVERSION = YES; 264 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 265 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 266 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 267 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 268 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 269 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 270 | CLANG_WARN_STRICT_PROTOTYPES = YES; 271 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 272 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 273 | CLANG_WARN_UNREACHABLE_CODE = YES; 274 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 275 | CODE_SIGN_IDENTITY = "-"; 276 | COPY_PHASE_STRIP = NO; 277 | DEBUG_INFORMATION_FORMAT = dwarf; 278 | ENABLE_STRICT_OBJC_MSGSEND = YES; 279 | ENABLE_TESTABILITY = YES; 280 | GCC_C_LANGUAGE_STANDARD = gnu11; 281 | GCC_DYNAMIC_NO_PIC = NO; 282 | GCC_NO_COMMON_BLOCKS = YES; 283 | GCC_OPTIMIZATION_LEVEL = 0; 284 | GCC_PREPROCESSOR_DEFINITIONS = ( 285 | "DEBUG=1", 286 | "$(inherited)", 287 | ); 288 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 289 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 290 | GCC_WARN_UNDECLARED_SELECTOR = YES; 291 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 292 | GCC_WARN_UNUSED_FUNCTION = YES; 293 | GCC_WARN_UNUSED_VARIABLE = YES; 294 | MACOSX_DEPLOYMENT_TARGET = 10.15; 295 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 296 | MTL_FAST_MATH = YES; 297 | ONLY_ACTIVE_ARCH = YES; 298 | SDKROOT = macosx; 299 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 300 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 301 | }; 302 | name = Debug; 303 | }; 304 | 922D19A12291B21C00DBF67D /* Release */ = { 305 | isa = XCBuildConfiguration; 306 | buildSettings = { 307 | ALWAYS_SEARCH_USER_PATHS = NO; 308 | CLANG_ANALYZER_NONNULL = YES; 309 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 310 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 311 | CLANG_CXX_LIBRARY = "libc++"; 312 | CLANG_ENABLE_MODULES = YES; 313 | CLANG_ENABLE_OBJC_ARC = YES; 314 | CLANG_ENABLE_OBJC_WEAK = YES; 315 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 316 | CLANG_WARN_BOOL_CONVERSION = YES; 317 | CLANG_WARN_COMMA = YES; 318 | CLANG_WARN_CONSTANT_CONVERSION = YES; 319 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 320 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 321 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 322 | CLANG_WARN_EMPTY_BODY = YES; 323 | CLANG_WARN_ENUM_CONVERSION = YES; 324 | CLANG_WARN_INFINITE_RECURSION = YES; 325 | CLANG_WARN_INT_CONVERSION = YES; 326 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 327 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 328 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 329 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 330 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 331 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 332 | CLANG_WARN_STRICT_PROTOTYPES = YES; 333 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 334 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 335 | CLANG_WARN_UNREACHABLE_CODE = YES; 336 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 337 | CODE_SIGN_IDENTITY = "-"; 338 | COPY_PHASE_STRIP = NO; 339 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 340 | ENABLE_NS_ASSERTIONS = NO; 341 | ENABLE_STRICT_OBJC_MSGSEND = YES; 342 | GCC_C_LANGUAGE_STANDARD = gnu11; 343 | GCC_NO_COMMON_BLOCKS = YES; 344 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 345 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 346 | GCC_WARN_UNDECLARED_SELECTOR = YES; 347 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 348 | GCC_WARN_UNUSED_FUNCTION = YES; 349 | GCC_WARN_UNUSED_VARIABLE = YES; 350 | MACOSX_DEPLOYMENT_TARGET = 10.15; 351 | MTL_ENABLE_DEBUG_INFO = NO; 352 | MTL_FAST_MATH = YES; 353 | SDKROOT = macosx; 354 | SWIFT_COMPILATION_MODE = wholemodule; 355 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 356 | }; 357 | name = Release; 358 | }; 359 | 922D19A32291B21C00DBF67D /* Debug */ = { 360 | isa = XCBuildConfiguration; 361 | buildSettings = { 362 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 363 | CLANG_ENABLE_MODULES = YES; 364 | CODE_SIGN_IDENTITY = "Apple Development"; 365 | CODE_SIGN_STYLE = Automatic; 366 | COMBINE_HIDPI_IMAGES = YES; 367 | CURRENT_PROJECT_VERSION = 2.3.2; 368 | DEVELOPMENT_TEAM = 7M6D294GQ9; 369 | ENABLE_HARDENED_RUNTIME = YES; 370 | INFOPLIST_FILE = "Brightness Sync/Info.plist"; 371 | LD_RUNPATH_SEARCH_PATHS = ( 372 | "$(inherited)", 373 | "@executable_path/../Frameworks", 374 | ); 375 | MARKETING_VERSION = 2.3.2; 376 | PRODUCT_BUNDLE_IDENTIFIER = "dev.vandijk.Brightness-Sync"; 377 | PRODUCT_NAME = "$(TARGET_NAME)"; 378 | PROVISIONING_PROFILE_SPECIFIER = ""; 379 | SWIFT_OBJC_BRIDGING_HEADER = "Brightness Sync/Brightness Sync-Bridging-Header.h"; 380 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 381 | SWIFT_VERSION = 5.0; 382 | }; 383 | name = Debug; 384 | }; 385 | 922D19A42291B21C00DBF67D /* Release */ = { 386 | isa = XCBuildConfiguration; 387 | buildSettings = { 388 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 389 | CLANG_ENABLE_MODULES = YES; 390 | CODE_SIGN_IDENTITY = "Apple Development"; 391 | CODE_SIGN_STYLE = Automatic; 392 | COMBINE_HIDPI_IMAGES = YES; 393 | CURRENT_PROJECT_VERSION = 2.3.2; 394 | DEVELOPMENT_TEAM = 7M6D294GQ9; 395 | ENABLE_HARDENED_RUNTIME = YES; 396 | INFOPLIST_FILE = "Brightness Sync/Info.plist"; 397 | LD_RUNPATH_SEARCH_PATHS = ( 398 | "$(inherited)", 399 | "@executable_path/../Frameworks", 400 | ); 401 | MARKETING_VERSION = 2.3.2; 402 | PRODUCT_BUNDLE_IDENTIFIER = "dev.vandijk.Brightness-Sync"; 403 | PRODUCT_NAME = "$(TARGET_NAME)"; 404 | PROVISIONING_PROFILE_SPECIFIER = ""; 405 | SWIFT_OBJC_BRIDGING_HEADER = "Brightness Sync/Brightness Sync-Bridging-Header.h"; 406 | SWIFT_VERSION = 5.0; 407 | }; 408 | name = Release; 409 | }; 410 | 922F72A324339AD800AF6830 /* Debug */ = { 411 | isa = XCBuildConfiguration; 412 | buildSettings = { 413 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 414 | CODE_SIGN_ENTITLEMENTS = BrightnessSyncLauncher/BrightnessSyncLauncher.entitlements; 415 | CODE_SIGN_IDENTITY = "Apple Development"; 416 | CODE_SIGN_STYLE = Automatic; 417 | COMBINE_HIDPI_IMAGES = YES; 418 | DEVELOPMENT_ASSET_PATHS = ""; 419 | DEVELOPMENT_TEAM = 7M6D294GQ9; 420 | ENABLE_HARDENED_RUNTIME = YES; 421 | ENABLE_PREVIEWS = YES; 422 | INFOPLIST_FILE = BrightnessSyncLauncher/Info.plist; 423 | LD_RUNPATH_SEARCH_PATHS = ( 424 | "$(inherited)", 425 | "@executable_path/../Frameworks", 426 | ); 427 | MACOSX_DEPLOYMENT_TARGET = 10.15; 428 | PRODUCT_BUNDLE_IDENTIFIER = dev.vandijk.BrightnessSyncLauncher; 429 | PRODUCT_NAME = "$(TARGET_NAME)"; 430 | SKIP_INSTALL = YES; 431 | SWIFT_VERSION = 5.0; 432 | }; 433 | name = Debug; 434 | }; 435 | 922F72A424339AD800AF6830 /* Release */ = { 436 | isa = XCBuildConfiguration; 437 | buildSettings = { 438 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 439 | CODE_SIGN_ENTITLEMENTS = BrightnessSyncLauncher/BrightnessSyncLauncher.entitlements; 440 | CODE_SIGN_IDENTITY = "Apple Development"; 441 | CODE_SIGN_STYLE = Automatic; 442 | COMBINE_HIDPI_IMAGES = YES; 443 | DEVELOPMENT_ASSET_PATHS = ""; 444 | DEVELOPMENT_TEAM = 7M6D294GQ9; 445 | ENABLE_HARDENED_RUNTIME = YES; 446 | ENABLE_PREVIEWS = YES; 447 | INFOPLIST_FILE = BrightnessSyncLauncher/Info.plist; 448 | LD_RUNPATH_SEARCH_PATHS = ( 449 | "$(inherited)", 450 | "@executable_path/../Frameworks", 451 | ); 452 | MACOSX_DEPLOYMENT_TARGET = 10.15; 453 | PRODUCT_BUNDLE_IDENTIFIER = dev.vandijk.BrightnessSyncLauncher; 454 | PRODUCT_NAME = "$(TARGET_NAME)"; 455 | SKIP_INSTALL = YES; 456 | SWIFT_VERSION = 5.0; 457 | }; 458 | name = Release; 459 | }; 460 | /* End XCBuildConfiguration section */ 461 | 462 | /* Begin XCConfigurationList section */ 463 | 922D198F2291B21C00DBF67D /* Build configuration list for PBXProject "Brightness Sync" */ = { 464 | isa = XCConfigurationList; 465 | buildConfigurations = ( 466 | 922D19A02291B21C00DBF67D /* Debug */, 467 | 922D19A12291B21C00DBF67D /* Release */, 468 | ); 469 | defaultConfigurationIsVisible = 0; 470 | defaultConfigurationName = Release; 471 | }; 472 | 922D19A22291B21C00DBF67D /* Build configuration list for PBXNativeTarget "Brightness Sync" */ = { 473 | isa = XCConfigurationList; 474 | buildConfigurations = ( 475 | 922D19A32291B21C00DBF67D /* Debug */, 476 | 922D19A42291B21C00DBF67D /* Release */, 477 | ); 478 | defaultConfigurationIsVisible = 0; 479 | defaultConfigurationName = Release; 480 | }; 481 | 922F72A224339AD800AF6830 /* Build configuration list for PBXNativeTarget "BrightnessSyncLauncher" */ = { 482 | isa = XCConfigurationList; 483 | buildConfigurations = ( 484 | 922F72A324339AD800AF6830 /* Debug */, 485 | 922F72A424339AD800AF6830 /* Release */, 486 | ); 487 | defaultConfigurationIsVisible = 0; 488 | defaultConfigurationName = Release; 489 | }; 490 | /* End XCConfigurationList section */ 491 | }; 492 | rootObject = 922D198C2291B21C00DBF67D /* Project object */; 493 | } 494 | --------------------------------------------------------------------------------