In this article, we’ll review methods for changing display brightness on a Mac.
I personally find it irritating when my display shuts off, or my computer locks down, while I’m still looking at it. This happens frequently when watching videos, following a recipe or slowly trying to grok something on the screen. Therefore, I have my display set to turn off at an interval that’s over 1 hour. I know this makes me a bad person.
I wrote a daemon that dims my display’s backlight if my computer becomes idle and my face isn’t within the viewing area of my built-in FaceTime webcam.
You can check it out here.
Adjusting Brightness on macOS
Unlike iOS, there is no convenient screen brightness API for macOS.
Here’s how you’d change your screen’s brightness programmatically with Swift in iOS, where
someBrightnessFloat is a
float in the interval
UIScreen.mainScreen().brightness = someBrightnessFloat
It would be convenient if that API were available, but to accomplish this on a Mac, we’ll need to jump through some hoops.
Eventually, through some research, I came across the relevant APIs in the
IOKit Framework. Great!
Bridging Between Objective-C and Python
I decided to build my daemon with
pyObjc and Python 3.7.0, simply because developing command line daemons with Python is fun.
pyObjc tends to have wrappers for Apple’s development frameworks, except none exist for
IOKit. That’s fine, we can wrap it with
pyObjc or even
ctypes if we’re desperate.
Let’s look at the relevant Objective-C code. In Objective-C, we’d want to do something along these lines:
float brightness = ourBrightnessFloatGoesHere; CGDirectDisplayID display = CGMainDisplayID(); io_service_t service = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching(display)); IODisplaySetFloatParameter(service, kNilOptions, CFSTR(kIODisplayBrightnessKey), brightness);
We’ll need to translate that to Python code with
ctypes, we need to provide the library with correct
arg_types and return types. This can be tedious.
Thankfully, BridgeSupport files can be generated from the library with macOS system tools. These files keep function signatures and typing information in an XML structure.
pyObjc can use BridgeSupport data so we don’t have to manually annotate each function we want to call.
We’ll need to
IOKit.framework to find the values we need, which are:
kIOMasterPortDefault, a constant.
kIODisplayBrightnessKey, a constant.
IOServiceMatching, a function.
IOServiceGetMatchingService, a function.
IODisplaySetFloatParameter, a function.
After poking around the framework, we’ll find that information we need is located in
Now, we’ll need to generate a BridgeSupport file.
export BRIDGESUPPORT=".bridgesupport" gen_bridge_metadata -c "-l/System/Library/Frameworks/IOKit.framework/IOKit -I/System/Library/Frameworks/IOKit.framework/Headers/graphics -I/System/Library/Frameworks/IOKit.framework/Headers" \ /System/Library/Frameworks/IOKit.framework/Headers/graphics/IOGraphicsLib.h \ /System/Library/Frameworks/IOKit.framework/Headers/IOKitLib.h \ /System/Library/Frameworks/IOKit.framework/Headers/graphics/IOGraphicsTypes.h > "$BRIDGESUPPORT"
If you’re on MacOS 10.13+, you’ll get an error.
gen_bridge_metadata will look in a directory that doesn’t exist, so we’ll need to symlink the directory we need to the location that doesn’t yet exist.
export TOOLCHAINS_DIR="/Applications/Xcode.app/Contents/Developer/Toolchains" export WORKING_DIR=`pwd` # fix issue where gen_bridge_metadata cannot find toolchain # https://trac.macports.org/ticket/54939 if defaults read loginwindow SystemVersionStampAsString | grep '10.13' then cd "$TOOLCHAINS_DIR" if [ ! -e OSX10.13.xctoolchain ] then sudo ln -s XcodeDefault.xctoolchain OSX10.13.xctoolchain fi cd "$WORKING_DIR" fi
gen_bridge_metadata command again, and we’ll get a nice BridgeSupport file.
We aren’t in the clear yet.
pyObjc supports a certain dialect in BridgeSupport files, and the one we generated will have values the library doesn’t understand. We’ll need to scrub the BridgeSupport file of those values.
IOTimingInformation has a weird signature, and it’s irrelevant to our use case, so let’s get rid of it.
sed -i "" -e "/name='IOTimingInformation'/d" "./$BRIDGESUPPORT"
Great, now we’ve generated a BridgeSupport file for the utilities in
IOKit we’ll use. Now, we can write some application code.
Let’s import our
CFString manipulation libraries:
from CoreFoundation import CFStringCreateWithCString, CFRelease, kCFStringEncodingASCII
Then, let’s write a function to import
from __future__ import annotations from typing import Callable, Any from pathlib importh Path import objc IOKIT_FRAMEWORK: str = "/System/Library/Frameworks/IOKit.framework" BRIDGESUPPORT_FILE: str = ".bridgesupport" DISPLAY_CONNECT: bytes = b"IODisplayConnect" def import_iokit(iokit_location: str = IOKIT_FRAMEWORK, namespace: dict[str, Any] = None): if namespace is None: namespace = globals() bridgesupport_file = Path(BRIDGESUPPORT_FILE).read_text() objc.parseBridgeSupport(bridgesupport_file, namespace, objc.pathForFramework(iokit_location))
When we run
import_iokit() it will read the BridgeSupport file into a buffer, and
pyObjc will parse and load our wrapped functions and values into global namespace.
Now, let’s write a function that uses
IOKit to change our Mac’s brightness:
def set_brightness_iokit(brightness: float) -> int: brightness /= 100 service = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching(DISPLAY_CONNECT)) return IODisplaySetFloatParameter(service, 0, kIODisplayBrightnessKey, brightness)
set_brightness_iokit(50) will dim our display to 50% brightness. Great!
Undocumented CoreDisplay API
Well, if you’re on macOS 10.13+, things aren’t so great. You’ll notice that brightness adjustments using
IOKit’s API reset after a certain period. That is, on macOS 10.13, your brightness will go back to its default setting after N seconds/minutes/hours.
This is because Apple has deprecated
IOKit’s brightness functionality in favor of a private, undocumented API. The words ‘private undocumented API’ should instill fear in any developer who encounters them on their journey to solve a problem. They’re the Here be dragons of software development and dragons are less scary than undocumented features.
After doing some digging, it appears nobody has documented this API, nor is there a convenient Python bridge for it. The closest I’ve come to seeing developers use this API in the open is the hardcore macOS hacking community, who have patched the framework to support older hardware with newer displays & resolutions. Nobody’s touched it in an effort to change screen brightness.
Normally, this is the point where I throw my hands up and say, “well, that was a waste of time” and focus my energy somewhere else. However, I built out a pretty good solution around the
IOKit enabled functionality and I am not ready to give that up.
Reverse Engineering CoreDisplay
First, let’s see if there’s anything we can glean from this framework. We could break out a hex editor, disassembler and all sorts of fun things, but I don’t want to get too crazy, lest it be for naught.
We need to determine which functions handle setting brightness, or if they even exist.
alex@mbp:/System/Library/Frameworks/CoreDisplay.framework$ grep -rn Brightness . Binary file ./CoreDisplay matches
Cool, while there aren’t any header files or code we can use, there seems to be something related to brightness in
CoreDisplay.framework, we don’t have much to work with:
PList files and compiled libraries.
Let’s dig into
CoreDisplay.framework/CoreDisplay a little further:
alex@mbp:/System/Library/Frameworks/CoreDisplay.framework$ strings CoreDisplay | grep Brightness | wc -l 49
There are 49 relevent lines. This is a good thing. Let’s take a deeper look:
alex@mbp:/System/Library/Frameworks/CoreDisplay.framework$ strings CoreDisplay | grep Brightness | grep Set void CoreDisplay::Display::SetUserBrightness(double) void CoreDisplay::Display::SetLinearBrightness(double) void CoreDisplay::Display::SetLinearBacklightBrightness(double) void CoreDisplay::Display::SetBacklightBrightness(double) void CoreDisplay_Display_SetDynamicLinearBrightness(CGDirectDisplayID, double) void CoreDisplay_Display_SetDynamicLinearBrightness_Server(CGDirectDisplayID, double) Skip SetLinearBrightness on offline display 0x%08x void CoreDisplay::Display::SetDynamicLinearBrightness(double) SetDynamicLinearBrightness void CoreDisplay_Display_SetLinearBrightness(CGDirectDisplayID, double) void CoreDisplay_Display_SetLinearBrightness_Server(CGDirectDisplayID, double) void CoreDisplay_Display_SetUserBrightness(CGDirectDisplayID, double) void CoreDisplay_Display_SetUserBrightness_Server(CGDirectDisplayID, double) Skip SetUserBrightness on offline display 0x%08x void CoreDisplay_Display_SetAutoBrightnessIsEnabled(CGDirectDisplayID, bool) void CoreDisplay_Display_SetAutoBrightnessIsEnabled_Server(CGDirectDisplayID, bool) Skip SetAutoBrightnessIsEnabled on offline display 0x%08x void CoreDisplay::Display::SetAutoBrightnessIsEnabled(bool) void CoreDisplay::EDRDisplay::ScheduleSetBrightnessIn(double)_block_invoke ScheduleSetBrightnessIn_block_invoke
We’ve got function signatures! This was easy, no need for breaking out heavier tools and methods.
Inspecting futher, it seems like we’ve got a few functions that are relevant to the problem at hand:
void CoreDisplay::Display::SetUserBrightness(double) void CoreDisplay::Display::SetLinearBrightness(double) void CoreDisplay::Display::SetLinearBacklightBrightness(double) void CoreDisplay::Display::SetBacklightBrightness(double) void CoreDisplay_Display_SetDynamicLinearBrightness(CGDirectDisplayID, double) void CoreDisplay_Display_SetDynamicLinearBrightness_Server(CGDirectDisplayID, double) Skip SetLinearBrightness on offline display 0x%08x void CoreDisplay::Display::SetDynamicLinearBrightness(double) SetDynamicLinearBrightness void CoreDisplay_Display_SetLinearBrightness(CGDirectDisplayID, double) void CoreDisplay_Display_SetLinearBrightness_Server(CGDirectDisplayID, double) void CoreDisplay_Display_SetUserBrightness(CGDirectDisplayID, double)
That last line looks pretty relevant to me, so let’s try to call that function from Python.
ctypes to Call CoreDisplay’s Private API
ctypes gives us a convenient foreign function interface. Here’s how we’d use it for our problem:
from ctypes import CDLL, c_int, c_double CoreDisplay = CDLL("/System/Library/Frameworks/CoreDisplay.framework/CoreDisplay")
Now, we’ve got a handle on
ctypes needs to know how to call
CoreDisplay.CoreDisplay_Display_SetUserBrightness.argtypes = [c_int, c_double]
Let’s test it out:
Yes! This successfully changes the brightness on display
0 to 50%!
Finally, we’ll wrap it in a function so that we can easily call it elsewhere from Python :
def set_brightness_coredisplay(display: int, brightness: float) -> int: brightness /= 100 return CoreDisplay.CoreDisplay_Display_SetUserBrightness(display, brightness)
We’ve learned how to use
pyObjc and BundleSupport files to wrap macOS libraries that aren’t conveniently wrapped for us. We poked around undocumented libraries to locate functionality we’re interested in, and we’ve also learned how to use
ctypes to call out to those private macOS frameworks.
Apparently, we’ve also learned that there’s no limit to the amount of hoops I’ll jump through to make inconsequential things work.
In conclusion, we built the foundation for my daemon that uses facial recognition to manipulate your screen’s brightness. You should check it out!