Switch to Previous Desktop Space

Thanks @_jims , that's a very interesting idea that I'll chew on. It's different from what I usually do, but it has possibilities.

Before I can say yes, I have to really understand them and how it would affect my workflow as well as how it might affect things down the road.

Wow. This solves some problems I hadn't gotten to yet. I've been working on calling AppleScript from Swift, under the assumption that the Swift observer had to be run from there. But your approach is a whole lot simpler. Thanks! I'll take it!

Here's my KM script that will get called:
image

And here's the macro it calls:
image

@CJK ,

I agree, this is really cool.

This is where the actual triggered action occurs, right?:

to notify:message
		global msgs
		set D to (current date) as «class isot» as string
		write D & linefeed as "utf8" starting at eof to msgs
		display notification "Space changed" with title D
end notify:

Where msgs is the file

property msgs : "/private/tmp/NSNotifications.txt"

And these lines in the on run block set up the msgs to start out empty.

		close access (open for access msgs)
		set eof of msgs to 0

So if the action upon triggering was to be calling a KBM macro, msgs would not need to be defined and the KBM macro call would go inside the to notify: block. But calling a KBM macro would not have the notification definition

to notify:message

So it needs something else instead of message there, right?

What? Nothing?

And what goes inside the to notify: block?
Is it as simple as using the AppleScript KBM call, from the template in the KBM macro?:

to notify:
	tell application "Keyboard Maestro Engine"
		do script "Got ActiveSpaceDidChangeNotification"
	end tell
end notify:

I'm sure I'm missing something, but googling those terms is way too generic and doesn't help.

AND am I understanding correctly that having this code in Script Editor, when you first save it, there is an option to save it as a Stay Open application? No Terminal running a script in the background (&)?

Once it is saved, how do you invoke it? Is there a simple idiom to run it "on system startup" or "on Keyboard Maestro startup"?

Thanks.

I think this is Export as Application, right?

When you "Save" or "Save As..." you can set "File Format" to "Application". When you do that you then get the option "Stay open after run handler" -- tick that to get a Stay Open application.

You can invoke it by putting into your account's "Login Items", by having KM launch it at login, etc -- it's just another app. But a better method would be to create a LaunchAgent for it because then you can set it to open at login and auto-relaunch if it quits/crashes. Sounds scary, but actually quite simple -- my fave launchd walkthrough is https://www.launchd.info

1 Like

I'm surprised you got notifications from either of these two methods. Inside an Exectue an AppleScript action, the code would get executed once, then terminate, and the observer won't exists anymore. That explains why there's no notifications using this method. What I'm piqued by is that you got notifications running it from within Script Editor, since the situation is largely the same: once the script terminates, the observer shouldn't exist anymore.

message (or a parameter of any name) needs to be there, or it won't be invoked as a selector by the Objective-C API. With respect to notifications sent out when you switch space, the message variable will always be empty, but it needs to remain. So whether or not KM has access to that variable won't make a difference.

But, otherwise, yes, it should be as simple as you suggest:

to notify:message
	tell application "Keyboard Maestro Engine"
		do script "Got ActiveSpaceDidChangeNotification"
	end tell
end notify:

Give a try first. To be honest, I skimmed your messages, and I'm replying now quickly just because I know I've got a busy w/e ahead of me with my day job, so the next time I'll be able to attend to this is Tuesday. I'll read your messages more thoroughly then and find out how the above attempt went for you. If it doesn't work, I apologise—it'll likely be a result of my skim-reading where I've caught the wrong end of the stick.

1 Like

That's cool. It uses a private Core Services API, CGSCopyManagedDisplaySpaces.

1 Like

Thanks @Nige_S

Thirty years ago I wrote a tutorial on how to use crontab and I used launchd a year ago to automatically do an update on my Trello to-do list using their API. Yes, the concepts are simple, and the syntax needs a refresher every time you pick it up, because the whole point is that you don't use it daily.

The term "private Core Services" sounds like a contradiction to me. How does that work?

That's what I've been using in my prototyping.

And here's the macro it calls:
image

That puts the current Space Number (=N in "Desktop N") into the variable `WhichSpace Number Current".

That variable then goes into a notification of some kind (e.g. Display Text Large), for now, to let me know that the macro is finding the right number:
image

That all depends on having WhichSpace installed and running.

Me too. Using @CJK's AppleScript I've made some minor changes* to my Desktop Spaces macro set and thus far during testing the tracking has been flawless.

@CJK's AppleScript, with the do script tweek, is the next best thing to a new Keyboard Maestro trigger: Desktop Space Trigger (The desktop space: The desktop space changes).

*The logic in Go to Previous Desktop that ran when the macro was triggered by The focus window changes is now triggered by AppleScript.


@August, as I mentioned above, the published versions of my macros/subroutines already track the Current and Previous desktops, albeit not as efficiently because I'm using the The focus window changes trigger. However, I'm unlikely to upload the test versions because @CJK's pseudo trigger requires additional setup that could deter new users. However, if @peternlewis is able to leverage @CJK's insight to create a new trigger in Keyboard Maestro, I'll certainly update my macro/subroutine set.

Thanks @Nige_S,

I'm running Mojave still, so maybe that's the difference. In Script Editor, there is no Save As option, only Export. When I choose Export, then there are File Format options. If I pick Application, then I can optionally choose "Stay open after run handler".

WHICH BEGS another question, aimed at EVERYONE:
When I run the Stay Open app, how do I quit it?

Update: Rebooting worked. The problem has not recurred.

It doesn't show up in the Activity Monitor or in the Force Quit menu. Everything that I've found by Googling says to simply Quit the app, sometimes they say to use its Quit button, one example even implying that the Quit button will be available automatically somehow. But there's no button, no window for this app while it's running in Stay Open mode.

How do I quit the Stay Open AppleScript app?

I tried starting it a second time. This time CJKsNotifyOnSpaceChange shows up in Activity Monitor. When I select it there and click the Info button, I see some process information and a Quit button. I can quit that instance. But the original one that I started this morning is still running and I can't find it to quit it.

I created a new script/app with a new name, this one adding the operation to call a KBM macro. When I export that as an app I also got the option to show a start menu. I did that and got Run and Quit buttons, but that dialog goes away when I press Run, so there's still no Quit button. That app name shows up in both Activity Monitor and in the Force Quit menu, so I can quit it.

And the hidden version continues to notify me of Space Changes, and I can quit it.

I really don't like rebooting having to be part of my script debugging cycle, but maybe that's it. But how did I get there?

Could I have created this "ghost process" by changing the name of the file while it was running?

As with many "modernised' Apple apps -- hold down the Option key to get "Save As...".

Then something's not right. You should get a Dock icon while the app is running, minimal menus when you bring it to the front, and a "Quit" item in your app's main menu. You can force-quit via the Dock icon or the Force Quit dialog, etc.

It really is just another app. It sounds like yours is quitting/crashing, which is something in your script.

Try making a test app with the following, which deliberately puts you into the Finder, throws a dialog to show you it's running, then you can switch back and ⌘Q to quit.

tell application "Finder"
	display dialog "Application running..."
end tell

Rebooting seems to have cleared up the funny, zombie app. After rebooting, running the defined app seems to behave normally. I see the ScriptEditor icon in the Dock, it shows up in the Force Quit menu and in Activity Monitor, just like it's supposed to. The Dock icon has a right-click choice of Quit, which works, although it takes a few seconds.

I don't know how it happened, but apparently I did something. It was the very first app that I created from AppleScript, but even though it was a verbatim copy of CJK's, it still could well have been operator error. If it reoccurs, I'll did deeper.

Thanks.

If you try my test app above you'll probably find it insta-quits -- that's because it doesn't do anything when you kill it! But @CJK's is a proper app and you'll see it has an on quit handler that removes itself from the Notification Centre process -- the delay is while it waits for Notification Center's "OK, that's done" response before finally quitting.

Thanks Nige! This is very helpful in beginning to grasp how this all goes together.

I've documented existing Observers and Handlers before, so I've had some of the basic concepts, but I never had to explain how to create one, or what a proper one should include.

Why isn't this AppleScript working to call Keyboard Maestro?

Update: This got answered by @Nige_S and @CJK below, most particularly by

I got @CJK's Applescript to run and give me Notifications whenenver the Desktop Workspace changed. Sweet. So I started adding the calls to WhichSpace and CurrentKey, via AppleScript in KBM. Also Sweet, Working fine to keep track of both the Desktop Number (as displayed by WhichSpace) and the CurrentKey Room Name, both Current and Previous. Lovely.

Then it occurred to me, why am I having AppleScript tell KBM to run a macro that then calls AppleScript twice to get the current Space Number and Name. Why not fold that into the initial AppleScript? That would require getting the name and number to KBM as parameters on the call to run the KBM macro. That should be simple, no? [Update: No.]

So I've spent all day on it and there's something basic that I'm not getting.

In my expanded AppleScript, I added a notification that the Observer had started, and identified the current Space Number and Room Name in the Notification. Works fine. I added a Notification that the quit: routine had run as well. No problem.

But something has gone wrong when trying to run the notify: block as triggered by the Observer. If I'm lucky, it does nothing. Most of variations in the coding that I've been trying have resulted in crashing the Script Editor. From SE I save the app as an app, with the Stay Open option set. Then I open the app by doubleclicking it in Finder. The startup Notification appears, I change Spaces, and Script Editor crashes.

And for every change, when I save it as the same app name, it's internally a different app, so I have to remove Accessibility Permission for the old version of the name and add Accessibility to the new version of the name before I can run it. So each interation takes several minutes, a complicated save, setting Accessibility permissions, running it, testing it, restartng ScriptEditor and moving the SE window back the desktop space that I started in. [Update: This awkward workflow is primary reason for my ultimate decision to do all the complicated stuff inside KBM and to keep the initial AppleScript notification as simple as possible, so that it doesn't need changes.]

I'd be grateful if someone could help me figure out where I don't have things declared, or they're improperly nested, or should be in quotes, or whatever simple thing it is that I'm overlooking.

Thanks.

Here's the script:

use framework "AppKit"
use scripting additions

on run
	tell application "System Events" to tell process ¬
		"WhichSpace" to set mySpaceNumberCurrent ¬
		to (title of menu bar items of menu bar 1)
	
	tell application "CurrentKey Stats"
		set myRoomNameCurrent to getactiveroom
	end tell
	
	tell my NSWorkspace's sharedWorkspace's notificationCenter ¬
		to addObserver:me selector:("notify:") ¬
		|name|:(my NSWorkspaceActiveSpaceDidChangeNotification) ¬
		object:(missing value)
	
	set DateNow to (current date) as «class isot» as string
	display notification "Space Observer Started" & ¬
		" in Space: " & mySpaceNumberCurrent & "
in Room: " & myRoomNameCurrent ¬
		with title DateNow
	
end run

on idle
	
end idle

on quit
	set DateNow to (current date) as «class isot» as string
	display notification ¬
		"Space Observer Removed" with title DateNow
	
	tell my NSWorkspace's sharedWorkspace's ¬
		notificationCenter to removeObserver:me
	
	continue quit
end quit

to notify:message
	(*set DateNow to (current date) as «class isot» as string
		display notification ¬
			"Space Changed" & mySpaceNumberCurrent & "," & mySpaceNumberPrevious ¬
			with title DateNow
		*)
	tell application "Finder"
		set mySpaceNumberPrevious to mySpaceNumberCurrent
		set myRoomNamePrevious to myRoomNameCurrent
	end tell
	
	tell application "System Events" to tell process "WhichSpace"
		set mySpaceNumberCurrent ¬
			to (title of menu bar items of menu bar 1)
	end tell
	
	tell application "CurrentKey Stats"
		set myRoomNameCurrent to getactiveroom
	end tell
	
	tell application "Keyboard Maestro Engine"
		do script "Got ActiveSpaceDidChangeNotification"
		--do script "Got ActiveSpaceDidChangeNotification" with parameter ¬
		--	mySpaceNumberCurrent & "," & mySpaceNumberPrevious
		
	end tell
	
end notify:

This is all new to me, but the first thing that leaps to mind is variable scoping.

@CJK's AppleScript starts the to notify... block with

global msgs

...presumably so it can access the msgs property. You may need to do the same for the variables in the first Finder block.

But I'm not entirely sure why you've go all those tell blocks in the notify section (or even if they work there!) -- functionally, it feels more appropriate for them to be in the run handler. Put them there, keep notify for the notification itself, and see if that solves your problem.

Thanks @Nige_S,
The tell blocks are in the notify: section because that's where they are meaningful.

My understanding is that the run block runs once, when the app starts. That's where the Observer gets initiated. The tell blocks inside that get the current values of the Desktop Number (from WhichSpace) and Room Name (from CurrentKey) for the current Desktop Workspace.

Then, if understand it correctly, the notify: block gets run every time the Observer sees that NSWorkspaceActiveSpaceDidChangeNotification is true, meaning that the Active Space changed, whether manually, automatically, by Mission Control or some app, it doesn't matter. So the first thing the notify: block does is re-do those tell blocks to WhatSpace and CurrentKey to find out what the new values of Desktop Number and Room Name are, saving the previous values as ...Previous values.

Then it calls the KBM macro, hopefully passing it the four values of Current Number and Name and Previous Number and Name. (Right now, the KBM call with the parameter, eventually a 4-tuple, is commented out while I attempt to get the rest working.)

Maybe you're right, maybe I'm trying to pack too much activity into the notify: block and asking it to keep track of all my variables rather than have it simply do the single task of notifying.

My previous version of this, where I did just that, and let KBM call WhichSpace and CurrentKey and keep track of the values, worked just fine, as far as I got with it. So if this doesn't shake out quickly, I'll revert to that.

It just seemed to me that it would be somehow more "efficient" to do all the AppleScript at once, in one place, instead of having AppleScript Notify KBM of the Space change and then have KBM turn around and ask AppleScript to get the Space Number and Room Name from WhichSpace and CurrentKey.

Maybe that's a mistaken idea of "efficiency". I've seen AppleScript debugging threads on Stack Overflow and Ask Different where someone explained that it didn't work to have the app name in a tell block be specified by a variable because the AppleScript interpreter looks at what app you are talking to and interpret the rest of your commands within that context, with different terms and functionality available for different apps. Maybe it's like that, where the AppleScript interpreter has a limited idea of what someone will be doing or would ever want to do within a notify: block, and it gets confused when I try to do non-notifying kinds of things.

As I said above, I have a working route that I can revert to, I just thought this would be somehow "better". And maybe it's a case of Apple insisting that Apple knows best.

Thanks.

My bad -- I was reading it sort-of upside down. Never noticed a to block before, and hadn't realised it was a synonym for on (if, in fact, it is!).

So I'm a bit out of my depth. That won't stop from me taking another stab, though!

I'm still worried about your variable scoping. As I understand it, defining eg mySpaceNumberCurrent in the run block makes it local to the run block. So I'm not confident that the reference to it in the notify block is valid.

See some old documentation about variable scope in both script objects and handlers. Note that @CJK used the construct just below Table 3-2, declaring msgs to be a global in the notify handler, forcing AS to look beyond the handler to resolve it.

Try putting

global mySpaceNumberCurrent
global myRoomNameCurrent

...at the start of the notify block, emulating @CJK's original script, and see what happens.