automatically change keyboard shortcuts when keyboard layout changes

I have a MacBook Pro with American keyboard, but I also use a Bluetooth keyboard with Spanish layout. To cycle through windows I use command+` in the built in keyboard, but when I'm using the Spanish keyboard this does not work and I have to change the shortcut to command+º (this is the key that is in the corresponding position on top of the tab key). Is there a way to quickly or automatically change this shortcut so I don't have to open the settings window every time when I change the input source?


Solution 1:

Updated Answer

The purpose of this update is to provide a fully automated solution to the changing of the keyboard shortcut used for System Preferences > Keyboard > Shortcuts > Keyboard > Move focus to next window when the user switches between English and Spanish - ISO on the Input menu of the menu bar and between the use of the built-in US English keyboard and the attached Bluetooth keyboard with a Spanish - ISO layout.

There was a time when one could use PlistBuddy and or defaults in Mac OS X/OS X to modify an entry in the com.apple.symbolichotkeys.plist file to accomplish this task and it would just automatically update, however, the update is no longer possible in macOS without rebooting. As a result, this will be accomplished by using AppleScript run by osascript as a Launch Agent and UI Scripting of the keyboard shortcut for System Preferences > Keyboard > Shortcuts > Keyboard > Move focus to next window, while not having to reboot.

The example AppleScript code, shown further below, was tested and worked for me without issues as is under macOS Catalina 10.15.7 and macOS Big Sur 11.2.2 as a shell script using a #!/usr/bin/osascript shebang, and a Launch Agent that watches for changes to the com.apple.HIToolbox.plist file and changes to the value of its AppleCurrentKeyboardLayoutInputSourceID key. It also checks the value of parameters of the AppleSymbolicHotKeys for the target keyboard shortcut, (27 in this use case), in the com.apple.symbolichotkeys.plist file to ensure the changes to the com.apple.HIToolbox.plist file only trigger when changing of the target keyboard shortcut because the keyboard input method has changed.

The following steps are necessary to implement this solution and once completed, it is fully automatic thereafter, until the Launch Agent is stopped, unloaded and removed when no longer needed/wanted.


Create the shell script:

Hint: Copy and paste the shell commands from here to Terminal, and use the same window until completely finished.

In Terminal, assuming the pwd is your Home directory:

touch changeKSforMFTNW; open -e changeKSforMFTNW; chmod +x changeKSforMFTNW

Copy and paste the example AppleScript code, shown further below, into the opened changeKSforMFTNW document and then save and close the document.

Back in Terminal:

[ ! -d '/usr/local/bin/' ] && sudo mkdir /usr/local/bin/ 
sudo mv -v changeKSforMFTNW /usr/local/bin/
sudo -k

Create the Launch Agent:

With the shell script taken care of, create the Launch Agent in Terminal, assuming the pwd is your Home directory:

cd Library
[ ! -d 'LaunchAgents' ] && mkdir LaunchAgents
cd LaunchAgents
touch com.my.change.KS.for.MFTNW.plist
open -e com.my.change.KS.for.MFTNW.plist

Copy and paste the example XML Plist code, shown further below, into the opened com.my.change.KS.for.MFTNW.plist document and change you in the value for WatchPaths to your Home directory, then save and close the document.

With the the com.my.change.KS.for.MFTNW.plist file set up and before loading it as the Launch Agent, osascript needs to be added to System Preferences > Security & Privacy > Privacy > Accessibility. To do this, in Terminal run open /usr/bin, and in the Finder window that opens to /usr/bin locate the osascript file so it can be dragged and dropped in the next step.

Open System Preferences to Security & Privacy > Privacy > Accessibility and click the Lock, enter your (Admin) credentials and then drag and drop osascript onto the list on the right side of Accessibility and ensure it is checked. You can then close System Preferences.

NOTE: Make sure you do this before loading the Launch Agent.


Loading the Launch Agent:

Back in Terminal:

launchctl load com.my.change.KS.for.MFTNW.plist

This loads the Launch Agent and the first time you select the target language from the Input menu on the menu bar that is not currently selected, if System Events under osascript System Preferences > Security & Privacy > Privacy > Automation is unchecked or not there, a dialog box, e.g. '"osascript" wants access to control "System Events". Allowing control will provide access to documents and data in "System Events" and to perform actions within that app.', will appear and you need to click the OK button.

Note that if you were presented with the aforementioned dialog box, the shell script will fail this first time because of the dialog box, but just select again the target language that is not currently selected from the Input menu on the menu bar and it should work this time.

NOTE: Because this is using UI Scripting, once you have selected a target from Input menu on the menu bar you will need to allow the UI Scripting to complete uninterrupted!

I found in testing that in normal switching between English and Spanish - ISO on the Input menu of the menu bar that the UI Scripting would take place almost immediately and take two or three seconds to complete. Time may vary base on speed of the system, what other process are running and if any are CPU intensive at the time.


Example AppleScript code:

#!/usr/bin/osascript

--  # Flush cache on the 'com.apple.HIToolbox.plist' file.
--  # Flush cache on the 'com.apple.symbolichotkeys.plist' file.
--  #
--  # The next three lines of code are commented out (with a # character)
--  # and I found them not to be necessary, but have left them in if timing
--  # debugging becomes necessary if the cache does need to be flushed.

# do shell script "defaults read com.apple.HIToolbox.plist 'foobar'; exit 0"
# do shell script "defaults read com.apple.symbolichotkeys 'foobar'; exit 0"
# delay 0.5

--  # Get the fully qualified POSIX pathname of the target .plist file.

set thePropertyListFilePath to POSIX path of ¬
    (path to preferences from user domain) & ¬
    "com.apple.HIToolbox.plist"

--  # See which keyboard layout is the current one.

tell application "System Events" to ¬
    tell property list file thePropertyListFilePath to ¬
        set theCurrentKeyboardLayout to value of ¬
            property list item "AppleCurrentKeyboardLayoutInputSourceID"

--  # Get the value of the parameters of the AppleSymbolicHotKeys for the
--  # target keyboard shortcut as an aggregated numeric string to be used
--  # to stop the script from processing all of its code if the watched .plist
--  # changes for other than to the currently selected keyboard layout.

set shellCMD to ¬
    {"/usr/libexec/PlistBuddy -c ", ¬
        "'Print :AppleSymbolicHotKeys:27:value:parameters' ", ¬
        "$HOME/Library/Preferences/com.apple.symbolichotkeys.plist | ", ¬
        "awk '/[0-9]/{sub(/[ ]+/, \"\"); printf( \"%s\", $0 );}'"} ¬
        as string

set ksNumericValue to do shell script shellCMD

if (ksNumericValue is "96501048576") and ¬
    (theCurrentKeyboardLayout is "com.apple.keylayout.US") then
    return
else if (ksNumericValue is "186101048576") and ¬
    (theCurrentKeyboardLayout is "com.apple.keylayout.Spanish-ISO") then
    return
end if

--  # Check to see if System Preferences is 
--  # running and if yes, then close it.
--  # 
--  # This is done so the script will not fail 
--  # if it is running and a modal sheet is 
--  # showing, hence the use of 'killall' 
--  # as 'quit' fails when done so, if it is.
--  #
--  # This is also done to allow default behaviors
--  # to be predictable from a clean occurrence.

if running of application "System Preferences" then
    try
        tell application "System Preferences" to quit
    on error
        do shell script "killall 'System Preferences'"
    end try
    delay 0.1
end if

--  # Make sure System Preferences is not running before
--  # opening it again. Otherwise there can be an issue
--  # when trying to reopen it while it is actually closing.

repeat while running of application "System Preferences" is true
    delay 0.1
end repeat

--  # Open System Preferences to the Shortcuts tab of the Keyboard pane.
--  # 
--  # This UI Script needs it to be visible, hence the activate command.

tell application "System Preferences"
    activate
    reveal anchor "shortcutsTab" of ¬
        pane id "com.apple.preference.keyboard"
end tell

--  # System Events does the navigating around the 
--  # Shortcuts tab of the Keyboard pane and changes
--  # the value of the target keyboard shortcut.

tell application "System Events"
    
    --  # Make sure the UI is ready to be navigated.
    
    repeat until (exists checkbox 1 of ¬
        tab group 1 of ¬
        window 1 of ¬
        process "System Preferences")
        delay 0.05
    end repeat
    
    --  # Press the Tab key twice.
    
    repeat 2 times
        key code 48
        delay 0.1
    end repeat
    
    --  # Select 'Keyboard' from the list on the left.
    --  # This is wrapped in a try statement to get
    --  # past a bug in how the select command is 
    --  # interpreted and is silently eaten.
    
    try
        select (every row of ¬
            table 1 of ¬
            scroll area 1 of ¬
            splitter group 1 of ¬
            tab group 1 of ¬
            window 1 of ¬
            process "System Preferences" whose ¬
            value of static text 1 = "Keyboard")
    end try
    
    --  # Select 'Move focus to next window' from the 
    --  # list on the right. This is wrapped in a try
    --  # statement to get past a bug in how the select  
    --  # command is interpreted and is silently eaten.
    
    try
        select (every row of ¬
            outline 1 of ¬
            scroll area 2 of ¬
            splitter group 1 of ¬
            tab group 1 of ¬
            window 1 of ¬
            process "System Preferences" whose ¬
            value of static text 1 of ¬
            UI element 2 is "Move focus to next window")
    end try
    
    --  # By default, when System Preferences opens, focus is in
    --  # the Search text box, and depending on whether or not the
    --  # 'Use keyboard navigation to move focus between controls'
    --  # is checked, it makes a difference when 'UI Scripting' is
    --  # being used. Set a flag so as to handle the differences.
    
    set isChecked to ¬
        the value of ¬
            checkbox 1 of ¬
            tab group 1 of ¬
            window 1 of ¬
            process "System Preferences" as boolean
    
    --  # Based on the state of the target check box,
    --  # press the Tab key the necessary times to
    --  # navigate to and or open the text field for
    --  # input of the target keyboard shortcut.
    
    if isChecked then
        delay 0.1
        repeat 3 times
            key code 48 --  # Tab key.
            delay 0.1
        end repeat
    else
        key code 48 --  # Tab key.
        delay 0.01
    end if
    
    --  # Toggle the value of the target keyboard shortcut.
    
    if theCurrentKeyboardLayout is "com.apple.keylayout.US" then
        key code 50 using command down --   # ⌘` keys
    else if theCurrentKeyboardLayout is "com.apple.keylayout.Spanish-ISO" then
        key code 10 using command down --   # ⌘º keys
    end if
    
    delay 0.5
    
end tell

tell application "System Preferences" to quit

Because of the comments and coding style the script is very long. It ends with tell application "System Preferences" to quit, so make sure you highlight all of it when copying and pasting to Script Editor for testing and or into the ** changeKSforMFTNW** shell script for production purposes.


Example XML Plist code:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.my.change.KS.for.MFTNW</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/changeKSforMFTNW</string>
    </array>
    <key>RunAtLoad</key>
    <false/>
    <key>WatchPaths</key>
    <array>
        <string>/Users/you/Library/Preferences/com.apple.HIToolbox.plist</string>
    </array>
</dict>
</plist>

Notes:

  • Be sure to change you in <string>/Users/you/Library/Preferences/com.apple.HIToolbox.plist</string> in the XML Plist file used for the Launch Agent to the name of you Home folder.

  • Sans the code changes to target the correct key code and fully automating the solution, the conditions mentioned in the original answer, listed below, still apply as applicable.

  • The naming convention for the shell script e.g. changeKSforMFTNW stands for, change keyboard shortcut for move focus to next window and applies to that portion of the com.my.change.KS.for.MFTNW.plist file too. Obviously for testing purposes I need to use something. However, change the naming convention as you desire.

  • Some of the shell commands that are formatted as a compound command are done so because in a clean install of macOS the bin directory in /usr/local does not exist even though /usr/local/bin is in the PATH (and path in a zsh shell). As well as the LaunchAgents directory in $HOME/Library. (mkdir -p could be used in lieu of the [ ... ] | && paradigm.)

  • When using Launch Agents and Launch Daemons, I highly recommend reading the manual pages for launchctl, launchd.plist and launchd. You can read the manual page for command in Terminal by typing command and then right-click on it and select: Open man Page

Reverting Changes

To completely undo the changes made by this, in Terminal:

cd ~/Library/LaunchAgents
launchctl stop com.my.change.KS.for.MFTNW.plist
launchctl unload com.my.change.KS.for.MFTNW.plist
rm com.my.change.KS.for.MFTNW.plist
cd /usr/local/bin
sudo rm changeKSforMFTNW

In System Preferences > Security & Privacy > Privacy > Accessibility uncheck and remove osascript if not being used otherwise.

In System Preferences > Security & Privacy > Privacy > Automation uncheck System Events under osascript if not being used otherwise.




Original Answer

Note: The original answer is beening kept as part of the overall answer as the example AppleScript code used as the proof of concept has value for someone else wanting this solution for different keyboard input other then the two languages beening used by the OP.

Whenever I have have to do something repetitiously I automate it in some manner so as to not have to manually take all the steps necessary to accomplish the task.

In this particular case, as much as I do not like to use UI Scripting, nonetheless, I'll do it when it is the easiest way I know, with all things considered.

Presented as a proof of concept, the example AppleScript code shown below was tested and worked for me without issues as is under macOS Catalina 10.15.7 in Script Editor, under the following conditions:

  • System Preferences > Language & Region:
    • Preferred languages:
    • English
    • English (US) — Primary
  • System Preferences > Security & Privacy > Privacy > Accessibility
    • [√] Script Editor
  • System Preferences > Keyboard > Shortcuts > Keyboard
    • [ ] Use keyboard navigation to move focus between controls
    • [√] Use keyboard navigation to move focus between controls
  • Allowed the script to run uninterrupted and not taking focus away from System Preferences, as is necessary in this use case in order to use UI Scripting to set the keyboard shortcut.
  • Changes may be needed to the code in general when run on other versions of macOS and or different Language settings, and was only tested under the aforementioned conditions and the condition expressed in the next paragraph.

I also tested the example AppleScript code saved as an AppleScript application, noting that the application needs to be added to System Preferences > Security & Privacy > Privacy > Accessibility and clicking the OK button on the, e.g., '“Toggle ⌘` - ⌘=“ wants access to control “System Events“. Allowing control will provide access to documents and data in “System Events“, and to perform actions within that app.' dialog box. (Obviously the name of the application will be whatever it was named. I used Toggle ⌘` - ⌘= for testing purposes.)

Although it was not tested in the following manner, nonetheless, the example AppleScript code should also be able to be used in a Run AppleScript action in an Automator Service/Quick Action, or a third-party automation type application that can make use of AppleScript.

The example AppleScript code toggles the value of the keyboard shortcut for: System Preferences > Keyboard > Shortcuts > Keyboard > Move focus to next window

For this proof of concept it toggles between ⌘` and ⌘=, and ⌘= is not really meant to be used, it strictly a placeholder for whatever the key code value of º is, which you'll have to determine.

I determine the value of key codes using a free third-party utility named Key Codes, by Many Tricks, and is available for download from the developers web site, or the Mac AppStore. (I am not affiliated with the developer of Key Codes, just a satisfied user.)

I have also commented the code so you may have an understanding of what it's doing, and it is near the very bottom of the code where you need to change:

key code 24 using command down --   # {"⌘="}

To:

key code ? using command down --    # {"⌘º"}

Where ? is the value for Key Code: in the Key Codes application when the º key is pressed.

Or possibly1 use the keystroke command, e.g.:

keystroke "º" using command down -- # {"⌘º"}

1 Without a Spanish keyboard I cannot effectively test the the keystroke command.



Example AppleScript code:

--  # Check to see if System Preferences is 
--  # running and if yes, then close it.
--  # 
--  # This is done so the script will not fail 
--  # if it is running and a modal sheet is 
--  # showing, hence the use of 'killall' 
--  # as 'quit' fails when done so, if it is.
--  #
--  # This is also done to allow default behaviors
--  # to be predictable from a clean occurrence.

if running of application "System Preferences" then
    try
        tell application "System Preferences" to quit
    on error
        do shell script "killall 'System Preferences'"
    end try
    delay 0.1
end if

--  # Make sure System Preferences is not running before
--  # opening it again. Otherwise there can be an issue
--  # when trying to reopen it while it is actually closing.

repeat while running of application "System Preferences" is true
    delay 0.1
end repeat

--  # Open System Preferences to the Shortcuts tab of the Keyboard pane.
--  # 
--  # This UI Script needs it to be visible, hence the activate command.

tell application "System Preferences"
    activate
    reveal anchor "shortcutsTab" of ¬
        pane id "com.apple.preference.keyboard"
end tell

--  # System Events does the navigating around the 
--  # Shortcuts tab of the Keyboard pane and changes
--  # the value of the target keyboard shortcut.

tell application "System Events"
    
    --  # Make sure the UI is ready to be navigated.
    
    repeat until (exists checkbox 1 of ¬
        tab group 1 of ¬
        window 1 of ¬
        process "System Preferences")
        delay 0.1
    end repeat
    
    --  # Press the Tab key twice.
    
    repeat 2 times
        key code 48
        delay 0.1
    end repeat
    
    --  # Select 'Keyboard' from the list on the left.
    --  # This is wrapped in a try statement to get
    --  # past a bug in how the select command is 
    --  # interpreted and is silently eaten.
    
    try
        select (every row of ¬
            table 1 of ¬
            scroll area 1 of ¬
            splitter group 1 of ¬
            tab group 1 of ¬
            window 1 of ¬
            process "System Preferences" whose ¬
            value of static text 1 = "Keyboard")
    end try
    
    --  # Select 'Move focus to next window' from the 
    --  # list on the right. This is wrapped in a try
    --  # statement to get past a bug in how the select  
    --  # command is interpreted and is silently eaten.
    
    try
        select (every row of ¬
            outline 1 of ¬
            scroll area 2 of ¬
            splitter group 1 of ¬
            tab group 1 of ¬
            window 1 of ¬
            process "System Preferences" whose ¬
            value of static text 1 of ¬
            UI element 2 is "Move focus to next window")
    end try
    
    --  # By default, when System Preferences opens, focus is in
    --  # the Search text box, and depending on whether or not the
    --  # 'Use keyboard navigation to move focus between controls'
    --  # is checked, it makes a difference when 'UI Scripting' is
    --  # being used. Set a flag so as to handle the differences.
    
    set isChecked to ¬
        the value of ¬
            checkbox 1 of ¬
            tab group 1 of ¬
            window 1 of ¬
            process "System Preferences" as boolean
    
    --  # Based on the state of the target check box,
    --  # press the Tab key the necessary times to
    --  # navigate to and or open the text field for
    --  # input of the target keyboard shortcut.
    
    if isChecked then
        delay 0.1
        repeat 3 times
            key code 48 --  # Tab key.
            delay 0.1
        end repeat
    else
        key code 48 --  # Tab key.
        delay 0.01
    end if
    
    --  # Toggle the value of the target keyboard shortcut.
    
    if the value of ¬
        text field 1 of ¬
        UI element 2 of ¬
        (every row of ¬
            outline 1 of ¬
            scroll area 2 of ¬
            splitter group 1 of ¬
            tab group 1 of ¬
            window 1 of ¬
            process "System Preferences" whose ¬
            value of ¬
            static text 1 of ¬
            UI element 2 ¬
                is "Move focus to next window") ¬
            is {"⌘`"} then
        
        key code 24 using command down --   # ⌘= keys
    else
        key code 50 using command down --   # ⌘` keys
    end if
    
    delay 0.5
    
end tell

tell application "System Preferences" to quit

Because of the comments and coding style the script is very long. It ends with tell application "System Preferences" to quit, so make sure you highlight all of it when copying and pasting to Script Editor for testing.



Note: The example AppleScript code is just that and sans any included error handling does not contain any additional error handling as may be appropriate. The onus is upon the user to add any error handling as may be appropriate, needed or wanted. Have a look at the try statement and error statement in the AppleScript Language Guide. See also, Working with Errors. Additionally, the use of the delay command may be necessary between events where appropriate, e.g. delay 0.5, with the value of the delay set appropriately.