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

But Backspace works in Finder, i.e., it deletes a char in a text field, and goes up outside a text field! That’s the reason I asked - I thought maybe there is a trick for other single keys as well
Otherwise yes, I just use other apps with better keyboard support since it’s too bad to lose the text field functionality

Return and Enter are different keys

I’m aware they’re differnt, but don’t remember which one is which :slight_smile: But anyway, I copied the one from the defaults read command after setting it in the GUI, so it’s all good

Keyboard Shortcuts MacOS v2.xlsx.zip (1.6 MB)

Ok, this took a bit of tidying up but it should be there now. This excel is a bit messy as I use it to generate the code to put into @tree_frog 's script in this post.

The tabs:

Shortcuts
The main sheet - Yellow columns are input, white are just helpers, green is meant to be pasted into an AppleScript file that calls @tree_frog 's script.

I’ve previously been using this to update with my own shortcut in column E, then running the code in columns J and K.

I’ve added J (the removal code) in there so that I can easily wipe the values each time I run it.

The values in there are the default ones for the Apple applications that I’ve been running - with ONE caveat. You’ll see that there are a number of records in this sheet with the Application as NSGlobalDomain. These were already run on my machine prior to running the script to scrape the shortcuts from each application - meaning that the application specific shortcut would not show the MacOS default in the list but would actually show my new shortcut. There aren’t many there and it should be pretty obvious to see which they are if you ever wanted to back track.

Special Keys
A lookup reference for special keys and their uni codes/character id’s, etc that I use to swap out in various parts of the spreadsheet. Column D here (Use in addCustomKeyboardShortcut) holds the values that actually work when populated into @tree_frog 's script. I’ve tested these individually.

Shortcut Major List
This is me trying to squish the code from “Application Shortcuts” into something I could copy and paste into the “Shortcuts” tab. Green columns being those to copy over, the red columns being source data or helper columns.

Application Code
The application codes of those applications (I will use this to populate the addGUICustomKeyboardShortcutApplication call in @tree_frog 's script).

Applications
The list of applications I used to run the script on

Application Shortcuts
This is the output from the script in the post I linked to above (MenuItem scripting | Apple Developer Forums). It’s just the raw rtf files concatenated into one big long list.

My next two tasks from here are going to be:

  1. Grouping then updating the default shortcuts to the windows eqv (e.g. filtering by “Find” and updating them all to “^F”)
  2. Finding a way to solve the wildcard problem. Now that I’ve got the majority of the default ones here, I’m going to hunt for which applications use wildcards for text editing and attempt to list them out. The main ones I care about are undo/redo and cut/copy/pate - esp when an application changes the Undo menu item to something like “Undo Indentation”. I’m hoping that if I get enough of them I’ll be 95% the way there. I can’t think of a way to get that last 5% (e.g. where there is something like “Open file testing.xlsx…”) without going to a keyboard maestro type app but it may not be a big deal after these changes.
2 Likes

One more question for @tree_frog - I’ve halted my bulk testing of this mainly due to the fact I can’t get the method below to work.

Here’s an example:
addGUICustomKeyboardShortcutApplication(“com.1password.1password”) of kLib

This doesn’t add it when I look into the keyboard shortcut settings (whether or not I have added a shortcut after). I have also double checked the spelling and case and it is correct. Am I passing in the wrong this or is there a quirk here I’m not aware of?

Once I manually create one application specific shortcut, I can add further individual shortcuts for that application (programatically) no problem.

I’d like to avoid manually adding in the ~80 applications prior to running my update so any help would be great!

Thanks a bunch, also for the extra cleaning effort and the guide!

I don’t have 1Password to verify, but make sure the Bundle ID you’re using is for the GUI application & not any helper applications.

The only quirk with the handler is that currently System Preferences must be quit & relaunched in order to update. I’m still on macOS 12 Monterey, so I don’t know if anything has changed with the new System Settings in Ventura.

Also note that the display of the shortcuts in System Preferences is optional (it’s controlled with different plists than the shortcuts themselves). Any shortcuts you add will still work even if you don’t make them visible in System Preferences with addGUICustomKeyboardShortcutApplication, so you won’t be undoing any of your work if you figure out the 1Password part later.

tree_frog’s code was a GREAT help. I could get defaults write ... to add keyboard shortcuts to apps in Ventura (13.5.1), but nothing appeared in System Settings → Keyboard → Keyboard Shortcuts… → App Shortcuts. Further, adding anything through App Shortcuts then deleted any shortcuts added via defaults.

I’ve modified tree_frog’s code into a command-line version that can work as:

prefs_shortcut_writer.applescript com.apple.mail add 'Message->Move to->Mailbox1' '^@1' 'Message->Move to->Mailbox2' '^@2'
prefs_shortcut_writer.applescript com.apple.mail rm  'Message->Move to->Mailbox1' 'Message->Move to->Mailbox2'

Code is below. Feel free to adapt.

#! /usr/bin/osascript

(* Public Release 1.1 *)
-- from https://forum.latenightsw.com/t/setting-other-applications-keyboard-shortcuts-using-nsuserdefaults-defaults-not-updating/3537/1

use scripting additions
use framework "Foundation"

property usage : "usage: prefs_shortcut_writer.applescript [-h | --help] appID add menutitle1 shortcut1 [menutitle2 shortcut2 ...]" & linefeed & "       prefs_shortcut_writer.applescript [-h | --help] appID rm  menutitle1 [menutitle2 ...]"

on run argv
    local n, i, domain, action, arg
    set n to count items of argv
    set i to 0
    repeat while (i < n)
        set arg to item (i + 1) of argv as string
        if arg starts with "-" then
            set i to i + 1
            if arg is "--" then exit repeat
            if {"-h", "-H", "--help"} contains arg then
                return usage
            else
                error "unknown switch: " & arg
            end if
        else
            exit repeat
        end if
    end repeat
    if i + 1 < n then
        set i to i + 1
        set domain to item i of argv as string
        set i to i + 1
        set action to item i of argv as string
    else
        error "must have appID and action arguments" & linefeed & usage
    end if
    if action is "remove" then set action to "rm"
    if {"add", "rm"} does not contain action then error "\"" & action & "\" is not a valid action" & linefeed & usage
    if i < n then
        set arg to items (i + 1) thru -1 of argv as list
    else
        set arg to {}
    end if
    set n to count items of arg
    if action is "add" then
        if n mod 2 is not 0 then error "add action must be followed by menutitle/shortcut pairs" & linefeed & usage
        set n to n div 2
    end if
    verifyFullDiskAccess()
    addGUICustomKeyboardShortcutApplication(domain)
    if action is "add" then
        addCustomKeyboardShortcuts(domain, arg)
    else
        removeCustomKeyboardShortcuts(domain, arg)
    end if
end run



-- NEW HANDLER TO VERIFY FULL DISK ACCESS -- 
to verifyFullDiskAccess()
    local theFile, msg, num
    try
        set theFile to (path to library folder from user domain as string) & "Containers:com.apple.Safari:tmp" -- some file that we cannot write without Full Disk Access
        close access (open for access theFile with write permission)
        do shell script "/bin/rm -f " & quoted form of POSIX path of theFile -- tell application "Finder" to delete theFile
    on error msg number num
        tell application id "com.apple.systempreferences"
            tell pane id "com.apple.settings.PrivacySecurity.extension" to reveal anchor named "Privacy_AllFiles"
        end tell
        tell application "System Events" to tell process "System Settings" -- application id "sevs"
            repeat until window "Full Disk Access" exists
                delay 0.02
            end repeat
        end tell
        tell application id "com.apple.systempreferences" to activate
        error "Need \"Full Disk Access\" to change most system prefs. Toggle the setting \"on\" for \"" & (name of current application) & "\"."
    end try
    return
end verifyFullDiskAccess


--- 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_idxs)) 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 addCustomKeyboardShortcuts(bundle_id, menu_titles__keyboard_shortcuts) -- new handler
    -- Add the custom keyboard shortcut for menu items 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.
    local i
    repeat with i from 1 to (count items of menu_titles__keyboard_shortcuts) by 2
        (mutable_dictionary's setObject:(item (i + 1) of menu_titles__keyboard_shortcuts as string) forKey:(my encodeMenuTitle(item i of menu_titles__keyboard_shortcuts as string)))
    end repeat
    legacy_defaults's setObject:mutable_dictionary forKey:"NSUserKeyEquivalents"
    if has_container then sandboxed_defaults's setObject:mutable_dictionary forKey:"NSUserKeyEquivalents"
end addCustomKeyboardShortcuts
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 removeCustomKeyboardShortcuts(bundle_id, menu_titles) -- new handler
    -- Remove the custom keyboard shortcut for menu items menu_titles 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.
    local menu_title
    repeat with menu_title in menu_titles
        (mutable_dictionary's removeObjectForKey:(my encodeMenuTitle(menu_title as string)))
        if mutable_dictionary's |count|() as integer is 0 then
            set mutable_dictionary to missing value
            exit repeat
        end if
    end repeat
    legacy_defaults's setObject:mutable_dictionary forKey:"NSUserKeyEquivalents"
    if has_container then sandboxed_defaults's setObject:mutable_dictionary forKey:"NSUserKeyEquivalents"
end removeCustomKeyboardShortcuts
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
1 Like

Glad it was helpful.

Did you call e.g. addGUICustomKeyboardShortcutApplication("com.apple.mail")? That’s required to make it show up in System Preferences.

I’m still on macOS Monterey, so haven’t tested on Ventura yet. It’s possible something changed with the System Preferences (now Settings) re-write.

The command line interface looks neat. Just as an FYI – each line will probably take about 0.3 s to execute, as osascript has to re-load the ASOC bridging files each time (could be avoided by using FastScripts from the command line, which keeps the scripting component alive). Not a big deal, but a list of 100 shortcuts may take ~30 seconds to execute.

The first paragraph describe the problems using only the standard defaults command line. The AppleScript approach using addGUICustomKeyboardShortcutApplication(...) worked fine.

I thought about writing the command line interface to take arguments like

prefs_shortcut_writer.applescript domain1 menuitem1 keystroke1 domain2 menuitem2 keystroke2 ...

but decided that my workflow is involved setting multiple keyboard shortcuts but to a limited number of apps, and changing keyboard shortcuts is not something I do often – mostly when setting up a new computer or a wipe-and-reinstall, both of which ideally happen only every few years – so speed is not a huge issue.

1 Like

My updated codes is below … the original addGUICustomKeyboardShortcutApplication() fails if there had never been any custom menu keystrokes (gui_bundle_ids ended up as {missing value}, which then caused the setObject:forKey: on the following line to fail). Also updated code to make sure that FullDiskAccess is enabled.

#! /usr/bin/osascript

(* Public Release 1.2 *)
-- from https://forum.latenightsw.com/t/setting-other-applications-keyboard-shortcuts-using-nsuserdefaults-defaults-not-updating/3537/1

use scripting additions
use framework "Foundation"

on usage()
	local appName, opts, ans
	set appName to name of me & ".applescript"
	set opts to " [-h | --help]"
	set ans to "usage: "
	set ans to ans & appName & opts & " appID add menutitle1 shortcut1 [menutitle2 shortcut2 ...]"
	set ans to ans & linefeed & "       " & appName & opts & " appID rm  menutitle1 [menutitle2 ...]"
	ans
end usage

on run argv
	if class of argv is not list then -- running withing Script Editor
		error "" & (quoted form of name of me) & " is intended to run from the command line."
		-- set argv to {"com.apple.Safari", "rm", "Quit Safari", "@~q"} -- for testing
	end if
	local n, i, arg -- modified 12/29/2023
	set n to count items of argv
	set i to 0
	repeat while (i < n)
		set arg to item (i + 1) of argv as string
		if arg starts with "-" then
			set i to i + 1
			if arg is "--" then exit repeat
			if {"-h", "-H", "--help"} contains arg then
				return usage()
			else
				error "unknown switch: " & arg
			end if
		else
			exit repeat
		end if
	end repeat
	local action, domain -- moved here 12/29/2023
	if i + 1 < n then
		set i to i + 1
		set domain to item i of argv as string
		set i to i + 1
		set action to item i of argv as string
	else
		error "must have appID and action arguments" & linefeed & usage()
	end if
	if action is "remove" then set action to "rm"
	if {"add", "rm"} does not contain action then error "\"" & action & "\" is not a valid action" & linefeed & usage()
	if i < n then
		set arg to items (i + 1) thru -1 of argv as list
	else
		set arg to {}
	end if
	set n to count items of arg
	if action is "add" then
		if n mod 2 is not 0 then error "add action must be followed by menutitle/shortcut pairs" & linefeed & usage()
		set n to n div 2
	end if
	verifyFullDiskAccess()
	addGUICustomKeyboardShortcutApplication(domain)
	if action is "add" then
		addCustomKeyboardShortcuts(domain, arg)
	else
		removeCustomKeyboardShortcuts(domain, arg)
	end if
end run



-- EAJ HANDLERS -- 
to verifyFullDiskAccess()
	local theFile, msg, num
	try
		set theFile to (path to library folder from user domain as string) & "Containers:com.apple.Safari:eajtmp"
		close access (open for access theFile with write permission)
		do shell script "/bin/rm -f " & quoted form of POSIX path of theFile -- tell application "Finder" to delete theFile
	on error msg number num
		activate
		display dialog "Need \"Full Disk Access\" to change most system prefs. Open System Settings to toggle the setting \"on\" for \"" & (name of current application) & "\"?" buttons {"Cancel", "Open System Settings"} cancel button 1 default button 2
		tell application id "com.apple.systempreferences"
			tell pane id "com.apple.settings.PrivacySecurity.extension" to reveal anchor named "Privacy_AllFiles"
		end tell
		try -- bail if Accessibility is not turned on
			tell application "System Events" to tell process "System Settings" -- application id "sevs"
				repeat until window "Full Disk Access" exists
					delay 0.02
				end repeat
			end tell
		on error msg number num
			delay 1 -- just assume this will be long enough
		end try
		tell application id "com.apple.systempreferences" to activate
		local timer
		repeat with timer from 1 to 40
			try
				close access (open for access theFile with write permission)
				do shell script "/bin/rm -f " & quoted form of POSIX path of theFile -- tell application "Finder" to delete theFile
				set num to 0
				activate
				exit repeat
			on error msg number num
				delay 0.25
			end try
		end repeat
		if num is not 0 then verifyFullDiskAccess()
	end try
	return
end verifyFullDiskAccess


--- 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.
	local gui_defaults -- new 12/29/2023
	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.
	local gui_defaults -- new 12/29/2023
	set gui_defaults to my getUserDefaults("com.apple.universalaccess")
	gui_defaults's setObject:(my getUniqueItems(bundle_idxs)) 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.
	local gui_defaults, gui_bundle_ids -- new 12/29/2023
	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 gui_bundle_ids is {missing value} then set gui_bundle_ids to {} -- new 12/29/2023
	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.
	local gui_defaults, gui_bundle_ids -- new 12/29/2023
	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).
	local user_defaults, dictionary_representation -- new 12/29/2023
	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.
	local shortcut_dictionary -- new 12/29/2023
	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.
	local has_container, legacy_defaults -- new 12/29/2023
	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 addCustomKeyboardShortcuts(bundle_id, menu_titles__keyboard_shortcuts)
	-- Add the custom keyboard shortcut for menu items 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.
	local has_container, legacy_defaults -- new 12/29/2023
	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.
	local shortcut_dictionary, mutable_dictionary -- new 12/29/2023
	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.
	local i
	repeat with i from 1 to (count items of menu_titles__keyboard_shortcuts) by 2
		(mutable_dictionary's setObject:(item (i + 1) of menu_titles__keyboard_shortcuts as string) forKey:(my encodeMenuTitle(item i of menu_titles__keyboard_shortcuts as string)))
	end repeat
	legacy_defaults's setObject:mutable_dictionary forKey:"NSUserKeyEquivalents"
	if has_container then sandboxed_defaults's setObject:mutable_dictionary forKey:"NSUserKeyEquivalents"
end addCustomKeyboardShortcuts
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.
	local has_container, legacy_defaults -- new 12/29/2023
	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.
	local shortcut_dictionary, mutable_dictionary -- new 12/29/2023
	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 removeCustomKeyboardShortcuts(bundle_id, menu_titles)
	-- Remove the custom keyboard shortcut for menu items menu_titles 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.
	local has_container, legacy_defaults -- new 12/29/2023
	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.
	local shortcut_dictionary, mutable_dictionary -- new 12/29/2023
	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.
	local menu_title
	repeat with menu_title in menu_titles
		(mutable_dictionary's removeObjectForKey:(my encodeMenuTitle(menu_title as string)))
		if mutable_dictionary's |count|() as integer is 0 then
			set mutable_dictionary to missing value
			exit repeat
		end if
	end repeat
	legacy_defaults's setObject:mutable_dictionary forKey:"NSUserKeyEquivalents"
	if has_container then sandboxed_defaults's setObject:mutable_dictionary forKey:"NSUserKeyEquivalents"
end removeCustomKeyboardShortcuts
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.
	local has_container, legacy_defaults -- new 12/29/2023
	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.
	local shortcut_dictionary, mutable_dictionary -- new 12/29/2023
	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)
	local unique_items, an_item -- new 12/29/2023
	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)
	local L_length, new_L, i, L_item -- new 12/29/2023
	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)
	local previous_TIDs, s_text_items, s -- new 12/29/2023
	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