Hello,
I’m writing a setup script to facilitate setting up custom keyboard shortcuts on new Macs.
Keyboard shortcuts are stored as a dictionary of {“Menu Item Name”:“Keyboard Shortcut String”} under the NSUserKeyEquivalents
key in each application’s preference database, whose on-disk location varies depending on whether the application is sandboxed.
For example, assigning the “Strikethrough” menu item to command-shift-x would store this as {"Strikethrough":"@$x"}
.
Initially, I was hoping to do this using a shell script that calls defaults
, but this has some practical limitations…
-
defaults
is not well-suited to managing nested data structures. -
defaults
generally requires values to be set with NextStep-style plist syntax (or XML fragments), which can be quite difficult to generate for non-ASCII values.
…and you end up with weirdness like this:
-
defaults write com.citrix.XenAppViewer NSUserKeyEquivalents --array '{"Preferences\\U2026" = "@,";}'
– Assigns “Preferences…” to command-comma. -
defaults write com.apple.mail NSUserKeyEquivalents --array-add "\033Mailbox\033Erase Deleted Items\033In All Accounts" "@$\\U2327"
– Assigns “Mailbox->Erase Deleted Items->In All Accounts” to comand-shift-clear.
Because of this, ASObjC seems ideally suited for setting the keyboard shortcuts, since it has no dependencies on new machines (unlike ObjC, swift, swift-sh, or PyObjC).
The issue:
I’m having trouble getting changes to NSUserDefaults to be recognized by the applications. With my code below, the keyboard shortcuts appear to be written correctly to disk (and appear in ~/Library/Preferences/$BUNDLE_ID.plist
), but are not recognized by the applications or System Preferences, nor by defaults
.
Any advice would be greatly appreciated!
use scripting additions
use framework "Foundation"
-- Test example.
set prefs_before to my getCustomKeyboardShortcuts("com.apple.Notes")
my addCustomKeyboardShortcut("com.apple.Notes", "Bulleted List", "@l")
set prefs_after to my getCustomKeyboardShortcuts("com.apple.Notes")
set defaults_after to do shell script "defaults read com.apple.Notes NSUserKeyEquivalents"
return
--- HANDLERS ---
-- Getting user defaults.
to getUserDefaults(bundle_id)
return current application's NSUserDefaults's alloc()'s initWithSuiteName:bundle_id
end getUserDefaults
-- Setting keyboard shortcuts.
to getCustomKeyboardShortcuts(bundle_id)
set user_defaults to my getUserDefaults(bundle_id)
set shortcut_dictionary to user_defaults's dictionaryForKey:"NSUserKeyEquivalents"
return shortcut_dictionary
end getCustomKeyboardShortcuts
to addCustomKeyboardShortcut(bundle_id, menu_title, keyboard_shortcut)
set user_defaults to my getUserDefaults(bundle_id)
set shortcut_dictionary to user_defaults's dictionaryForKey:"NSUserKeyEquivalents"
set mutable_dictionary to current application's NSMutableDictionary's dictionaryWithDictionary:shortcut_dictionary
mutable_dictionary's setObject:keyboard_shortcut forKey:menu_title
user_defaults's setObject:mutable_dictionary forKey:"NSUserKeyEquivalents"
#user_defaults's synchronize() -- No effect.
end addCustomKeyboardShortcut
Running this results in:
prefs_before is {"Strikethrough":"@$x"}
prefs_after is {"Strikethrough":"@$x", "Bulleted List":"@l"}
– (Great!)
but, defaults_after is still {"Strikethrough":"@$x"}
– (Not so great!)
So the question is, how do I get cfprefsd to recognize that I’ve updated the defaults?
I feel that it’s one of two issues:
- I’m not correctly persisting the values using NSUserDefaults.
- I’m not writing to the correct plist (
~/Library/Preferences/com.apple.Notes.plist
vs.~/Library/Containers/com.apple.Notes/Data/Library/Preferences/com.apple.Notes.plist
), and if so, how do I write to the containerized path using NSUserDefaults?
Background FYI:
- The above example was run with 1 custom keyboard shortcut added manually to the Notes application using System Preferences.
- NSUserDefaults domains are case-sensitive! Cf. com.apple.mail vs. com.apple.Notes.
- In order for custom keyboard shortcuts for a given application to display in System Preferences, the bundle id for the application must appear exactly once in the array stored under “com.apple.custommenu.apps” in “com.apple.universalaccess.plist”.
- Adding, editing, or removing a keyboard shortcut to the System Preferences pane causes duplicate settings to be written to the application’s legacy (~/Library/Preferences) & containerized path (if the container itself exists).
- System Preferences always loads the keyboard shortcut from the containerized path (unless the container itself does not exist), even if the application &
defaults
load it from the legacy path. - Certain Apple-developed applications, notably Safari, Mail, and Preview, have their preference files protected with SIP (both read & write access), but
defaults
has not been updated to handle this gracefully.