Setting Other Applications' Keyboard Shortcuts using NSUserDefaults - Defaults Not Updating

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:

  1. I’m not correctly persisting the values using NSUserDefaults.
  2. 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.

I suspect that’s the explanation, but I’m afraid I don’t know the solution.

1 Like

Thanks so much for looking it over, Shane (I’ve been using your excellent books as reference!). There’s just not much information out there about accessing the defaults of other applications, and it gets very complex with sandboxing & SIP. If my NSUserDefaults usage above looks good, then it looks like it’s a plist location issue that I will work through.

I’ll post the library when I’m done, since I imagine it could be useful for others.

Could you try making the changes manually with my Prefs Editor from apps.tempel.org

If that is recognized by the apps, then it should work with your code as well and I may be able to help you figure out what’s wrong

2 Likes

Thanks Thomas! I’ve heard great things about your application – it’s on my list of things to try out!

After a lot of frustration, I think I’m mostly there now – I just need to do some more testing & improve the interface.

Below, I’ll refer to the legacy path for preferences (~/Library/Preferences/$BUNDLE_ID.plist) as well as the containerized/sandboxed path (~/Library/Containers/$BUNDLE_ID/Data/Library/Preferences/$BUNDLE_ID.plist).

Some additional things I’ve learned about how keyboard shortcuts are managed on macOS:

  • System Preferences:

    • System Preferences always reads from the containerized path if the container exists, or from the legacy path if it does not. This is true even if the application itself does not read from the containerized path.
    • System Preferences always writes to the legacy path and writes to the containerized path if the container itself exists.
      • The reason for writing to the legacy path for containerized applications is not clear. Perhaps it is intended to allow other applications to read a protected application’s keyboard shortcuts, or perhaps it is just a bug.
      • When removing the last shortcut for a given application, the legacy path is not updated if the container exists. I regard this as a bug.
  • Applications:

    • Sandboxed applications read NSUserKeyEquivalents from the containerized path, and non-sandboxed applications read from the legacy path.
    • Applications also separately read NSUserKeyEquivalents from NSGlobalDomain to get the global shortcuts, which requires special handling of NSUserDefaults (see below).
  • NSUserDefaults:

    • NSUserDefaults normally retrieves values in the following search order (abbreviated; see here):
      1. The application’s domain identified by its bundle id
      2. NSGlobalaDomain
      3. NSRegistrationDomain, which are fallback values set programmatically by the program
    • NSUserDefault’s standard methods normally search in this order, and return the first value that exists for a given key. Note that this is not sufficient for managing keyboard shortcuts, since trying to read from the application’s domain when no custom keyboard shortcuts are set will instead return the value from NSGlobalDomain. The solution for this seems to be calling persistentDomainForName: on an NSUserDefaults instance.
    • Critically, it is also possible to specify a preference domain using a custom path, similar to the defaults command-line tool. From what I can see, this is undocumented, and allows the caller to differentiate between the legacy and containerized domains, using the form /Path/To/$BUNDLE_ID).

Because of this, my solution does the following:
* Like System Preferences, always write to the legacy path and the containerized path if the container exists.
* Read preferences using persistentDomainForName:, to avoid values stored in NSGlobalDomain from being returned if no shortcuts are set in the application’s domain.

Here’s my working solution, which requires some cleaning up & testing (remember, Full Disk Access must be enabled or SIP disabled):

(* Public Release 1.0 *)

use scripting additions
use framework "Foundation"


--- HANDLERS ---
-- Getting a user defaults object.
to getUserDefaults(domain)
	-- Return a NSUserDefaults object for domain, which may be a bundle ID or path-style domain.
	if domain is "NSGlobalDomain" then set domain to ".NSGlobalDomain"
	return current application's NSUserDefaults's alloc()'s initWithSuiteName:domain
end getUserDefaults


-- Getting preference domain paths.
to applicationHasContainer(bundle_id)
	-- Return true if a container exists for bundle_id, false otherwise.
	return my fileExists(POSIX path of (path to home folder) & "Library/Containers/" & bundle_id & "/")
end applicationHasContainer

to getLegacyDomain(bundle_id)
	-- Return the standard preferences domain for bundle_id (without the .plist extension) starting with "~/Library/Preferences/".
	if bundle_id is "NSGlobalDomain" then set bundle_id to ".GlobalPreferences"
	return POSIX path of (path to home folder) & "Library/Preferences/" & bundle_id
end getLegacyDomain

to getSandboxedDomain(bundle_id)
	-- Return the containerized preferences domain for bundle_id (without the .plist extension) for use with sandboxed applications.
	return POSIX path of (path to home folder) & "Library/Containers/" & bundle_id & "/Data/Library/Preferences/" & bundle_id
end getSandboxedDomain


-- Controlling whether applications should appear in the System Preferences "Application Shortcuts" list.
to getGUICustomKeyboardShortcutApplications()
	-- Return the ordered list of applications appearing in the custom keyboard shortcut section of System Preferences.
	set gui_defaults to my getUserDefaults("com.apple.universalaccess")
	return (gui_defaults's stringArrayForKey:"com.apple.custommenu.apps") as list
end getGUICustomKeyboardShortcutApplications

to setGUICustomKeyboardShortcutApplications(bundle_ids)
	-- Set the ordered list of applications appearing in the custom keyboard shortcut section of System Preferences.
	set gui_defaults to my getUserDefaults("com.apple.universalaccess")
	gui_defaults's setObject:(my getUniqueItems(bundle_ids)) forKey:"com.apple.custommenu.apps"
end setGUICustomKeyboardShortcutApplications

to hasGUICustomKeyboardShortcutApplications(bundle_id)
	-- Return whether the application id bundle_id appears in the custom keyboard shortcut section of System Preferences.
	return {bundle_id} is in my getGUICustomKeyboardShortcutApplications()
end hasGUICustomKeyboardShortcutApplications

to addGUICustomKeyboardShortcutApplication(bundle_id)
	-- Add the application id bundle_id to the custom keyboard shortcut section of System Preferences if it is not already included therein.
	set gui_defaults to my getUserDefaults("com.apple.universalaccess")
	set gui_bundle_ids to (gui_defaults's stringArrayForKey:"com.apple.custommenu.apps") as list
	if {bundle_id} is not in gui_bundle_ids then gui_defaults's setObject:(gui_bundle_ids & {bundle_id}) forKey:"com.apple.custommenu.apps"
end addGUICustomKeyboardShortcutApplication

to removeGUICustomKeyboardShortcutApplication(bundle_id)
	-- Remove the application id bundle_id from the custom keyboard shortcut section of System Preferences if it is found therein.
	set gui_defaults to my getUserDefaults("com.apple.universalaccess")
	set gui_bundle_ids to (gui_defaults's stringArrayForKey:"com.apple.custommenu.apps") as list
	if {bundle_id} is in gui_bundle_ids then
		if length of gui_bundle_ids is 1 then -- This is the last bundle ID.
			gui_defaults's setObject:(missing value) forKey:"com.apple.custommenu.apps"
		else -- Other bundle IDs still exist.
			gui_defaults's setObject:(my makeListByRemovingItem(gui_bundle_ids, bundle_id)) forKey:"com.apple.custommenu.apps"
		end if
	end if
end removeGUICustomKeyboardShortcutApplication


-- Manipulating custom keyboard shortcuts.
to getCustomKeyboardShortcutDictionary(bundle_id)
	-- Return an NSDictionary representation of the custom keyboard shortcuts for application id bundle_id.
	-- Like System Preferences, this retrieves the values from the sandboxed domain if the container exists, or the legacy domain if it does not.
	
	-- Get the dictionary representation of the specified domain only (without using the standard defaults search path).
	set user_defaults to my getUserDefaults(bundle_id)
	if my applicationHasContainer(bundle_id) then -- Sandboxed application.
		set dictionary_representation to user_defaults's persistentDomainForName:(my getSandboxedDomain(bundle_id))
	else -- Not sandboxed.
		if bundle_id is "NSGlobalDomain" then
			set dictionary_representation to user_defaults's dictionaryRepresentation()
		else -- Legacy application.
			set dictionary_representation to user_defaults's persistentDomainForName:(my getLegacyDomain(bundle_id))
		end if
	end if
	
	-- Return the dictionary represenation, or an empty dictionary if no shortcuts are found.
	if dictionary_representation is missing value then -- The defaults domain does not exist.
		set shortcut_dictionary to current application's NSDictionary's dictionary()
	else -- The domain exists (but the key may not).
		set shortcut_dictionary to dictionary_representation's objectForKey:"NSUserKeyEquivalents"
		if shortcut_dictionary is missing value then set shortcut_dictionary to current application's NSDictionary's dictionary()
	end if
	return shortcut_dictionary
end getCustomKeyboardShortcutDictionary

to setCustomKeyboardShortcutDictionary(bundle_id, shortcut_dictionary)
	-- Set the custom keyboard shortcuts for application id bundle_id using the NSDictionary shortcut_dictionary.
	-- Like System Preferences, this sets the shortcuts in the legacy domain & sandboxed domain (if the container exists).
	
	-- Get the user defaults.
	set has_container to my applicationHasContainer(bundle_id)
	set legacy_defaults to my getUserDefaults(my getLegacyDomain(bundle_id))
	if has_container then set sandboxed_defaults to my getUserDefaults(my getSandboxedDomain(bundle_id))
	
	-- Add the new custom keyboard shortcut.
	legacy_defaults's setObject:shortcut_dictionary forKey:"NSUserKeyEquivalents"
	if has_container then sandboxed_defaults's setObject:shortcut_dictionary forKey:"NSUserKeyEquivalents"
end setCustomKeyboardShortcutDictionary

to addCustomKeyboardShortcut(bundle_id, menu_title, keyboard_shortcut)
	-- Add the custom keyboard shortcut keyboard_shortcut for menu item menu_title for application id bundle_id. If the menu item already has a keyboard shortcut, it will be updated to the new shortcut.
	-- The menu title is processed to handle menu hierarchies.
	-- Like System Preferences, this adds the shortcut to the legacy domain & sandboxed domain (if the container exists).
	
	-- Get the user defaults.
	set has_container to my applicationHasContainer(bundle_id)
	set legacy_defaults to my getUserDefaults(my getLegacyDomain(bundle_id))
	if has_container then set sandboxed_defaults to my getUserDefaults(my getSandboxedDomain(bundle_id))
	
	-- Get the current keyboard shortcuts.
	set shortcut_dictionary to my getCustomKeyboardShortcutDictionary(bundle_id)
	set mutable_dictionary to current application's NSMutableDictionary's dictionaryWithDictionary:shortcut_dictionary
	
	-- Add the new custom keyboard shortcut.
	mutable_dictionary's setObject:keyboard_shortcut forKey:(my encodeMenuTitle(menu_title))
	legacy_defaults's setObject:mutable_dictionary forKey:"NSUserKeyEquivalents"
	if has_container then sandboxed_defaults's setObject:mutable_dictionary forKey:"NSUserKeyEquivalents"
end addCustomKeyboardShortcut

to removeCustomKeyboardShortcut(bundle_id, menu_title)
	-- Remove the custom keyboard shortcut for menu item menu_title for application id bundle_id.
	-- The menu title is processed to handle menu hierarchies.
	-- Unlike System Preferences, this always removes the shortcut from the legacy domain & sandboxed domain (if the container exists).
	
	-- Get the user defaults.
	set has_container to my applicationHasContainer(bundle_id)
	set legacy_defaults to my getUserDefaults(my getLegacyDomain(bundle_id))
	if has_container then set sandboxed_defaults to my getUserDefaults(my getSandboxedDomain(bundle_id))
	
	-- Get the current keyboard shortcuts.
	set shortcut_dictionary to my getCustomKeyboardShortcutDictionary(bundle_id)
	set mutable_dictionary to current application's NSMutableDictionary's dictionaryWithDictionary:shortcut_dictionary
	
	-- Remove the custom keyboard shortcut.
	mutable_dictionary's removeObjectForKey:(my encodeMenuTitle(menu_title))
	if mutable_dictionary's |count|() as integer is 0 then set mutable_dictionary to missing value
	legacy_defaults's setObject:mutable_dictionary forKey:"NSUserKeyEquivalents"
	if has_container then sandboxed_defaults's setObject:mutable_dictionary forKey:"NSUserKeyEquivalents"
end removeCustomKeyboardShortcut


-- Formatting custom keyboard shortcuts.
to encodeMenuTitle(menu_title)
	-- Convert the plain-text representation of menu hierarchie menu_title (e.g. "File->Save") to its escaped version.
	if menu_title contains "->" then
		return character id 27 & my replaceText(menu_title, "->", character id 27)
	else
		return menu_title
	end if
end encodeMenuTitle


-- Library helpers.
to fileExists(file_specifier)
	try
		((file_specifier as «class furl») as alias)
		return true
	on error number -1700
		try
			using terms from application "System Events"
				((path of file_specifier) as alias)
				return true
			end using terms from
		end try
	end try
	return false
end fileExists

to getUniqueItems(L)	
	set unique_items to {}
	repeat with an_item in L
		set an_item to contents of an_item
		if an_item is not in unique_items then set the end of unique_items to an_item
	end repeat
	return unique_items
end getUniqueItems

to makeListByRemovingItem(L, O)
	set L_length to length of L
	set new_L to {}
	repeat with i from 1 to L_length
		set L_item to item i of L
		if L_item is not O then set the end of new_L to L_item
	end repeat
	return new_L
end makeListByRemovingItem

to replaceText(s, search_string, replacement_string)
	set previous_TIDs to AppleScript's text item delimiters
	set AppleScript's text item delimiters to search_string
	set s_text_items to text items of s
	set AppleScript's text item delimiters to replacement_string
	set s to s_text_items as string
	set AppleScript's text item delimiters to previous_TIDs
	return s
end replaceText

If anyone’s interested in testing it or providing feedback, happy to hear it!

1 Like

I’ve updated & extensively commented the code in the previous post. It should now be fully functional & feature-complete! This has been a long, tortuous (and torturous) road for me, so I’m thrilled to be able to share something that works.

At a later date, I will try to fully document the keyboard shortcut format, and determine if there are any API ergonomics to improve.

In brief, the shortcuts use the same format as in DefaultKeyBinding.dict (but delete is \U0008 and forward delete is \U007F). The following are used to specify the modifier keys:

  • ^ is control
  • ~ is option
  • $ is shift
  • @ is command
  • # is used to denote number pad keys on full keyboards (e.g. #1 means 1 key on the number pad)

There’s also a little known feature in System Preferences that allows you to specify full menu hierarchies by separating the items with -> (e.g. File->Save, which is converted behind the scenes to use the escape character as a leading & separating character), which allows for disambiguation in the event multiple menu items exist with the same name. That’s supported here as well & is automatically converted.

The example below shows how to add global shortcuts and application-specific shortcuts for Finder (a non-sandboxed application) and Notes (a sandboxed application).

addGUICustomKeyboardShortcutApplication ensures the application will appear in the list in System Preferences (but is not necessary for the shortcut to be functional).

If creating your own shortcuts, remember:

  • Bundle IDs are case sensitive.
  • Full Disk Access must be enabled for applications protected by SIP (e.g. Safari, Mail, Preview).

-- Add global application shortcuts.
addGUICustomKeyboardShortcutApplication("NSGlobalDomain")
addCustomKeyboardShortcut("NSGlobalDomain", "File->Save As…", "@$s")
addCustomKeyboardShortcut("NSGlobalDomain", "System Preferences…", "@~^,")

-- Add Finder shortcuts.
addGUICustomKeyboardShortcutApplication("com.apple.finder")
addCustomKeyboardShortcut("com.apple.finder", "Show Package Contents", "@s")

-- Add notes shortcuts.
addGUICustomKeyboardShortcutApplication("com.apple.Notes")
addCustomKeyboardShortcut("com.apple.Notes", "Strikethrough", "@$x")

Applications must be re-started for the changes to take effect. If anyone knows how to post a notification to notify the applications that the preferences have changed (like System Preferences does on recent versions of macOS), I’d love to know!

1 Like

Hi, tree_frog. At first, please name this script.

And write your copyright header to this script. This is yours.

I stored your script as an AppleScript Library and put it to ~/Library/Script Libraries/

Then I wrote a sample script to add keyboard shortcut to my “Kamenoko” app full written in AppleScript and sold at Mac App Store.

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions
use kLib : script "Keyboard Shortcuts Lib"

-- Add Kamenoko shortcuts.
addGUICustomKeyboardShortcutApplication("jp.piyomarusoft.kamenoko1") of kLib
addCustomKeyboardShortcut("jp.piyomarusoft.kamenoko1", "About Kamenoko", "@^s") of kLib

スクリーンショット 2022-02-06 1.57.45

I had to restart my app to show the new keyboard shortcut (Command-Control-s).

I use Japanese language user environment with my Mac. Finder seemed not to accept new sohrtcut. Non-ascii menu item may not be able to add accept new shortcut.

This script is a very valuable one, I ensure.

Takaaki Naganoya
http://piyocast.com/as/

1 Like

This is a really fantastic contribution. I was looking for an easily maintainable way of setting keyboard shortcuts and this would seem to be it. I’ve set it up so that that I have a .csv file with three columns:

App name | Menu name | Shortcut

My script then reads the .csv file and uses @tree_frog’s library to commit each row in turn. So much easier than fiddling with System Preferences every time!

1 Like

Hi All,

First off, @tree_frog you’re brilliant. After being given a work Mac I could barely type let alone code without some significant key mappings (both in the Keyboard Settings and in the DefaultKeyBinding.dict file.

I’ve been working on compiling the list that I have already imported (plus a couple of new mappings to test with) and have put this into a script file and running it directly from the Script Editor. I used @Piyomaru 's approach and compiled your script and put it into the /Library/Script Libraries/ folder so that it would be referenced in my script below:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions
use kLib : script "KeyboardShortcutsUpdateScript"

-- Add Shortcuts copied from Keyboard Shortcuts MacOS spreadsheet.
--addGUICustomKeyboardShortcutApplication("NSGlobalDomain")
--addCustomKeyboardShortcut("NSGlobalDomain", "File->New Text File", "^N") of kLib

--addGUICustomKeyboardShortcutApplication("com.apple.finder")
addCustomKeyboardShortcut("com.apple.finder", "Show Package Contents", "@s")

You’ll see I’ve been trying a few things (with, and without the Application row, for the NSGlobalDomain and just for the Finder application). Each of these gives me an error that I can’t seem to find any detail on, other than that there might be an encoding issue. I’ve copied this from the site here, and checked the obvious things like quotes, but has anyone else encountered this and/or know what I’m doing wrong?

error "*** -[BAGenericObjectNoDeleteOSAID addGUICustomKeyboardShortcutApplication]: unrecognized selector sent to object <BAGenericObjectNoDeleteOSAID @0x600000c0c180: OSAID(1) ComponentInstance(0x82000e)>" number -10000

This is an example from the addGUICustomKeyboardShortcutApplication handler error - same error when commenting that out and just trying the addCustomKeyboardShortcut handler.

Thanks in advance

If using a script library that does not define custom terminology in a dictionary, you must target the library either using the possessive form (generally preferred) or using a tell block. The unrecognized selector error in ASObjC generally means that the method/function name can’t be found (either because of a typo, missing use clause, or incorrect target).

Possessive Form:

kLib's addGUICustomKeyboardShortcutApplication("NSGlobalDomain")
kLib's addCustomKeyboardShortcut("NSGlobalDomain", "File->New Text File", "^N")

Tell Block:

tell kLib
    addGUICustomKeyboardShortcutApplication("NSGlobalDomain")
    addCustomKeyboardShortcut("NSGlobalDomain", "File->New Text File", "^N")
end tell
1 Like

Perfect - works like a charm. Thanks!

However I’m stumped at how to put two keys in (F5 and Tab). I’ve tried various ways:

  • Using the symbol directly
  • using \t and \Uf708 respectively
  • using those codes but trying to escape the backslash

But none of them seem to work - some throw an error (presumably because I’m not sending a string) and others run without error but I’ll end up with 8 or t or \ as the shortcut key rather than the ^⇥ or F5 that I am after.

Have you come across this with your dataset and if so, how did you achieve it?

Examples that I’ve run individually that don’t achieve the behaviour I’m looking for:

addCustomKeyboardShortcut("NSGlobalDomain", "Change Tab", "^'\t'") of kLib --Ctrl+Tab
addCustomKeyboardShortcut("NSGlobalDomain", "Change Tab", "^\t") of kLib --Ctrl+Tab
addCustomKeyboardShortcut("NSGlobalDomain", "Change Tab", "^⇥") of kLib --Ctrl+Tab
addCustomKeyboardShortcut("NSGlobalDomain", "Change Tab", "^\t") of kLib --Ctrl+Tab

addCustomKeyboardShortcut("NSGlobalDomain", "View->Reload This Page", "\Uf708") of kLib --F5
addCustomKeyboardShortcut("NSGlobalDomain", "View->Reload This Page", "F5") of kLib
addCustomKeyboardShortcut("NSGlobalDomain", "View->Reload This Page", "\\Uf708") of kLib --F5

I made some progress here, around the Tab. I’m still struggling with the delete key, and the F# keys though if there is anyone that can help around that it’d be much appreciated.

Including Tab in the script:

addCustomKeyboardShortcut("NSGlobalDomain", "Change Tab", "^" & character id 8677) of kLib --Ctrl+Tab

Inserting Del in the script:

addCustomKeyboardShortcut("NSGlobalDomain", "File->Move to Bin", character id 8998) of kLib --Del

Character ID’s were just the unicode for the MacOS symbols, ⇥ and ⌦.

Another useful thing I came across - using wildcards within the Menu Title (esp useful when applications decide to append dynamic text to menu items, such as “Undo Typing “the quick brow…””

addCustomKeyboardShortcut("NSGlobalDomain", "^Edit->Redo.*", "^Y") of kLib --Ctrl+Y

The best way to sort out those special keys is to manually set the shortcut in System Preferences, then interrogate the defaults with the getCustomKeyboardShortcutDictionary(bundle_id) handler supplied in the library above.


This is very interesting, and news to me! Where did you come across this? Is the “magic string” .*?

Originally here from a keyboardmaestro forum (sorry can’t paste links here yet!). I was searching google for selecting a menu using wildcards.

"Yes. If you look at the wiki <> part way down the page it says: “Alternatively, you can start the name with an ^ and use a regular expression to match the menu." So ^Duplicate. would pick out the Duplicate Layer “Bitmap Layer 8” i…*”

So, not actually referring to the system settings, but a third party app - however I started to play with it today and it seemed to work.

After a day of use and trying multiple apps, I seem to be getting some odd behaviour - It does seem to work, but is restricted to some apps, and even then seems to decide when it would like to - a similar grief I have with both my Keyboard Shortcuts added in Settings, and with my DefaultKeyBindings.dict file behaviour. If I find out a more reliable way of doing a wildcard for these Keyboard Shortcuts I’ll post it up here.

And thanks for the info on getting the getCustomKeyboardShortcutDictionary(bundle_id) handler - I’ll give that a go.

I believe all of that is specific to Keyboard Maestro, unfortunately. As far as I know, the native user key equivalents don’t support wildcards (*), choices (Show|Hide), or regular expressions.

As far as I’m aware, the only special features of the native system is the ability to specify menu hierarchies for the purpose of disambiguation using ->. If you don’t specify the hierarchy, though, the system can target toolbar items and PDF print workflows, which is useful.

I haven’t found a way to specifically target the following as part of a hierarchy:

  • The Apple menu
  • Toolbar items
  • PDF print workflows
1 Like

yes, I’ve just tested with an app and this doesn’t work :frowning: (I got so excited​:tada: I thought I’ve finally found a way around this bad system where a menu item has no unique ID you could reference and instead rely on a dynamic name…)

Just an update on my end. I’ve found when using the script in this post to programmatically add shortcuts, I simply missed a whole lot. Not a limitation of the script, simply that every application can have their own. For those going down this path and not having something like keyboard maestro, I’ve come up with an incredibly verbose way of handling it.

  1. Get the shortcut addition script working in this post
  2. Scrape every installed applications potential shortcuts by traversing the menuitems (this script is annoyingly slow as it opens every application, then pulls the shortcuts out one by one). I updated the one in this post (MenuItem scripting | Apple Developer Forums) and restricted it to the applications I cared about (around 75) and let it rip. It took about 2 hours all up.
  3. Combine the .rtf files that are output and plug them into a spreadsheet to create the lines of code needed, to add to the script in this post.
  4. Done! I know have a custom shortcut for every application I use. It doesn’t solve the wildcard problem mentioned above (sorry for the goose chase!) but it comes a lot closer/more accurate to what I was after.

any chance you could share the defaults for those apps so that others don’t have to waste time on that slow process?

(as a side note: is it correct that you can bind Enter to any action (wanted to open an item on Enter in Finder)? I’ve tried setting \U21a9 as that’s what seems to be Enter when I manually set a key combo, and I see it rebound in the Finder menu, but it has no effect. But at least I was able to rebind backspace with <BS> (can’t paste backspace here as is) to go a folder up!)

That’s a very useful little script. It catches the menu items that are not visible unless certain modifier keys are not pressed (e.g. TextEdit’s File->Close All, which is not visible unless the user holds the option key).

I’m not aware of any third-party application that provides a list of unused keyboard shortcuts (I’ve looked in the past), but it could certainly be generated using this approach. I don’t believe there’s a faster way to do this, other than looping through the menus, because of the different ways that keyboard shortcuts can be implemented by application developers.

It would certainly be useful for someone to maintain some kind of database for each major version of macOS.

You’re in for a bad time if you try to bind Return, Enter, Escape, Forward Delete, Space or Delete/Backspace without any modifier keys, because of how they interact with Cocoa text fields. I use command-return to open files - works very well.

(Also, you’re likely aware of this, but just to be sure – Return and Enter are different keys; on the compact keyboard like macbooks, Enter is sent by pressing fn-Return.)