Portable UI Applescript for choosing any menu item through any hierarchy of sub-menus

applescriptmenu bar

At the bottom of this page Apple explains how to use UI scripting to automate selecting menu items. There are two hardcoded examples that I'm drawing on in an effort to write a generic library script for use in a number of projects that can choose any menu item from any menu through any hierarchy of submenus.

The TL;DR version of this question is: How do I do that? ?

But for some more detail, if desired…


…I've made some progress, and come up with a few approaches for this but each of them has one fatal flaw or another…

Option 1.

If all the parameters are text or lists of text: eg:

on ChooseMenuItem(theApp, theMenuName, theSubMenusNamesList, theMenuItemName)

which might be called with eg:

ChooseMenuItem("TextEdit", "Format", {"Font", "Ligatures"}, "Use Default")

then I can use a bunch of text parsing and concatenation etc. — including a repeat loop through the theSubMenusNamesList parameter to carefully construct a script in a string something like "tell menu ... click ... tell menu item ... click ... tell menu item ... click ..." etc. and then:

run script theScript in AppleScript

Voila! And this works. Except when any of those parameters – particularly the menu name – doesn't exist. eg. trying to connect or disconnect a bluetooth device through the bluetooth menu extra. So…

Option 2.

If the menu is nameless then let's change theMenuName to theMenu and pass it in by reference. eg:

tell application "System Events" to tell process "SystemUIServer" to set theMenu to (menu bar item 1 of menu bar 1 whose description contains "bluetooth")
...
ChooseMenuItem("SystemUIServer", theMenu, {"Magic Keyboard"}, "Connect")

Note, here theMenu variable and parameter as opposed to theMenuName. After performing the set statement, theMenu variable is not text, it's an AppleScript object, something like:

menu bar item 6 of menu bar 1 of application process "SystemUIServer"

Proceeding from here this fails one of three ways.

2a. If I try to construct a script that includes: tell theMenu ... it tells me, not surprisingly, that …the variable theMenu is not defined.

2b. If I pull theMenu out of the quotes: "tell " & theMenu & "..." or "tell \"" & theMenu & "\"..." it errors with: Can’t make «class mbri» 6 of «class mbar» 1 of «class pcap» "SystemUIServer" of application "System Events" into type Unicode text.

ie. it can't coerce the aforementioned menu bar item 6 of menu bar 1… into text (unlike in Option 1 where I could call the menu by name).

2c. If I try to pull theMenu's tell statement out of the constructed text script so theScript text variable is something like tell menu item ... click ... tell menu item ... click (without the initial tell menu ... click) and then:

tell theMenu
    click
    run script theScript in AppleScript
end tell

then it tells me the variable "click" is not defined. I believe it's referring to the first "click" in theScript variable that run script is trying to … well … run. run script... can't seem to pick up any context from where it's called from, or if it can I haven't figured out how.

Option 3

I've tried to think through how I might somehow include actual tell menu item theSubMenuName ... statements (ie. not text strings of the same that I then try to run) inside the above-mentioned repeat loop, but since it's a hierarchy of tells I can't quite figure out how to make that work. Maybe there's a recursive solution here, but my brain's hurting too much to figure that out.

Conclusion

So, I'm at a loss… I can't figure this out, but I also can't believe there isn't a way to do this. My inclination is that there has to be some way to refer to menu bar item 6 of menu bar 1 of… in my theScript variable somewhere… but how? Or if really not that, then some other ultimate solution here?

Best Answer

After wrestling with this for days before posting the above question, it looks like I figured out a solution - I think the act of actually describing the question in detail to post it got my thinking to there.

I'm sharing it for the benefit of anyone else hitting something similar...

My struggle was with the idea that the hierarchical nature meant increasing the level of tell for each item in theSubMenusNamesList. I couldn't figure out how to put that in a loop without each iteration of the loop going in at that level but then having to come back out at the same level, thus not going deeper.

This was where Option 3 was a fail from the start, which had me going down the rabbit hole of trying to construct the script in text, to then run with run script.

However, Option 3 is actually the answer - code it direct without trying to construct it in text - but I had to wrestle with that some more to get there.

I eventually realized that I can put each iteration's subMenu into a variable - set to menu item ... of the previous iteration of itself. That successfully goes deeper into each level of the hierarchy without pulling out too early.

Of course if that doesn't make sense, then here's the code, which you're welcome to steal and adapt of course, and which may or may not make more sense... ;)

on chooseMenuItem(theApp, theMenu, theSubMenusNamesList, theMenuItemName)
    set theAppName to the name of theApp
    activate theApp
    tell application "System Events" to tell process theAppName
        tell theMenu to click

        repeat with theMenuName in theSubMenusNamesList
            set theMenu to menu item theMenuName of menu 1 of theMenu
            tell theMenu to click
        end repeat

        set theMenuItem to menu item theMenuItemName of menu 1 of theMenu
        tell theMenuItem to click
    end tell
end chooseMenuItem

This works for me - tried with a number of different options. For example: As in the original question, call it with something like:

ChooseMenuItem("TextEdit", "Format", {"Font", "Ligatures"}, "Use Default")

Or an example with no sub menus at all, just leave the third parameter as an empty list:

ChooseMenuItem("TextEdit", "File", {}, "New")

Hope this helps someone.