What is Best Method to Set System Preferences Modifier Keys?

finder
foundation
asobjc

(Jim Underwood) #1

I have a colleague who needs to toggle the settings for Control and Command keys, basically switching them each time the script is run. I have written a proof-of-concept script below which uses the System Preferences UI, and it seems to work OK, at least in macOS 10.11.6. I’m not sure if it will work with Sierra.

But I hate using UI scripting, unless there is no alternative.

So,

What is Best Method to Set System Preferences Modifier Keys?

Is there an ASObjC or Shell Script method that will set the modifier keys directly, without using the UI? I have done extensive searching of the Internet, and have not found any.

Screenshot of SP Keyboard Panel

This is what I’m trying to change.

image

If not, then I have some questions about UI scripting, but I’ll wait to hear from you guys first. Hopefully there is another solution.

AppleScript UI Scripting of System Preferences

(*
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    Example to Show How to ReMap Control Key to Command Key
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    • Works in El Capitan
    * Untested in Sierra
*)
use AppleScript version "2.5" -- macOS 10.11.6+
use framework "Foundation"
use scripting additions

set frontApp to path to frontmost application as text

tell application "System Preferences" to activate
set elapTime to my pauseUntilWin("System Preferences", "System Preferences", 1)

tell application "System Events"
  tell process "System Preferences"
    
    click button "Keyboard" of scroll area 1 of window "System Preferences"
    set elapTime to my pauseUntilWin("Keyboard", "System Preferences", 1)
    
    log elapTime
    
    tell window "Keyboard"
      
      repeat until (button "Modifier Keys…" of tab group 1) exists
        delay 0.1
      end repeat
      
      click button "Modifier Keys…" of tab group 1
      
      repeat until (sheet 1) exists
        delay 0.1
      end repeat
      
      tell sheet 1 --  (modifier key list)
        
        --- TOGGLE CONTROL KEY Between CONTROL & COMMAND KEY CODE ---
        tell pop up button "Control (⌃) Key:"
          
          set ctrlVal to value
          set numDown to 3 -- for Command
          if (ctrlVal contains "Command") then set numDown to 1 -- for Control
          set newKeyMap to item numDown of {"Control", "", "Command"}
          
          perform action "AXShowMenu"
          delay 0.5 -- the "delay until exists" method doesn't work for this
          
          
          ### It should work with these menu items, but I get errors ###
          (*
                    --tell menu 1
                    --  --set menuList to title of every menu item
                    --              
                    --              
                    --  tell menu item 4 -- "⌘ Command"
                    --    --perform action "AXPress"
                    --    set menuTitle to title
                    --  end tell
                    --end tell
          *)
          
          ### So I was forced to use Key Codes to Move with Arrows ###
          --- Move Selection to TOP Menu Item ---
          key code 126 using {option down} -- UP Arrow
          
          --- TOGGLE Control Key ---
          repeat with iK from 1 to numDown
            key code 125 -- DOWN Arrow
            delay 0.1
          end repeat
          
          key code 36 -- return            
          
        end tell -- pop up button "Control (⌃) Key:"
        
        click button "OK" -- on sheet 1 (modifier key list)
        
      end tell -- sheet 1    --  (modifier key list)
    end tell -- window "Keyboard"
  end tell -- "System Preferences"
end tell -- "System Events"


tell application "System Preferences" to quit


tell application frontApp
  set msgStr to "CONTROL Key has been mapped to: " & newKeyMap
  set msgTitleStr to "Remap of Modifier Keys"
  display dialog msgStr with title msgTitleStr
end tell

on pauseUntilWin(pWinTitle, pProcessName, pMaxTimeSec)
  local startTime, elapTime, errMsg
  
  set startTime to current application's NSDate's |date|()
  
  tell application "System Events"
    
    repeat until window pWinTitle of process pProcessName exists
      if ((startTime's timeIntervalSinceNow()) > pMaxTimeSec) then
        set errMsg to "Max Time of " & pMaxTimeSec & " exceeded waiting for:" & LF & ¬
          "Window: " & pWinTitle & LF & "Process: " & pProcessName
        log errMsg
        error errMsg
      end if
      delay 0.1
    end repeat
    
    --- Make Sure Window is Fully Defined ---
    set uiElem to UI elements of window pWinTitle of process pProcessName
    
  end tell
  
  set elapTime to (-(round ((startTime's timeIntervalSinceNow()) * 100)) / 100.0)
  
  return elapTime
end pauseUntilWin


(Shane Stanley) #2

There may be some setting you can change via defaults or NSUserDefaults, but that’s also the kind of thing that can change between versions of the OS.


(Jim Underwood) #3

Thanks, Shane. Can you please point me to an example of doing this?
I found a number of hits on “NSUserDefaults”, but none that were of any practical help.


(Shane Stanley) #4

It’s a bit hard in the abstract, but here’s a simple example:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use framework "AppKit"
use scripting additions

set theDefaults to current application's NSUserDefaults's alloc()'s initWithSuiteName:"com.apple.finder"
set showsAll to theDefaults's boolForKey:"AppleShowAllFiles"
theDefaults's setBool:(not (showsAll)) forKey:"AppleShowAllFiles"

As this also shows, sometimes changing the value is not enough; it depends on how the app/process is designed whether the change is recognized immediately or only after a relaunch.


(Jim Underwood) #5

Thanks, but I’m getting an error on this line:

The variable theDefaults is not defined.

I’m running Script Debugger 6.0.5 (6A205) on macOS 10.11.6

So, does this line toggle the setting?

Where would I find the keys for the modifier key settings?
What would the initWithSuiteName parameter be for System Preferences Keyboard?


(Jim Underwood) #6

Shane, I’ve been trying to figure this out. I’ve read a bunch of ObjC documents, including this one, but I’m stumped.

Preferences and Settings Programming Guide

I find the ObjC documents not very helpful:

  • They don’t give any examples
  • They don’t provide critical keys needed.

(Shane Stanley) #7

Whoops — see the corrected line.

No idea. It’s an exercise in reverse engineering.


(Shane Stanley) #8

That’s because they’re mostly not meant to be public.


(Jim Underwood) #9

If I was knowledgable in ObjC, I might stand a chance of doing that.
But I’m not, so that’s a dead end for me.


(Shane Stanley) #10

There’s really nothing related to Objective-C, or any other language, about it. It’s basically a bit of guesswork and trial and error. You change a preference, see if you can find a prefs file that got modified, and then see what it contains.


(Nigel Garvey) #11

On my El Capitan machine, with the user Preferences folder open in list view and sorted by modification date:

  1. Juxtapose the Control and Command key settings in System Preferences and click OK. The ByHost folder immediately rises to the top in the Finder window.

  2. Opening the folder reveals no obvious changes, but in fact it also contains invisible .GlobalPreferences plists with the same ID in their names as the visible files have. The one without more blurb in its name after ‘.plist’ is apparently the current one.

  3. The ID in the names in my own ByHost folder is C40F7B2B-7EFD-5B11-946C-6D7880DB47F6, so:

do shell script "defaults read 'ByHost/.GlobalPreferences.C40F7B2B-7EFD-5B11-946C-6D7880DB47F6'"

The result contains the following entry, which doesn’t exist when the modifier key settings are the defaults:

\"com.apple.keyboard.modifiermapping.1452-567-0\" =     (
            {
        HIDKeyboardModifierMappingDst = 4;
        HIDKeyboardModifierMappingSrc = 2;
    },
            {
        HIDKeyboardModifierMappingDst = 12;
        HIDKeyboardModifierMappingSrc = 10;
    },
            {
        HIDKeyboardModifierMappingDst = 2;
        HIDKeyboardModifierMappingSrc = 4;
    },
            {
        HIDKeyboardModifierMappingDst = 10;
        HIDKeyboardModifierMappingSrc = 12;
    }
);

I suspect that the 1452-567-0 my also be unique to my machine.

  1. To cut a long story short, analysis of results with various settings suggests that:
    i) 0 = Caps Lock, 2 = Control, 3 = Option, and 4 = Command.
    ii) Only one destination/source pair is used to change the function of the Caps Lock key, the destination being the number of the other function and the source being 0.
    iii) Two destination/source pairs are used to change the functions of each of the other modifier keys. In the first pair, the destination is the number of the other function and the source is the number of the original. In the second pair, 8 is added to both numbers except to the number 0.
    iv) iii may not be entirely true after individual settings are restored after multiple changes.

(Shane Stanley) #12

No, I get the same here — maybe it’s a keyboard layout or something.

However, I get quite different values for elsewhere. Here’s what I get when I map command to control and control to command:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use framework "AppKit"
use scripting additions

set theUUID to do shell script "ioreg -rd1 -c IOPlatformExpertDevice |  awk '/IOPlatformUUID/ { print $3; }'"
set theDefaults to current application's NSUserDefaults's alloc()'s initWithSuiteName:("ByHost/.GlobalPreferences." & theUUID)
theDefaults's dictionaryRepresentation()
theDefaults's objectForKey:"com.apple.keyboard.modifiermapping.1452-597-0"
--> 
(NSArray) {
	{
		HIDKeyboardModifierMappingSrc:30064771296,
		HIDKeyboardModifierMappingDst:30064771299
	},
	{
		HIDKeyboardModifierMappingSrc:30064771300,
		HIDKeyboardModifierMappingDst:30064771303
	},
	{
		HIDKeyboardModifierMappingSrc:30064771299,
		HIDKeyboardModifierMappingDst:30064771296
	},
	{
		HIDKeyboardModifierMappingSrc:30064771303,
		HIDKeyboardModifierMappingDst:30064771300
	}
}

So there seems to be a bit more to it…


(Nigel Garvey) #13

I’ve been doing some more research on this — although if Jim’s still in Houston, he probably has other things to think about at the moment. :frowning:

“1452-567-0” identifies the keyboard. It consists of a vendor id, a product id, and a zero. The only guess I’ve been able to find on the Web about the zero is that it might change if two or more keyboards of the same type were attached to the machine. The two ids, like the UUID, can be found with ioreg. Presumably having more than one keyboard attached would turn up more than one set of results, but I think this is OK for a single keyboard:

set shellScript to "# Derive a suite name from the IOPlatformUUID.
ioreg -rd1 -c IOPlatformExpertDevice | sed -En '/^[[:space:]]*\"?IOPlatformUUID[^[:xdigit:]]+([[:xdigit:]-]+).*$/ s||ByHost/.GlobalPreferences.\\1|p' ;
# Derive a keyboard modifiermapping key from ioreg keyboard data, assuming only one keyboard's attached.
ioreg -n IOHIDKeyboard -r | sed -En '
/\"VendorID\"|\"ProductID\"/ {
	/\"VendorID\"/ x
	H
}
$ {
	g
	s/[^0-9]+([0-9]+)[^0-9]+([0-9]+)/com.apple.keyboard.modifiermapping.\\1-\\2-0/p
}'"
set {suiteName, modifierMappingKey} to paragraphs of (do shell script shellScript)

The reason the array in the plist contains two dictionaries each for Control, Option, and Command is that there are separate ones for the left and right keys.

The reason Shane’s numbers differ wildly from mine is that they’ve changed in Sierra (Apple Technical Note TN2450). Up to El Capitan, the relevant numbers are:

-1: off
0: Caps Lock
(1. left Shift)
2: left Control
3: left Option
4: left Command

(9: right Shift)
10: right Control
11: right Option
12: right Command

In Sierra, they’re:

(-1: off) ?
27: Caps Lock
224: left Control
(225: left Shift)
226: left Option
227: left “GUI”

228: right Control
(229: right Shift)
230: right Option
231: right “GUI”

Additionally, these are OR’d with the hexadecimal value 0x700000000 (30064771072 decimal), so add this to each to match Shane’s numbers.

So far, I’ve not succeeded in writing a script that will actually change the key mapping. If I’ve understood the Xcode documentation correctly, NSUserDefaults can’t change ByHost preferences, and I’ve not yet grasped the other methods. The hidutil command line tool described in TN2450 appears only to have been introduced with Sierra

NG


(Kevin Parrott) #14

Hi all
this may help in some way, i’ve been a long time user of Karabiner Elements as Karbiner is not ready for 10.12 etc, to change key commands around, you can write your own commands etc, really quite cool, hope it helps, source code may add some incite also…