Background

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 [0.0, 1.0]:

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 pyObjc.

BridgeSupport

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 grep around IOKit.framework to find the values we need, which are:

  1. kIOMasterPortDefault, a constant.
  2. kIODisplayBrightnessKey, a constant.
  3. IOServiceMatching, a function.
  4. IOServiceGetMatchingService, a function.
  5. IODisplaySetFloatParameter, a function.

After poking around the framework, we’ll find that information we need is located in graphics/IOGraphicsLib.h, IOKitLib.h, and graphics/IOGraphicsTypes.h.

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

Rerun the 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"

Changing Brightness

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 IOKit.

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)

Calling 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.

Looking 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.

Using ctypes to Call CoreDisplay’s Private API

Python’s 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 CoreDisplay through ctypes.

ctypes needs to know how to call CoreDisplay_Display_SetUserBrightness

CoreDisplay.CoreDisplay_Display_SetUserBrightness.argtypes = [c_int, c_double]

Let’s test it out:

CoreDisplay.CoreDisplay_Display_SetUserBrightness(0, 0.5)

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)

Conclusion

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!