Toggle System Grayscale Mode

A colleague recently asked whether it was possible to connect a custom keyboard shortcut to the system-wide “Use grayscale” setting in the macOS Voiceover system preferences:

Screenshot of macOS preference options for accessibility options inlcuding 'Use grayscale'

I could not find any easy way to do this, and searching the web for solutions revealed that most people are addressing this want by using GUI scripting to automate literally opening System Preferences and clicking the pertinent checkbox.

I thought there must be a way to do this in a more streamlined fashion. Couldn’t the option be automated via AppleScript or something? After some brief research, my conclusion was “no.”

At this point I put on my “hacker hat” and proceeded to analyze the System Preferences code that handles the configuration. It’s a binary in /System/Library/PreferencePanes, and the following Terminal command got me on the right path:

cd /System/Library/PreferencePanes/UniversalAccessPref.prefPane/Contents/MacOS/
nm UniversalAccessPref | grep gray

In short, that means “dump all the symbols (nm) from the VoiceOver preference pane, and search them (grep) for the word ‘gray'”. Here’s what it spits out:

0000000000057210 S _OBJC_IVAR_$_UAPDisplayViewController._grayscaleCheckbox
                 U _UAGrayscaleIsEnabled
                 U _UAGrayscaleKey
                 U _UAGrayscaleSetEnabled

These look to me like exactly the names of functions that the preference pane is calling in order to check the current state, and to set the updated state, of the “Use grayscale” checkbox. The capital “U” stands for “Unimplemented.” I.e. it expects to find these symbols, function names in this case, in another library. But which library?

otool -L UniversalAccessPref

The “otool -L” command will dump all the libraries that the preference pane “links to,” meaning the libraries it expects to load functions or data from. There’s a huge list of frameworks in the output, but the most interesting one to me is:

/System/Library/PrivateFrameworks/UniversalAccess.framework/Versions/A/UniversalAccess

The framework name “UniversalAccess” correlates strongly with the “UA” prefix on the pertinent function names we dug up above. Great, so how do we call these? They’re private system functions which means you should not rely on them for production code, but for a quick hack to make toggling grayscale easier? It’s a reasonable risk in my opinion. Here’s a simple C program that takes advantage of the private methods to simply toggle grayscale mode on or off, depending on the current setting.

If you wanted to assign this functionality to a keystroke, as originally suggested, the easiest way in my opinion is to use an app like my own FastScripts. You could drop the compiled binary above into your ~/Library/Scripts folder, and run it directly from FastScripts. Or, if you don’t want to fuss around with compiling a C program, just copy and paste this AppleScript:

-- Line up a Python script for dynamically loading 
-- the private framework  and invoking the required
-- private methods to get current grayscale mode
-- and set it to the opposite value.
set toggleGrayScript to "python -c 'from ctypes import cdll
lib = cdll.LoadLibrary(\"/System/Library/PrivateFrameworks/UniversalAccess.framework/UniversalAccess\")
lib.UAGrayscaleSetEnabled(lib.UAGrayscaleIsEnabled() == 0)
'"
do shell script toggleGrayScript

This script takes advantage of Python’s ability to dynamically load an arbitrary shared library and invoke its exported functions. I wondered if I might be able to use AppleScript’s own “use framework” functionality but I couldn’t quite figure it out.

Hopefully this has been instructive generally for folks who are interested in hacking at system frameworks, and specifically for folks who were looking for an AppleScript for quickly toggling macOS grayscale mode on and off.