Bringing Music.app's main window to front

I’ve been doing a lot of work on controlling Music.app with my Stream Deck via Keyboard Maestro and/or AppleScript, but this one has been driving me moderately batty. I want to toggle the lyrics panel, but only the one in the main window — not the one in the MiniPlayer. I have both windows open all the time, and the keyboard shortcut is the same, so … it gets complicated.

I’ve come up with two methods, both of which involve focusing the main window and then sending the keyboard shortcut. Due to certain oddities, neither one works under all conditions.

One method that works — sometimes — is this:

Tell application "Music"

	activate

end tell

tell application "System Events"

	try
	
		perform action "AXRaise" of (windows of process "Music" whose title is "Music")
	
	end try
	
	keystroke "u" using {command down, control down}

end tell

Except it doesn’t work reliably when the Music app is in a different Space: if the MiniPlayer was the last window in focus, it gets the keystroke instead. Apparently, AXRaise is being sent while the OS is still switching Spaces and gets lost. Inserting a delay 0.4 after the activate command fixes it, but then it feels sluggish when I’m already in the right Space.

(Side note: the existence of that Try block is related in some strange way. If I’m in the Music app’s Space, AXRaise works but the rest of the script fails because of a -1708 error. If I’m in a different Space, there’s no error. What the heck? :face_with_raised_eyebrow: So the Try block is there just to let my script continue in spite of the error.)

Another method that does what I want — sometimes — is this:

Tell application "Music"

	activate

end tell

tell application "System Events"

	keystroke "0" using command down
	keystroke "u" using {command down, control down}

end tell

The trouble here is that if the main window is already in front, Command-0 acts as a toggle and closes it. Not what I had in mind. I guess I need a way to know when it’s in front and then skip that step altogether.

But if I try

tell application "Music"
	
	activate
	
	if index of browser window "Music" is not 1 then
	
		tell application "System Events"
		
			keystroke "0" using command down
		
		end tell
	
	end if
	
end tell

it behaves just like the AXRaise method, meaning it doesn’t work at all if I’m in a different Space. I have to insert delay 0.4 after activate again, because the index of the “Music” window is always 1 — even if it wasn’t when I left that Space! I have to wonder if that’s why AXRaise fails as well.

I could avoid all of this hassle if only UI scripting of the main window’s lyrics button worked reliably, but the button is unnamed (!!) and its index changes depending on what I’ve been doing in the app. It starts out as “button 5,” but sometimes it’s “button 3” instead. Maddening.

OK, the gears are starting to turn in my brain.

Going back to the original AXRaise method: I can use that error — or rather, the lack of one — to insert the delay only when Music is in another (off-screen) desktop Space. Here’s the current script in its entirety:

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

-- Toggle the lyrics panel in Music.app's main window (*not* the one in the MiniPlayer — that's important for my use case). Making it happen reliably takes some ... finagling.

tell application "System Events" to set musicIsRunning to exists (processes where name is "Music")

if musicIsRunning then
	
	tell application "Music"
		
		-- Activate the Music app so we can send keystrokes to it. Will automatically switch to Music's virtual desktop (aka Space) as needed.
		activate
		
	end tell
	
	tell application "System Events"
		
		try
			
			-- Bring the main window to front so the keystroke doesn't go to the MiniPlayer by mistake.
			perform action "AXRaise" of (windows of process "Music" whose title is "Music")
			
			-- But, if we're coming from another desktop, this will fail because it takes a little under 0.4 secs to switch desktops. The AXRaise command won't work until we've made the transition.
			
			-- AXRaise triggers a -1708 error if Music is in the current desktop (even though the command *works*). That's why we need this Tell block to let us move past the error. It skips the rest of the Tell block and proceeds to the keystroke action.
			
			-- If AXRaise *didn't* trigger an error, on the other hand, it means that Music is in another desktop and we're currently in transit. We can use that to insert a delay here and then redo the AXRaise action so it actually takes effect this time. (If we're already in the right desktop, the aforementioned error causes the script to skip this part, avoiding an unnecessary pause.)
			delay 0.4
			perform action "AXRaise" of (windows of process "Music" whose title is "Music")
			
			-- Et voilà.
			
		end try
		
		-- Now that the main window is in focus, send the Show/Hide Lyrics keyboard shortcut so we can all go home.
		keystroke "u" using {command down, control down}
		
	end tell
	
end if

Elegant? No. But if it works reliably, this will do the job. :grinning:

While the lack of an error gives me a way to see where Music isn’t, relying on it may not be ideal.

The failure of AXRaise to do anything when I’m in another desktop space is probably just a fact of life: apparently, Systems Events has no knowledge of windows in other desktops. But the error it gives when I am in the right space may be a bug for all I can tell — “errAEEventNotHandled:-1708” happens for a number of reasons, like trying to call a handler inside a tell block, but I don’t know if it’s valid in this case — and a bug could get fixed someday, even if by accident.

That link to the Keyboard Maestro forum gives me a different way to see if the “Music” window is in the current desktop space:

tell application "System Events"
	tell application process "Music"
		
		set winNameList to name of every window
		--> {}
	
	end tell
end tell

tell application "Music"
	
	set winNameList to name of every window
	--> {"Music", "MiniPlayer"}

end tell

If System Events doesn’t see the “Music” window, then I know AXRaise will fail before I even try.

So to test:

on windowExistsInThisSpace(thisProcess, thisWindow)
	
	tell application "System Events"
		
		tell application process thisProcess
			
			-- Will return an empty list {} if we're in the wrong Space,
			-- because System Events can't see windows in other Spaces.
			set windowList to name of every window
			
		end tell
		
	end tell
	
	-- Return whether a window by that name exists here.
	return (windowList contains thisWindow)
	
end windowExistsInThisSpace

This specifically tells me whether the main window is open in the current Space. It will also return false if I’m in the right Space but the window is closed — which is fair, since AXRaise won’t work in that case either. So I’ll test again after the 0.4 sec delay, and send Command-0 to open the window if it’s still returning false.

Hi @fuzzywan and welcome to this forum.

The problem with all your attempts is that the targeted application is not in the foreground when sending keystrokes.

This snippet checks if the main window is open, then if a track is playing, and finally opens the lyrics pane if necessary. Each step is performed by checking the menu items.

tell application "Music" to activate

tell application "System Events" to tell application process "Music"
	set frontmost to true
	if not (exists window "Music") then click menu item "Music" of menu 1 of menu bar item 9 of menu bar 1
	set theMenu to a reference to menu item 1 of menu 1 of menu bar item 7 of menu bar 1
	if name of theMenu ≠ "Pause" then return beep 2
	set theMenu to a reference to menu item 6 of menu 1 of menu bar item 6 of menu bar 1
	repeat until enabled of theMenu
		delay 0.01
	end repeat
	if name of theMenu = "Show Lyrics" then click theMenu
end tell

Thanks @ionah!

Most of the problems I’ve been grappling with are related to Music being off screen in a different virtual desktop (aka Space) when I launch the script. System Events can’t see the “Music” window until it finishes switching desktops, so even with your technique it still requires a delay. Otherwise the “Music” menu item ends up closing the window when it’s already open.

The number of the Show/Hide Lyrics menu item varies dramatically, depending on the main window’s view and what happens to be in focus at the time. It’s item 6 when I’m looking at it in UI Browser, but could be 8, 11, or 19 (and who knows what else ) when I actually run the script. It’s a tremendously freaky menu: whole chunks of it will disappear entirely instead of graying out. That’s why I prefer the keyboard shortcut — and I use the script to open or close the Lyrics pane, so a toggle is sufficient.

Still, this also works:

tell application "System Events" to tell application process "Music"

	try

		click menu item "Show Lyrics" of menu 1 of menu bar item "View" of menu bar 1

	on error errorMsg number errorNum

		if errorNum = -1728 then

			try

				click menu item "Hide Lyrics" of menu 1 of menu bar item "View" of menu bar 1

			end try

		end if

	end try

end tell

I’m just not sure it adds anything over keystroke "0" using command down.

It doesn’t matter if a track is playing, paused, or even queued up. Since I still need to raise the main window when it’s open but not in front, the minimum requirement is

tell application "Music" to activate
delay 0.4

tell application "System Events" to tell application process "Music"
	
	if not (exists window "Music") then
		
		click menu item "Music" of menu 1 of menu bar item 9 of menu bar 1
		
	else
		
		try
			
			tell application "System Events" to perform action "AXRaise" of (windows of process "Music" whose title is "Music")
			
		end try
		
	end if
	
	keystroke "u" using {control down, command down}
	
end tell

The added complexity of my version is just about skipping the delay when it isn’t necessary.

I stopped using set frontmost to true a while ago because I haven’t seen any difference in behavior with or without it. Is there ever a situation where activate doesn’t bring an app to front on its own?

I was not aware of that!

Try this:

tell application "Music" to activate

tell application "System Events" to tell application process "Music"
	set frontmost to true
	if not (exists window "Music") then click menu item "Music" of menu 1 of menu bar item 9 of menu bar 1
	set theMenu to a reference to menu item 1 of menu 1 of menu bar item 7 of menu bar 1
	if name of theMenu ≠ "Pause" then tell application "Music" to return display alert "No track is currently playing." message "No lyrics can be shown."
	set theMenu to a reference to menu item "Show Lyrics" of menu 1 of menu bar item 6 of menu bar 1
	if exists theMenu then
		click theMenu
		repeat until enabled of theMenu
			delay 0.01
		end repeat
	end if
end tell

if you are willing to use system events. you don’t need to focus the window. this will click the button as long as the window “exists” including if it is hidden.

tell application "System Events"
	tell application process "Music"
		--get properties of button 3 of splitter group 1 of window "Music"
		click button 3 of splitter group 1 of window "Music"
	end tell
end tell

if you are willing to use system events. you don’t need to focus the window. this will click the button as long as the window “exists” including if it is hidden.

I actually started with that, since it was an obvious way to target the main window and avoid accidentally opening the panel in the MiniPlayer, whereas the menu commands will apply to whichever one is in focus.

But it turns out that the button order changes whenever it feels like it. Sometimes the mute button (to the left of the volume slider) is button 3, and it’s really disconcerting when you try to toggle the lyrics and the audio cuts out on you instead. :rofl:

[rant] I don’t understand why so few of the toolbar icons in Music have labels. You don’t even need to hunt around in UI Browser to see that: just turn on Voice Control in the Accessibility settings and set the overlay to Item Names. Compare Music to something like Excel where everything has a name. Outdone by Microsoft of all people… [/rant]

This is why @ionah’s brought me around to scripting the menu commands. I was especially excited when I realized you can use that trick to add tracks to “Playing Next,” since there’s no AppleScript support for it.

Until yesterday I’d been using Shortcuts to add tracks to the queue, but it’s wayyyy slower than it ought to be. (The shortcut takes less than a second on my iPhone, but almost four on my Mac. Makes me wonder if I have a problem with my Music library file.)

Whereas something like this:

tell application "Music"
		
	activate		
	set thisTrack to track "The Song is Over (Remastered 2022)" of playlist "Who's Next"
	reveal thisTrack
		
	tell application "System Events" to tell application process "Music"
			
		click menu item "Play Next" of menu 1 of menu bar item "Song" of menu bar 1
			
	end tell
		
end tell

is practically instantaneous. :partying_face:

Totally understand and agree about the labels but I actually figured out the button via the description. If you uncomment the get properties line. You will see the description is “lyrics”I believe. So you could use the “whose description is…” to target the button more accurately. I do that in some of my AppleScripts in apps with less or no support for AppleScripting

Ah, I didn’t realize there were deeper levels of info about UI elements. I’m still learning my way around in SD’s Explorer view, and only heard about get properties a couple days ago…

Today, “Lyrics” is button 4, because “Playing Next” is number 3. Of course it is. :roll_eyes:

Anyway, this works:

tell application "Music" to activate
tell application "System Events" to tell application process "Music"
	
	click item 1 of (every button whose description is "Lyrics") of splitter group 1 of window "Music"
	
end tell

I still need to add a delay if Music is in another desktop/Space and make sure the main window is open, but we’ve eliminated AXRaise and its weird error.

Later, I’ll come back to it and add @ionah’s code for checking the “Show/Hide Lyrics” menu item state, only because when the main window is off screen, I definitely expect this script to open the lyrics panel instead of closing it. You can’t control that behavior with the button. So if the menu item is “Hide Lyrics” and the window isn’t visible, I just need to make it visible.

Anyone who doesn’t fool around with Spaces (I have 16 of them right now :crazy_face:) can probably get away with this minimal version of the script.

the button actually has a toggle state and provides a “value”. 0=off or 1=on. so you could just do a value check of the button.

bolded:
{minimum value:missing value, orientation:missing value, position:{1079, 126}, class:button, accessibility description:“Lyrics”, role description:“button”, focused:false, title:“”, size:{44, 40}, help:missing value, entire contents:{}, enabled:true, maximum value:missing value, role:“AXButton”, value:0, subrole:missing value, selected:missing value, name:missing value, description:“Lyrics”}

Now that’s interesting. Makes sense since the button has a visual on/off state.