Maybe Don't Bother Scripting Some macOS System Preferences
I got a new MacBook Pro (MBP), so now I have two Macs, and of course I want to sync a bunch of preferences between them. I want my trackpad to work the same on both machines, I want the same keyboard shortcuts, they’re the same screen size so I want the same resolution on both, and so on.
I’ve seen plenty of people scripting their macOS system preferences. One of the most well known is probably Mathias Bynens’ .macos
script, but there are plenty of others doing this. So when I got my new MBP, I said, “Finally, time for me to get my hastily noted preferences out of an org-mode file and into a reusable script!” After all, I love automation.
After untold hours of searching the web, reading Apple’s open source, disassembling Apple software, and trial and error, I have probably figured out how to make three finger horizontal trackpad swipes go backward/forward pages. On Mojave, at least. It requires using at least one private framework. I expect it to break in some future macOS release—who knows, maybe the one coming out this fall.
Or I could click one single setting in the System Preferences GUI. That takes about ten seconds, including starting System Preferences.
Maybe scripting some preferences isn’t worth it.
Seduction
I usually reinstall my Mac from scratch every couple years. Over time I’ve documented my install process in an org-mode file, including all the system preferences I need to change. As a developer and system administrator, though, the prospect of turning a bunch of manual “click this, change that” instructions into a script that can set up all my preferences quickly, reproducibly, and with no mistakes is nearly irresistible.
Mathias Bynens’ script seems like a great starting point. I quickly found out that it doesn’t include every preference you can find in the System Preferences GUI. That’s completely understandable: he wrote it for himself, so if he likes a default, it’s not going to be in his script.
defaults
is just modifying various Apple plist files throughout the system, so I used fswatch, PlistBuddy, and diff to figure out the preference names for various settings in the System Preferences GUI.
Warning Signs
The first signs I had that this was going to be harder than I thought was that changing a single preference would sometimes change multiple, seemingly unrelated preferences. Take, for example, the very first preference I wanted to change, changing the trackpad’s “Swipe between pages” setting from the default of “Scroll left or right with two fingers” to “Swipe with three fingers”. This seems to change two preferences:
com.apple.driver.AppleBluetoothMultitouch.trackpad.TrackpadThreeFingerHorizSwipeGesture
to enable three finger horizontal swiping.AppleEnableSwipeNavigateWithScrolls
to disable two finger horizontal swiping.
This raises some questions for me:
My trackpad is built-in. It’s not Bluetooth. Why does the preference have
Bluetooth
in the name? Not a big deal, but maybe a bit unexpected, and it does leave me wondering if I’ve really identified the one, correct preference to change.Why is the first preference prefixed with
com.apple
, as many preferences are, but the second has no prefix at all? I think the answer is probably something like: This preference is actually implemented in some “macOS GUI toolkit” library that every application ends up linking, and so the preference has to be read by every application.OK, but why is this two preferences rather than three? Why is three finger swiping seemingly implemented at some level below all applications, whereas two finger swiping is in the toolkit, as my hypothesis goes? Maybe two finger swiping came first during development, three finger swipe later, by which time Apple had decided to implement such preferences outside the GUI toolkit? Or maybe it’s something to do with hardware generations? Maybe two finger swipe was actually mapped to something pre-trackpad-gestures? I don’t know!
Problems
I need to put the trackpad settings aside for a moment so I can talk about keyboard remapping. I map caps lock to control, and I have my option and command modifiers swapped. I have to put my trackpad preferences aside because my muscle memory for these modifiers is far too strong, and so typing and trying to investigate three finger swiping preferences on the new laptop without these modifier changes is far, far too painful.
Changing the keyboard modifiers is pretty easy to do in the System Preferences GUI.
But we know all System Preferences are just different plist entries, right? How hard can it be to change this with defaults
!
The answers at an apple.stackexchange.com question titled “Updating modifier key mappings through defaults command tool” start to give me some idea of just how hard it can be.
First of all, the preference string encodes your keyboard vendor and product ID. That makes sense, since you can actually remap the modifiers separately for each keyboard. It’s still a pain in the ass, as you can see from people in that post using ioreg
, awk
, etc. to try and script pulling out the right IDs.
Next, the key code values for the preferences aren’t at all obvious. (It doesn’t help that defaults
shows you the values in base 10 rather than base 16.) It turns out that the key codes are the USB “usage page” shifted left 32 bits and then or’ed with the key code defined within that usage page. You can find most of these values in /System/Library/Frameworks/IOKit.framework/Headers/hid/IOHIDUsageTables.h
. For example, 30064771299
is 0x7000000E3
, which means kHIDPage_KeyboardOrKeypad
(0x7 << 32)
, key kHIDUsage_KeyboardLeftGUI
(0xE3
). I think “the GUI key” in USB spec parlance means “command key” on macOS.
At some point, System Preferences offered the ability to remap my internal keyboard’s Fn key, and the affected preferences confused me. Remapping the Fn key set preferences for 0xFF00000003
and 0xFF0100000003
. But look at the usage page enum in IOHIDUsageTables.h
!
/* Reserved 0x92 - 0xFEFF */
/* VendorDefined 0xFF00 - 0xFFFF */
Pages 0xFF
and 0xFF01
are not documented. Silly hacker, you need to look at AppleHIDUsageTables.h
for that! I am guessing Apple consider this “private”: I have IOHIDUsageTables.h
installed on my system, presumably thanks to installing Xcode, but I don’t have AppleHIDUsageTables.h
. And, for some reason, that header file in 10.14.1’s IOHIDFamily-1090.22.12
is missing all of its meaningful content, so I had to go back to a previous version of the IOHIDFamily sources to find one with values:
/* Usage Pages */
enum {
kHIDPage_AppleVendor = 0xff00,
/* ... */
kHIDPage_AppleVendorTopCase = 0x00ff
};
So there’s your answer to what those pages are, and further down in the file you can see that key 0x3
is kHIDUsage_AppleVendorKeyboard_Function
or kHIDUsage_AV_TopCase_KeyboardFn
, depending on which page you’re looking at. I haven’t looked into what a “top case” is. I’m guessing that the macOS GUI sets both preferences when you remap Fn to accommodate different generations of hardware which register this key under different usage pages, but that’s a complete guess.
Boiling Frogs
At this point I’ve figured out the name of the keys and values I need to set to remap caps lock to control, and to swap option and command. I write a Python script to do it, parsing hidutil list -m keyboard
output to get the vendor and product IDs that need to be in the preference key. I have to remap both the left and right option and command keys separately, which is fine.
It only took a couple hours.
I write my script. I run it. I try out my newly-remapped modifiers.
The modifiers are all unchanged. Caps lock is still caps lock. Option is still option. Command is still command.
defaults
shows that I set the preferences correctly.
But System Preferences doesn’t show the modifiers have changed.
And clicking something in System Preferences reverted the changes I made with defaults
.
I try this a few more times, methodically. It appears that setting the preferences isn’t enough to make System Preferences see it, nor to make the keys actually remap. And since System Preferences doesn’t show them as changed, I am guessing when I click “OK” in the dialog, it writes out whatever preferences are set there, overriding the inexplicably-ineffective preferences set via defaults
.
At this point I’ve spent a lot of time and I’m pissed off so I’m not giving up. I decide I’ll reverse engineer Keyboard.prefPane
to find out what it’s doing.
It turns out that changing the modifier mappings in System Preferences does two things:
- Sets the preferences, just like my Python script did.
- Makes some calls to
IOHIDServiceClientSetProperty
!
I am not a macOS developer, really, and I am definitely not someone who, say, develops HID drivers for macOS. Therefore, take my conclusion with a grain of salt: It seems that HIDs keep some (most?) preferences in the “I/O registry”1, and it is these preferences that control how the HID is currently behaving. If you just change the on-disk preferences, those won’t take effect unless you log out and back in, or reboot, which implies to me that something in macOS has the job of setting the I/O registry preferences from the on-disk preferences at login. (Maybe “BezelServices”?)
The System Preferences GUI, though, seems to read the values from the I/O registry, not from disk. That’s why my Python script’s changes didn’t show up in the GUI, and why System Preferences was overwriting my preference changes.
Let me stop here and say: I could probably set these preferences, then log out and back in, or reboot, and I think my preferences would take effect in that case. But reboot? What am I, a Windows XP user? My Macs run for months between reboots! Rebooting is admitting defeat. Not an option.
I mentioned before that Apple provides hidutil
. hidutil
can get and set properties from the I/O registry. Great, I’ll use that to set the I/O properties!2
It doesn’t work. I can’t see the existing HIDKeyboardModifierMappingPairs
property, and setting it through hidutil
has no effect.
hidutil
in the Apple IOHIDFamily sources doesn’t compile. I think Apple left out some internal files I would need. But I notice this call in hidutil
’s property.m
:
IOHIDEventSystemClientCreateWithType(
kCFAllocatorDefault,
kIOHIDEventSystemClientTypeSimple,
NULL
);
I remember IOHIDEventSystemClientCreateWithType
from disassembling Keyboard.prefPane
. In that disassembly I see it’s using a different value for the type of connection. So I hack up hidutil
sources to use a value I’m calling kIOHIDEventSystemClientTypeMonitor
instead. (I forget how I arrived at the name for this value, it might be a misleading name.)
Success! By setting the same prefs on disk and in the I/O registry, my modifier changes take effect immediately, and they are reflected in the System Preferences GUI!
And it only took me a couple days!
Well, surely keyboard modifiers are just an exception. Surely other preferences will be easier. Or maybe I’ll have to deal with this I/O registry again…
Despair
Back to making my trackpad three finger horizontal swipe gesture go backward/forward pages. I set the two preferences I discussed above. Just like the keyboard modifiers, setting these preferences caused no changes in the System Preferences UI, and the three finger gesture wasn’t working.
OK, so I probably need to set the matching property in the I/O registry. I already have a modified hidutil
I can use for that!
It doesn’t work. Various incantations such as hidutil property -g -m mouse TrackpadThreeFingerHorizSwipeGesture
give me either no output or just (null)
.3 It can’t even see the existing preference.
Back to IDA, I reverse Trackpad.prefPane
. I see it’s actually using the MTTGestureBackEnd
class in /System/Library/PrivateFrameworks/PreferencePanesSupport.framework
. OK, let’s disassemble that. I find MTTGestureBackEnd
method setThreeFingerHorizSwipe
. All told, changing the three finger swipe preference ends up doing the following, presented here in no particular order:
- Setting the on-disk preferences, as I was
- Calling
CFPreferencesSynchronize
, which I had been too lazy to do up to this point - Setting the properties in the I/O registry, as I wanted to do
- Calling
BSKernelPreferenceChanged
from BezelServices, all of which is undocumented as far as I can tell - Sending notifications after changing
AppleEnableSwipeNavigateWithScrolls
, which I absolutely was not doing
I took this information and actually scripted up completely setting three finger swipe between pages. It seems to work. After running this, three finger swipe works, two finger swipe doesn’t, and the System Preferences GUI reflects “Swipe with three fingers” for “Swipe between pages”. The BSKernelPreferenceChanged
didn’t seem to have any effect, so I left it commented out.
Pyrrhic Victory
I’ve spent a lot of my free time over the past week figuring this out, probably way too much. Did I pick the worst possible two preferences to start with, each having all sorts of special cases? Maybe. Will all of the other preferences I want to set be free of special cases? Maybe just set on-disk and I/O prefs and you’re done? Maybe. But probably not: after all the disassembly I’ve done, I think there’s going to be more “special” cases.
And what about when macOS changes? Will I be able to link to these private frameworks in the future? Will the preferences change? Will the APIs change? How many macOS releases until my Python scripts start hitting segmentation faults? Will it even be a year before they rot?
I think I’ve come to the conclusion that macOS doesn’t really want me to set all these preferences via anything other than the System Preferences GUI. Going against Apple’s grain here is just not worth it for me.
Maybe this applies to just HID preferences, or some other subset of preferences. I’m definitely still going to script some preferences. But at least for these HID preferences, I think I’m going back to my org file. Or maybe I’ll just use defaults
and then reboot afterwards, like a barbarian.
Epilogue
This wasn’t a complete waste. I am very disappointed that Apple has made scripting these preferences so hard, but I love reverse engineering.
I got to use Ghidra for the first time, and it’s quite good! On macOS with OpenJDK from MacPorts I did have to restrict it to 4 threads, lest it die constantly on locks, something like that.
It’s also been a long time since I’ve used IDA, and I don’t think I have ever used its native macOS version. The free version for macOS is also quite good. Looks like it’s a Qt app, and it feels more native and more responsive than Ghidra. I think it may do a little bit better than Ghidra when it comes to disassembling Objective-C. However, things like producing graphs and debugging are variously broken or simply not included in the free version of IDA, and of course Ghidra includes a decompiler whereas Hex-Rays charges a good chunk of change for their decompiler.
activateSettings
One final note: There is a binary, /System/Library/PrivateFrameworks/SystemAdministration.framework/Resources/activateSettings
. I disassembled it, and it looks an awful lot like it will take the HID preferences set on disk and write them into the I/O registry, but I think it may have something more to do with activating settings on a new account, or migrating settings from previous versions of macOS. While it was tempting to think that I had found a built-in binary that would all on-disk HID preferences into the I/O registry, I do not believe this is a utility that is normally run by macOS.
You can defaults write com.apple.activatesettings log -BOOL true
, then run it, and get some output from it. It takes a -u
option, and some other options. When it comes to setting the trackpad preferences, though, it appears it won’t do anything unless com.apple.driver.AppleBluetoothMultitouch.trackpad.version
is less than 5
. Mine is 5
by default. I didn’t try setting it to something else, but I suspect doing so will elicit different behavior from activateSettings
, possibly installing all your trackpad preferences into the I/O registry.
Or maybe it will delete your home directory.
If anyone from Apple ever reads this, a command line utility for the purpose of “tell macOS to re-read all preferences from disk and install them like it would at login” would be highly appreciated. And I’d also like a pony.
- That my name for it. Like I said, I’m not really a macOS developer. Corrections welcome. [return]
- Note that Apple’s TN2450 does document a way to remap modifiers. I didn’t use this for two reasons. First of all, you have to run that
hidutil
command at every boot. Not a big deal, just an annoyance. Second, the changes made in this way don’t show up in the System Preferences GUI, and yet they seem to override anything you set there. Imagine you set this up in a script to run at boot. A year later you’re at the Genius Bar getting your shitty MBP keyboard fixed, and you try to switch the command and option modifiers back to normal, because the gal helping you is really having a hard time with your bespoke configuration. In a rush you go to System Preferences. They’re not changed there. “Restore Defaults”. Doesn’t work? WTF? Oh right, I changed this in a script! Now, where is that… how do I disable it… I can’t remember… [return] - For some reason,
hidutil -g -m keyboard TrackpadThreeFingerHorizSwipeGesture
did work, though it showed two devices, probably one for my internal keyboard and one for my Bluetooth keyboard. WTF? I wasn’t comfortable going forward with that. [return]