MenuItem scripting

Hi Everyone,

I'm new to mac's and AppleScript so I apologise in advance for the level of questioning/quality of code.

I've been trying to create an applescript to go through every menuitem and list out the keyboard shortcuts. I've created a script to add keyboard shortcuts, but have only done so as global shortcuts. Some of them don't work in different apps so I'm trying to create a verbose list that has every menuitem for every application so I can populate my script, then eventually have application specific shortcuts for every menuitem that has a shortcut currently.

Below I'm trying to generate that list of current shortcuts but I'm stuck on the syntax of getting the properties of attributes of items and displaying them.

The below is where I have got to:

set allMenus to {}
set everything to {}
set onlyEnabled to {}


tell application "System Events" to tell process appName
    set allMenus to name of every menu of menu bar 1
end tell

global testCounter
global currentCounter
set testCounter to 14
set currentCounter to 0

repeat with menuName in allMenus
    set the end of everything to strings of getAppMenuItems(appName, menuName, onlyEnabled)
    if currentCounter > testCounter then
        log "EXIT REPEAT!!!!!!!!!!!!!!!"
        exit repeat
    end if
end repeat

on getAppMenuItems(appProcess, appMenus, enabledItems)
    tell application "System Events" to tell process appProcess
        
        # Get all menu items
        set theItems to every menu item of menu appMenus of menu bar 1
        
        log "BREAKAKKKKKKBREAKKKKKK"
        set appMenuName to name of menu appMenus of menu bar 1
        --log appname

        repeat with theMenuItem in theItems

            set currentCounter to currentCounter + 1
            if currentCounter > testCounter then
                exit repeat
            end if

            set itemAttrib to get every attribute of theMenuItem

            repeat with aAttrib in itemAttrib  
                if the name of the aAttrib is "AXMenuItemCmdChar" then
                    set props to get properties of attribute "AXMenuItemCmdChar" of every menu item of menu appMenus of menu bar 1
                    repeat with aProps in props
                        if the value of aProps as text is not "missing value" then
                            log (appMenuName & "->" & name of theMenuItem as text & "->" & value of aProps as text)
                        end if
                    end repeat

                    log (appMenuName & "->" & name of theMenuItem as text & "BREAKAKKKKKKBREAKKKKKK")

                end if
            end repeat
        end repeat
        return name of every menu item of menu appMenus of menu bar 1
    end tell
end getAppMenuItems



(*
tell application "System Events"
    get name of menu item 2 of menu 3 of menu bar 1 of process "Finder"
        --> "New Folder"
    get every attribute of menu item 2 of menu 3 of menu bar 1 of process "Finder"
           --> {attribute "AXRole" of menu item "New Finder Window" of menu "File" of menu bar item "File" of menu bar 1 of application process "Finder", [...]
    get properties of attribute "AXMenuItemCmdChar" of [...]
        --> {value:"N", class:attribute, settable:false, name:"AXMenuItemCmdChar"}
    get properties of attribute "AXMenuItemCmdModifiers" of [...]
    --> {value:1, class:attribute, settable:false, name:"AXMenuItemCmdModifiers"}
*)

Currently this outputs:

Apple->About This Mac->Q
Apple->About This Mac->Q
Apple->About This MacBREAKAKKKKKKBREAKKKKKK
Apple->System Information…->Q
Apple->System Information…->Q
Apple->System Information…->Q

From the output I have surmised that when I'm logging the output (log (appMenuName & "->" & name of theMenuItem as text & "->" & value of aProps as text) The properties aren't actually the properties of theMenuItem attribute, but that it has found 3 AXMenuItemCmdChar in the full Apple menu, and is just putting those 3 against each theMenuItem.

I've tried a few ways around this, mainly changing set props to get properties of attribute AXMenuItemCmdChar of every menu item of menu appMenus of menu bar 1 to

set props to get properties of itemAttrib or

set props to get properties of attribute "AXMenuItemCmdChar" of itemAttrib but can't seem to find the right way of getting it to work. Furthermore, how I would correctly reference the parent within my log() statement (I can see the syntax for referencing the child (e.g. x of y of z) but how do you go the other way?

Thanks in advance

Accepted Reply

It is one thing to learn about AppleScript (pay attention to object specifiers and references), but the scripting terminology of any given application is like the wild west - just about the time you think you have everything figured out, some mysterious animal you haven't seen before comes over the hill and eats all your hair.

In System Events, an attribute is a class that is a named data value associated with a UI element. An attribute will have properties such as a name and value, but the attributes themselves vary depending on the UI object. The various class properties are documented in the scripting dictionary, but you will need to go exploring to find out what attributes are available and what they are. A lot of these attributes can be also figured out from the Cocoa API documentation.

There are some examples out there, although most seem to be based on a few StackExchange topics from over a decade ago. I blew the dust off my version, it uses System Events to get an application's menu item shortcuts and writes them to a file. Note that the script scans every menu item, so depending on how many there are it may take a while. Rich Text and symbols for the various keys and their modifiers are used for readability.

property glyphSymbols : {{2, "tab", "⇥"}, {4, "enter", "⌤"}, {9, "space", "␣"}, {10, "delete right", "⌦"}, {11, "return", "↩"}, {23, "delete left", "⌫"}, {27, "escape", "⎋"}, {28, "clear", "⌧"}, {98, "page up", "⇞"}, {100, "left arrow", "←"}, {101, "right arrow", "→"}, {104, "up arrow", "↑"}, {106, "down arrow", "↓"}, {107, "page down", "⇟"}, {111, "F1", "F1"}, {112, "F2", "F2"}, {113, "F3", "F3"}, {114, "F4", "F4"}, {115, "F5", "F5"}, {116, "F6", "F6"}, {117, "F7", "F7"}, {118, "F8", "F8"}, {119, "F9", "F9"}, {120, "F10", "F10"}, {121, "F11", "F11"}, {122, "F12", "F12"}, {135, "F13", "F13"}, {136, "F14", "F14"}, {137, "F15", "F15"}, {140, "eject", "⏏"}, {143, "F16", "F16"}} -- https://www.hammerspoon.org/docs/hs.application.html#menuGlyphs
property modifierSymbols : {{"command", "⌘"}, {"shift-command", "⇧⌘"}, {"option-command", "⌥⌘"}, {"option-shift-command", "⌥⇧⌘"}, {"control-command", "⌃⌘"}, {"control-shift-command", "⌃⇧⌘"}, {"control-option-command", "⌃⌥⌘"}, {"control-option-shift-command", "⌃⌥⇧⌘"}, {"none", ""}, {"shift", "⇧"}, {"option", "⌥"}, {"option-shift", "⌥⇧"}, {"control", "⌃"}, {"control-shift", "⌃⇧"}, {"control-option", "⌃⌥"}, {"control-option-shift", "⌃⌥⇧"}} -- modifier mask bits (⌘ is assumed): 1 = ⇧, 2 = ⌥, 4 = ⌃, 8 = no ⌘, 16 = fn

global ItemsScanned, totalFound -- sums for number of menu items scanned, number of shortcuts found

on run -- example to get a list of menu item key equivalents (shortcuts) for an application
   set output to linefeed
   set {ItemsScanned, totalFound} to {0, 0}
   set appName to name of application ((choose file of type "com.apple.application") as text)
   tell application appName to activate
   tell application "System Events" to tell process appName -- note that process names can be different
      repeat with aMenu in (menus of menu bar items 2 thru -1 of menu bar 1) -- skip Apple menu
         set someResults to my (getshortcuts for aMenu)
         if someResults is not in {"", missing value} then
            set output to output & name of aMenu & ":" & linefeed & someResults
         end if
      end repeat
   end tell
   set outputPath to ((path to desktop) as text) & id of application appName & " menu item shortcuts.rtf"
   do shell script "echo " & quoted form of output & " | /usr/bin/textutil -stdin -convert rtf -output " & quoted form of POSIX path of outputPath -- rich text (for formatting and better symbols)
   return "" & ItemsScanned & " menu items scanned for " & totalFound & " shortcuts" -- global sums
end run

# Recursive handler to get modifiers and any key equivalent (shortcut) for menu items of a menu.
to getshortcuts for someMenu given indent:indent : 1
   set {shortcuts, prefix} to {"", ""}
   repeat indent times
      set prefix to prefix & tab
   end repeat
   tell application "System Events" to repeat with menuItem in (menu items of someMenu)
      tell menuItem to try
         set itemName to name of it
         if itemName is not in {"", missing value} then -- skip separator items and views
            set ItemsScanned to ItemsScanned + 1 -- global sum
            set symbol to my (getCommandSymbols for it)
            if symbol is not missing value then -- found one
               set totalFound to totalFound + 1 -- global sum
               set spacer to tab
               if (count symbol) < 3 or symbol begins with "fn" then set spacer to spacer & tab -- alignment
               set shortcuts to shortcuts & prefix & symbol & space & spacer & itemName & linefeed
            else -- try submenu
               repeat with submenu in menus of it -- keep reference indexes to work with duplicate names
                  set theItems to my (getshortcuts for submenu given indent:(indent + 1))
                  if theItems is not in {"", missing value} then set shortcuts to shortcuts & prefix & itemName & ":" & linefeed & theItems
               end repeat
            end if
         end if
      on error errmess
         log "*** Error getting shortcut for " & (get name of it) & ": " & errmess
      end try
   end repeat
   return shortcuts
end getshortcuts

# Get character symbols as needed for a menu item command.
to getCommandSymbols for menuItem
   tell application "System Events" to tell menuItem to try
      set cmdModifiers to value of attribute "AXMenuItemCmdModifiers" of it
      if cmdModifiers is 8 then -- none
         return missing value
      else if cmdModifiers is 24 then -- function key
         set modifier to "fn"
      else
         set modifier to last item of item (cmdModifiers + 1) of modifierSymbols -- mask starts at 0
      end if
      set cmdChar to value of attribute "AXMenuItemCmdChar" of it
      if cmdChar is not in {"", missing value} then return modifier & cmdChar
      set cmdGlyph to value of attribute "AXMenuItemCmdGlyph" of it
      if cmdGlyph is not in {"", missing value} then repeat with glyph in glyphSymbols
         if contents of first item of glyph is cmdGlyph then return modifier & last item of glyph
      end repeat
   on error errmess
      log "*** Error getting command symbols for " & quoted form of (get name of it) & ": " & errmess
   end try
   return missing value
end getCommandSymbols

Replies

The general idea would be to get the menus of the menu bar, then repeat through the menu items of each of those menus. Since a menu item can also have a menu, a recursive handler can be used to descend the menu structure. Once you have a regular menu item (no submenu), you can then get the values of its attributes to build the shortcut. Attributes of interest would be AXMenuItemCmdModifiers which is a mask of the modifier keys used (shift, option, etc), AXMenuItemCmdChar which is the character, and AXMenuItemCmdGlyph which is a code for a glyph for keys such as tab or backspace that don’t have a character. From there it is just a matter of formatting the results into something readable.

  • Thanks @red_menace. Hadn't been able to find the AXMenuItemCmdGlyph or a good description for the other two attributes so thank you very much for that.

    I understand the basic concept of needing a recursive loop to propagate the menu tree, I'm just struggling to write it in AppleScript (my exp is in tsql, vbs and java - although the latter was a while ago now). Any chance you could help with the basic structure (or point me to a decent tutorial site)?

Add a Comment

It is one thing to learn about AppleScript (pay attention to object specifiers and references), but the scripting terminology of any given application is like the wild west - just about the time you think you have everything figured out, some mysterious animal you haven't seen before comes over the hill and eats all your hair.

In System Events, an attribute is a class that is a named data value associated with a UI element. An attribute will have properties such as a name and value, but the attributes themselves vary depending on the UI object. The various class properties are documented in the scripting dictionary, but you will need to go exploring to find out what attributes are available and what they are. A lot of these attributes can be also figured out from the Cocoa API documentation.

There are some examples out there, although most seem to be based on a few StackExchange topics from over a decade ago. I blew the dust off my version, it uses System Events to get an application's menu item shortcuts and writes them to a file. Note that the script scans every menu item, so depending on how many there are it may take a while. Rich Text and symbols for the various keys and their modifiers are used for readability.

property glyphSymbols : {{2, "tab", "⇥"}, {4, "enter", "⌤"}, {9, "space", "␣"}, {10, "delete right", "⌦"}, {11, "return", "↩"}, {23, "delete left", "⌫"}, {27, "escape", "⎋"}, {28, "clear", "⌧"}, {98, "page up", "⇞"}, {100, "left arrow", "←"}, {101, "right arrow", "→"}, {104, "up arrow", "↑"}, {106, "down arrow", "↓"}, {107, "page down", "⇟"}, {111, "F1", "F1"}, {112, "F2", "F2"}, {113, "F3", "F3"}, {114, "F4", "F4"}, {115, "F5", "F5"}, {116, "F6", "F6"}, {117, "F7", "F7"}, {118, "F8", "F8"}, {119, "F9", "F9"}, {120, "F10", "F10"}, {121, "F11", "F11"}, {122, "F12", "F12"}, {135, "F13", "F13"}, {136, "F14", "F14"}, {137, "F15", "F15"}, {140, "eject", "⏏"}, {143, "F16", "F16"}} -- https://www.hammerspoon.org/docs/hs.application.html#menuGlyphs
property modifierSymbols : {{"command", "⌘"}, {"shift-command", "⇧⌘"}, {"option-command", "⌥⌘"}, {"option-shift-command", "⌥⇧⌘"}, {"control-command", "⌃⌘"}, {"control-shift-command", "⌃⇧⌘"}, {"control-option-command", "⌃⌥⌘"}, {"control-option-shift-command", "⌃⌥⇧⌘"}, {"none", ""}, {"shift", "⇧"}, {"option", "⌥"}, {"option-shift", "⌥⇧"}, {"control", "⌃"}, {"control-shift", "⌃⇧"}, {"control-option", "⌃⌥"}, {"control-option-shift", "⌃⌥⇧"}} -- modifier mask bits (⌘ is assumed): 1 = ⇧, 2 = ⌥, 4 = ⌃, 8 = no ⌘, 16 = fn

global ItemsScanned, totalFound -- sums for number of menu items scanned, number of shortcuts found

on run -- example to get a list of menu item key equivalents (shortcuts) for an application
   set output to linefeed
   set {ItemsScanned, totalFound} to {0, 0}
   set appName to name of application ((choose file of type "com.apple.application") as text)
   tell application appName to activate
   tell application "System Events" to tell process appName -- note that process names can be different
      repeat with aMenu in (menus of menu bar items 2 thru -1 of menu bar 1) -- skip Apple menu
         set someResults to my (getshortcuts for aMenu)
         if someResults is not in {"", missing value} then
            set output to output & name of aMenu & ":" & linefeed & someResults
         end if
      end repeat
   end tell
   set outputPath to ((path to desktop) as text) & id of application appName & " menu item shortcuts.rtf"
   do shell script "echo " & quoted form of output & " | /usr/bin/textutil -stdin -convert rtf -output " & quoted form of POSIX path of outputPath -- rich text (for formatting and better symbols)
   return "" & ItemsScanned & " menu items scanned for " & totalFound & " shortcuts" -- global sums
end run

# Recursive handler to get modifiers and any key equivalent (shortcut) for menu items of a menu.
to getshortcuts for someMenu given indent:indent : 1
   set {shortcuts, prefix} to {"", ""}
   repeat indent times
      set prefix to prefix & tab
   end repeat
   tell application "System Events" to repeat with menuItem in (menu items of someMenu)
      tell menuItem to try
         set itemName to name of it
         if itemName is not in {"", missing value} then -- skip separator items and views
            set ItemsScanned to ItemsScanned + 1 -- global sum
            set symbol to my (getCommandSymbols for it)
            if symbol is not missing value then -- found one
               set totalFound to totalFound + 1 -- global sum
               set spacer to tab
               if (count symbol) < 3 or symbol begins with "fn" then set spacer to spacer & tab -- alignment
               set shortcuts to shortcuts & prefix & symbol & space & spacer & itemName & linefeed
            else -- try submenu
               repeat with submenu in menus of it -- keep reference indexes to work with duplicate names
                  set theItems to my (getshortcuts for submenu given indent:(indent + 1))
                  if theItems is not in {"", missing value} then set shortcuts to shortcuts & prefix & itemName & ":" & linefeed & theItems
               end repeat
            end if
         end if
      on error errmess
         log "*** Error getting shortcut for " & (get name of it) & ": " & errmess
      end try
   end repeat
   return shortcuts
end getshortcuts

# Get character symbols as needed for a menu item command.
to getCommandSymbols for menuItem
   tell application "System Events" to tell menuItem to try
      set cmdModifiers to value of attribute "AXMenuItemCmdModifiers" of it
      if cmdModifiers is 8 then -- none
         return missing value
      else if cmdModifiers is 24 then -- function key
         set modifier to "fn"
      else
         set modifier to last item of item (cmdModifiers + 1) of modifierSymbols -- mask starts at 0
      end if
      set cmdChar to value of attribute "AXMenuItemCmdChar" of it
      if cmdChar is not in {"", missing value} then return modifier & cmdChar
      set cmdGlyph to value of attribute "AXMenuItemCmdGlyph" of it
      if cmdGlyph is not in {"", missing value} then repeat with glyph in glyphSymbols
         if contents of first item of glyph is cmdGlyph then return modifier & last item of glyph
      end repeat
   on error errmess
      log "*** Error getting command symbols for " & quoted form of (get name of it) & ": " & errmess
   end try
   return missing value
end getCommandSymbols

You've been a great help - thank you.